fix(files): preserve WSL filenames in watcher snapshots (#34855)

* fix(files): preserve WSL filenames in watcher snapshots

* test(files): drop shell-producer WSL snapshot test

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-10-28 12:51:45 -07:00
committed by GitHub
co-authored by OrcaWin
parent 4ce1e18930
commit 278b02b27a
2 changed files with 37 additions and 9 deletions
@@ -19,8 +19,8 @@ import {
} from '../watcher-process/parcel-watcher-child-registry'
import type { WatchedRoot, WslWatcherDeps } from './filesystem-watcher-wsl'
const SNAPSHOT_START = '\x1e'
const SNAPSHOT_END = '\x1f'
const SNAPSHOT_START = '\0\x1e\0'
const SNAPSHOT_END = '\0\x1f\0'
const ROOT_KEY = '\\\\wsl.localhost\\Ubuntu\\home\\me\\repo'
class FakeChildProcess extends EventEmitter {
@@ -173,6 +173,34 @@ describe('createWslWatcher', () => {
])
})
it('preserves control characters in filenames across byte-split snapshot frames', async () => {
const { child, promise, deps } = startWatcher()
child.stdout.write(snapshotFrame([]))
const root = await promise
const names = [
'start\x1einside.txt',
'end\x1finside.txt',
'trailing\x1f',
'tab\tline\n漢字.txt'
]
const frame = (mtime: string) =>
snapshotFrame(names.map((name) => ['f', mtime, `/home/me/repo/${name}`]))
for (const mtime of ['1.0', '2.0']) {
for (const byte of Buffer.from(frame(mtime))) {
child.stdout.write(Buffer.from([byte]))
}
}
child.stdout.write(snapshotFrame([]))
expect(root.batch.events).toEqual(
['create', 'update', 'delete'].flatMap((type) =>
names.map((name) => ({ type, path: `${ROOT_KEY}\\${name}` }))
)
)
expect(deps.scheduleBatchFlush).toHaveBeenCalledTimes(3)
await root.subscription.unsubscribe()
})
it('turns watcher exit into an overflow refresh without retaining UNC paths', async () => {
const scheduleBatchFlush = vi.fn()
const deps = makeDeps(scheduleBatchFlush)
@@ -32,7 +32,7 @@ export type WslWatcherDeps = {
const POLL_INTERVAL_SECONDS = 2
const STARTUP_TIMEOUT_MS = 10_000
const [SNAPSHOT_START, SNAPSHOT_END] = ['\x1e', '\x1f']
const [SNAPSHOT_START, SNAPSHOT_END] = ['\0\x1e\0', '\0\x1f\0']
const MAX_STREAM_BUFFER_CHARS = 10 * 1024 * 1024
type WslSnapshotEntry = {
@@ -68,11 +68,11 @@ function buildSnapshotScript(ignoreDirs: readonly string[]): string {
'set -efu',
'root=$1',
'while :; do',
" printf '\\036'",
" printf '\\000\\036\\000'",
' if [ -d "$root" ]; then',
` find "$root" -mindepth 1 -maxdepth 2 ${prune} -printf '%y\\t%T@\\t%p\\0' 2>/dev/null || true`,
' fi',
" printf '\\037'",
" printf '\\000\\037\\000'",
` sleep ${POLL_INTERVAL_SECONDS} || exit 0`,
'done'
].join('\n')
@@ -211,13 +211,13 @@ export async function createWslWatcher(
while (true) {
const start = streamBuffer.indexOf(SNAPSHOT_START)
if (start === -1) {
streamBuffer = streamBuffer.slice(-1)
streamBuffer = streamBuffer.slice(-(SNAPSHOT_START.length - 1))
return
}
if (start > 0) {
streamBuffer = streamBuffer.slice(start)
}
const end = streamBuffer.indexOf(SNAPSHOT_END, 1)
const end = streamBuffer.indexOf(SNAPSHOT_END, SNAPSHOT_START.length)
if (end === -1) {
if (streamBuffer.length > MAX_STREAM_BUFFER_CHARS) {
streamBuffer = ''
@@ -226,8 +226,8 @@ export async function createWslWatcher(
}
return
}
const frame = streamBuffer.slice(1, end)
streamBuffer = streamBuffer.slice(end + 1)
const frame = streamBuffer.slice(SNAPSHOT_START.length, end)
streamBuffer = streamBuffer.slice(end + SNAPSHOT_END.length)
ingestFrame(frame)
}
}