mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 00:03:46 +00:00
fix(wsl): watch the whole worktree so nested external edits reach open editors
The WSL in-distro poll scan was capped at `-maxdepth 2`, so external edits to any file nested deeper never produced an `fs:changed` event. An open editor tab kept its in-memory content until it was closed and reopened — the reported workaround. Remove the depth cap so the scan matches the recursive watcher behaviour on native platforms. Fixes #7792 Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -174,6 +174,36 @@ describe('createWslWatcher', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('emits update events for files nested more than two levels deep', async () => {
|
||||
const scheduleBatchFlush = vi.fn()
|
||||
const { child, promise } = startWatcher(makeDeps(scheduleBatchFlush))
|
||||
|
||||
child.stdout.write(
|
||||
snapshotFrame([
|
||||
['d', '1.0', '/home/me/repo/docs'],
|
||||
['d', '1.0', '/home/me/repo/docs/api'],
|
||||
['f', '1.0', '/home/me/repo/docs/api/reference.md']
|
||||
])
|
||||
)
|
||||
const root = await promise
|
||||
child.stdout.write(
|
||||
snapshotFrame([
|
||||
['d', '1.0', '/home/me/repo/docs'],
|
||||
['d', '1.0', '/home/me/repo/docs/api'],
|
||||
['f', '2.0', '/home/me/repo/docs/api/reference.md'],
|
||||
['f', '1.0', '/home/me/repo/src/main/deep/new.md']
|
||||
])
|
||||
)
|
||||
|
||||
expect(root.batch.events).toEqual([
|
||||
{
|
||||
type: 'update',
|
||||
path: '\\\\wsl.localhost\\Ubuntu\\home\\me\\repo\\docs\\api\\reference.md'
|
||||
},
|
||||
{ type: 'create', path: '\\\\wsl.localhost\\Ubuntu\\home\\me\\repo\\src\\main\\deep\\new.md' }
|
||||
])
|
||||
})
|
||||
|
||||
it('turns watcher exit into an overflow refresh without retaining UNC paths', async () => {
|
||||
const scheduleBatchFlush = vi.fn()
|
||||
const deps = makeDeps(scheduleBatchFlush)
|
||||
|
||||
@@ -13,6 +13,7 @@ import { queueWatcherEvents } from './filesystem-watcher-event-batch'
|
||||
import { parseWslUncPath } from '../../shared/wsl-paths'
|
||||
import { createWslWatcherProcessExit, createWslWatcherStartup } from './wsl-watcher-process-exit'
|
||||
import { reserveWatcherChild, WatcherChildCapacityError } from './parcel-watcher-child-registry'
|
||||
import { buildSnapshotScript } from './wsl-snapshot-poll-script'
|
||||
|
||||
export type WatcherSubscription = {
|
||||
unsubscribe(): Promise<void>
|
||||
@@ -40,7 +41,6 @@ export type WslWatcherDeps = {
|
||||
watchedRoots: Map<string, WatchedRoot>
|
||||
}
|
||||
|
||||
const POLL_INTERVAL_SECONDS = 2
|
||||
const STARTUP_TIMEOUT_MS = 10_000
|
||||
const [SNAPSHOT_START, SNAPSHOT_END] = ['\x1e', '\x1f']
|
||||
const MAX_STREAM_BUFFER_CHARS = 10 * 1024 * 1024
|
||||
@@ -57,37 +57,6 @@ function toWslUncPath(linuxPath: string, distro: string): string {
|
||||
return `\\\\wsl.localhost\\${distro}${linuxPath.replace(/\//g, '\\')}`
|
||||
}
|
||||
|
||||
function quoteSafeFindName(name: string): string {
|
||||
if (!/^[A-Za-z0-9_.-]+$/.test(name)) {
|
||||
throw new Error(`Unsupported WSL watcher ignore name: ${name}`)
|
||||
}
|
||||
return `'${name}'`
|
||||
}
|
||||
|
||||
function buildPruneExpression(ignoreDirs: readonly string[]): string {
|
||||
if (ignoreDirs.length === 0) {
|
||||
return ''
|
||||
}
|
||||
const names = ignoreDirs.map((name) => `-name ${quoteSafeFindName(name)}`).join(' -o ')
|
||||
return `\\( -type d \\( ${names} \\) -prune \\) -o`
|
||||
}
|
||||
|
||||
function buildSnapshotScript(ignoreDirs: readonly string[]): string {
|
||||
const prune = buildPruneExpression(ignoreDirs)
|
||||
return [
|
||||
'set -efu',
|
||||
'root=$1',
|
||||
'while :; do',
|
||||
" printf '\\036'",
|
||||
' 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'",
|
||||
` sleep ${POLL_INTERVAL_SECONDS} || exit 0`,
|
||||
'done'
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
function parseSnapshotFrame(frame: string, distro: string): WslSnapshot {
|
||||
const snapshot: WslSnapshot = new Map()
|
||||
for (const rawEntry of frame.split('\0')) {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { buildSnapshotScript } from './wsl-snapshot-poll-script'
|
||||
|
||||
function scanLine(script: string): string {
|
||||
const line = script.split('\n').find((entry) => entry.includes('find "$root"'))
|
||||
expect(line).toBeDefined()
|
||||
return line as string
|
||||
}
|
||||
|
||||
describe('buildSnapshotScript', () => {
|
||||
it('scans the whole worktree so files nested below two levels are seen', () => {
|
||||
const line = scanLine(buildSnapshotScript(['node_modules', '.git']))
|
||||
|
||||
expect(line).toContain('-mindepth 1')
|
||||
expect(line).not.toContain('-maxdepth')
|
||||
})
|
||||
|
||||
it('prunes ignored directories at every depth', () => {
|
||||
const line = scanLine(buildSnapshotScript(['node_modules', '.git']))
|
||||
|
||||
expect(line).toContain("\\( -type d \\( -name 'node_modules' -o -name '.git' \\) -prune \\) -o")
|
||||
})
|
||||
|
||||
it('backs the poll interval off on large trees instead of capping depth', () => {
|
||||
const script = buildSnapshotScript(['node_modules'])
|
||||
|
||||
expect(script).toContain('size=$(wc -c < "$snapshot" 2>/dev/null || echo 0)')
|
||||
expect(script).toContain('interval=10')
|
||||
expect(script).toContain('sleep "$interval" || exit 0')
|
||||
})
|
||||
|
||||
it('fails loudly on distros whose find lacks GNU -printf', () => {
|
||||
expect(buildSnapshotScript([])).toContain('orca-watcher-unsupported-find')
|
||||
})
|
||||
|
||||
it('rejects ignore names that would break out of the find expression', () => {
|
||||
expect(() => buildSnapshotScript(["a' -o -delete '"])).toThrow(
|
||||
'Unsupported WSL watcher ignore name'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* The `sh` program the WSL watcher pipes into the distro: it polls the worktree
|
||||
* with `find` and streams one framed snapshot per interval.
|
||||
*/
|
||||
const POLL_INTERVAL_SECONDS = 2
|
||||
// Why: a recursive scan of a huge tree costs more per poll than the freshness it
|
||||
// buys, so back the in-distro loop off instead of capping depth (which blinds it).
|
||||
const SLOW_POLL_INTERVAL_SECONDS = 10
|
||||
const LARGE_SNAPSHOT_BYTES = 2_000_000
|
||||
|
||||
function quoteSafeFindName(name: string): string {
|
||||
if (!/^[A-Za-z0-9_.-]+$/.test(name)) {
|
||||
throw new Error(`Unsupported WSL watcher ignore name: ${name}`)
|
||||
}
|
||||
return `'${name}'`
|
||||
}
|
||||
|
||||
function buildPruneExpression(ignoreDirs: readonly string[]): string {
|
||||
if (ignoreDirs.length === 0) {
|
||||
return ''
|
||||
}
|
||||
const names = ignoreDirs.map((name) => `-name ${quoteSafeFindName(name)}`).join(' -o ')
|
||||
return `\\( -type d \\( ${names} \\) -prune \\) -o`
|
||||
}
|
||||
|
||||
export function buildSnapshotScript(ignoreDirs: readonly string[]): string {
|
||||
const prune = buildPruneExpression(ignoreDirs)
|
||||
return [
|
||||
'set -efu',
|
||||
'root=$1',
|
||||
// Why: BusyBox/Alpine find has no -printf; without this probe every frame is
|
||||
// silently empty and the watcher looks healthy while seeing nothing.
|
||||
"find / -maxdepth 0 -printf '' 2>/dev/null || { printf 'orca-watcher-unsupported-find\\n' >&2; exit 3; }",
|
||||
'snapshot=$(mktemp) || exit 4',
|
||||
`trap 'rm -f "$snapshot"' EXIT`,
|
||||
`trap 'rm -f "$snapshot"; exit 0' INT TERM`,
|
||||
`interval=${POLL_INTERVAL_SECONDS}`,
|
||||
'while :; do',
|
||||
' : > "$snapshot"',
|
||||
' if [ -d "$root" ]; then',
|
||||
// The prune expression drops .git/node_modules/etc at every depth, so full
|
||||
// recursion here matches what @parcel/watcher reports on native platforms.
|
||||
` find "$root" -mindepth 1 ${prune} -printf '%y\\t%T@\\t%p\\0' > "$snapshot" 2>/dev/null || true`,
|
||||
' fi',
|
||||
" printf '\\036'",
|
||||
' cat "$snapshot"',
|
||||
" printf '\\037'",
|
||||
' size=$(wc -c < "$snapshot" 2>/dev/null || echo 0)',
|
||||
` if [ "\${size:-0}" -gt ${LARGE_SNAPSHOT_BYTES} ]; then interval=${SLOW_POLL_INTERVAL_SECONDS}; else interval=${POLL_INTERVAL_SECONDS}; fi`,
|
||||
' sleep "$interval" || exit 0',
|
||||
'done'
|
||||
].join('\n')
|
||||
}
|
||||
Reference in New Issue
Block a user