diff --git a/src/main/wsl/wsl-automount-root.test.ts b/src/main/wsl/wsl-automount-root.test.ts index a43ac4866e7..0e274eb9a3e 100644 --- a/src/main/wsl/wsl-automount-root.test.ts +++ b/src/main/wsl/wsl-automount-root.test.ts @@ -6,6 +6,7 @@ vi.mock('../wsl', () => ({ listRunningWslDistrosAsync: vi.fn(), listWslDistrosAs import { _resetWslBackgroundDistroGateForTests, noteWslDistroMayHaveStopped, + noteWslDistroWoken, WSL_STOP_SETTLE_MS } from '../wsl-background-distro-gate' import { @@ -17,7 +18,8 @@ import { knownWslAutomountRoots, onWslAutomountRootsChanged, parseWslAutomountRoot, - resetWslAutomountRootsForTests + resetWslAutomountRootsForTests, + WSL_AUTOMOUNT_WAKE_REREAD_INTERVAL_MS } from './wsl-automount-root' let now = 1_000 @@ -72,6 +74,7 @@ describe('followWslAutomountRoots', () => { expect(changed).toHaveBeenCalledOnce() await listRunning([]) expect(knownWslAutomountRoots()).toEqual(['/mnt/']) + now += WSL_STOP_SETTLE_MS await listRunning(['Ubuntu']) await vi.waitFor(() => expect(read).toHaveBeenCalledTimes(2)) }) @@ -120,6 +123,7 @@ describe('followWslAutomountRoots: one guest read per distro start', () => { expect(reads).toBe(1) await listRunning([]) + now += WSL_STOP_SETTLE_MS await observeRunning(10) expect(reads).toBe(2) } @@ -158,7 +162,65 @@ describe('followWslAutomountRoots: one guest read per distro start', () => { await listRunning([]) finishFirst('/mnt/') await new Promise((resolve) => setImmediate(resolve)) + now += WSL_STOP_SETTLE_MS await observeRunning(3) expect(reads).toBe(2) }) + + // Terminals opened on a running distro each note a wake; only the interval lets one re-read through. + it('reads at most once more for many wakes while the distro runs', async () => { + let reads = 0 + follow(async () => { + reads += 1 + return '/mnt/' + }) + await observeRunning(1) + expect(reads).toBe(1) + + for (let i = 0; i < 10; i++) { + now += 1_000 + noteWslDistroWoken('Ubuntu') + await observeRunning(1) + } + expect(reads).toBe(1) + + now += WSL_AUTOMOUNT_WAKE_REREAD_INTERVAL_MS + for (let i = 0; i < 10; i++) { + noteWslDistroWoken('Ubuntu') + now += 1 + await observeRunning(1) + } + expect(reads).toBe(2) + }) + + it('picks up a root edited across a restart no probe saw, once Orca wakes the distro', async () => { + let root = '/old/' + const changed = vi.fn() + onWslAutomountRootsChanged(changed) + follow(async () => root) + await observeRunning(1) + expect(knownWslAutomountRoots()).toEqual(['/mnt/', '/old/']) + + // An unchanged re-read leaves listeners (the git router's cache) alone. + now += WSL_AUTOMOUNT_WAKE_REREAD_INTERVAL_MS + noteWslDistroWoken('Ubuntu') + now += 1 + await observeRunning(1) + expect(changed).toHaveBeenCalledOnce() + + // Stopped and started again between probes, wsl.conf edited meanwhile. + root = '/new/' + now += WSL_AUTOMOUNT_WAKE_REREAD_INTERVAL_MS + noteWslDistroWoken('Ubuntu') + now += 1 + await observeRunning(1) + expect(knownWslAutomountRoots()).toEqual(['/mnt/', '/new/']) + + root = '/mnt/' + now += WSL_AUTOMOUNT_WAKE_REREAD_INTERVAL_MS + noteWslDistroWoken('Ubuntu') + now += 1 + await observeRunning(1) + expect(knownWslAutomountRoots()).toEqual(['/mnt/']) + }) }) diff --git a/src/main/wsl/wsl-automount-root.ts b/src/main/wsl/wsl-automount-root.ts index f74e8cb4025..c108bbcb4c1 100644 --- a/src/main/wsl/wsl-automount-root.ts +++ b/src/main/wsl/wsl-automount-root.ts @@ -2,12 +2,17 @@ * Where each WSL project's distro mounts Windows drives (`[automount] root`, `/mnt/` by default), * so a worktree link WSL Git wrote as `c/src/app/.git/worktrees/x` maps back to `C:\…`. * Read only from a distro confirmed running after any stop settled, never waking a stopped one, - * and read again after it stops, since `wsl.conf` changes take effect on the next start. + * and read again after it stops, since `wsl.conf` changes take effect on the next start. A stop and + * restart between two probes goes unseen, so Orca's own wake also allows one re-read; a restart + * outside Orca with no wake after it keeps the old root until the next noticed stop or wake. */ import { runProcess } from '@orca/process-host' import { buildPosixStdoutFence } from '../../shared/posix-stdout-fence' import { buildWslExecArgs } from '../../shared/wsl-login-shell-command' -import { wasWslDistroLastSeenRunningSettled } from '../wsl-background-distro-gate' +import { + onWslDistroGateNote, + wasWslDistroLastSeenRunningSettled +} from '../wsl-background-distro-gate' import { resolveWslInteropSpawnCwd } from '../wsl-interop-spawn-directory' import { onConfirmedRunningWslDistros, @@ -18,12 +23,15 @@ import { resolveWslExecutablePath } from './wsl-executable-path' export const DEFAULT_WSL_AUTOMOUNT_ROOT = '/mnt/' const READ_TIMEOUT_MS = 5_000 +// A wake re-reads at most this often per distro, so terminals opened on a running distro stay free. +export const WSL_AUTOMOUNT_WAKE_REREAD_INTERVAL_MS = 5 * 60_000 export type WslAutomountRootReader = (distro: string) => Promise const roots = new Map() // Read since its last start, the default `/mnt/` too: one guest launch per start, not per probe. const readSinceStart = new Set() +const readAt = new Map() const pending = new Set() // Bumped on a stop, so a read still in flight then neither marks the restarted distro read. const stopCounts = new Map() @@ -78,10 +86,17 @@ export function followWslAutomountRoots( return } readSinceStart.add(key) - if (root !== DEFAULT_WSL_AUTOMOUNT_ROOT) { - roots.set(key, root) - changed() + readAt.set(key, performance.now()) + const previous = roots.get(key) ?? DEFAULT_WSL_AUTOMOUNT_ROOT + if (root === previous) { + return } + if (root === DEFAULT_WSL_AUTOMOUNT_ROOT) { + roots.delete(key) + } else { + roots.set(key, root) + } + changed() }) // Unreadable: links under it fall back to `/mnt/`. .catch(() => {}) @@ -94,15 +109,30 @@ export function followWslAutomountRoots( const key = distroKey(distro) stopCounts.set(key, (stopCounts.get(key) ?? 0) + 1) readSinceStart.delete(key) + readAt.delete(key) removed = roots.delete(key) || removed } if (removed) { changed() } }) + // A wake may have restarted it unseen: the next settled probe reads again, its old root kept until then. + const stopWoken = onWslDistroGateNote((note) => { + const key = distroKey(note.distro) + const lastRead = readAt.get(key) + if ( + note.kind === 'woken' && + readSinceStart.has(key) && + lastRead !== undefined && + performance.now() - lastRead >= WSL_AUTOMOUNT_WAKE_REREAD_INTERVAL_MS + ) { + readSinceStart.delete(key) + } + }) return () => { stopRunning() stopStopped() + stopWoken() } } @@ -146,6 +176,7 @@ export function parseWslAutomountRoot(drivePath: string | null): string | null { export function resetWslAutomountRootsForTests(): void { roots.clear() readSinceStart.clear() + readAt.clear() pending.clear() stopCounts.clear() changeListeners.clear()