From 278b02b27ab5cad4d32f8aadb9352ea09e885ff7 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Wed, 28 Oct 2026 12:51:45 -0700 Subject: [PATCH] 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> --- .../watchers/filesystem-watcher-wsl.test.ts | 32 +++++++++++++++++-- .../watchers/filesystem-watcher-wsl.ts | 14 ++++---- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/main/ipc/filesystem/watchers/filesystem-watcher-wsl.test.ts b/src/main/ipc/filesystem/watchers/filesystem-watcher-wsl.test.ts index bc533eaeb21..0052db17899 100644 --- a/src/main/ipc/filesystem/watchers/filesystem-watcher-wsl.test.ts +++ b/src/main/ipc/filesystem/watchers/filesystem-watcher-wsl.test.ts @@ -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) diff --git a/src/main/ipc/filesystem/watchers/filesystem-watcher-wsl.ts b/src/main/ipc/filesystem/watchers/filesystem-watcher-wsl.ts index 7850d83d2b9..0e83142fb9f 100644 --- a/src/main/ipc/filesystem/watchers/filesystem-watcher-wsl.ts +++ b/src/main/ipc/filesystem/watchers/filesystem-watcher-wsl.ts @@ -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) } }