fix(wsl): never boot a stopped distro to check a folder workspace's path (#35899)

A background read of a \\wsl.localhost folder workspace's status (sidebar
polling, runtime status RPC, a passive terminal restore) stat'ed the folder
and booted its stopped distro. The status now reports 'unavailable' without
touching it; a passive restore holds with Start; a spawn the user asked for
checks the folder as their act, which may start the distro as before.
This commit is contained in:
Neil
2026-10-29 03:32:56 -07:00
committed by GitHub
parent d865074b8f
commit bccdb0254f
7 changed files with 172 additions and 13 deletions
@@ -0,0 +1,112 @@
import type * as FsPromises from 'node:fs/promises'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as BackgroundDistroGateModule from '../../../wsl-background-distro-gate'
import type * as WslMoveScopeGuardModule from '../../../persistence/moving-wsl-projects/wsl-move-scope-guard'
import type { Store } from '../../../persistence'
import type { FolderWorkspace } from '../../../../shared/folder-workspace-types'
import { folderWorkspaceKey } from '../../../../shared/workspace-scope'
import { wslDistroHoldMessage } from '../../../../shared/wsl-distro-stopped-hold'
import { isWslUserActFor } from '../../../wsl/wsl-user-act-scope'
const mocks = vi.hoisted(() => ({
stat: vi.fn<(path: string) => Promise<{ isDirectory(): boolean }>>(),
readWslDistroRunningState: vi.fn<() => Promise<'running' | 'stopped' | 'unverifiable'>>()
}))
vi.mock('node:fs/promises', async (importOriginal) => ({
...(await importOriginal<typeof FsPromises>()),
stat: mocks.stat
}))
vi.mock('../../../wsl-background-distro-gate', async (importOriginal) => ({
...(await importOriginal<typeof BackgroundDistroGateModule>()),
readWslDistroRunningState: mocks.readWslDistroRunningState
}))
vi.mock(
'../../../persistence/moving-wsl-projects/wsl-move-scope-guard',
async (importOriginal) => ({
...(await importOriginal<typeof WslMoveScopeGuardModule>()),
isWslMoveCwdFenced: () => false,
isWslMoveWorkspaceBlocked: () => false
})
)
import { assertWorkspaceAcceptsNewPty } from './spawn-cwd'
import { getFolderWorkspacePathStatus } from '../../../project-groups/folder-workspace-path-status'
const FOLDER = '\\\\wsl.localhost\\Ubuntu\\home\\me\\notes'
const WORKTREE_ID = folderWorkspaceKey('folder-1')
const originalPlatform = process.platform
function makeStore(): Store {
const workspace: FolderWorkspace = {
id: 'folder-1',
projectGroupId: 'group-1',
name: 'notes',
folderPath: FOLDER,
linkedTask: null,
comment: '',
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 1,
createdAt: 1,
updatedAt: 1
}
const store = {
getRepos: () => [],
getProjectGroups: () => [],
getFolderWorkspaces: () => [workspace]
}
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the folder check reads only these three store methods.
return store as unknown as Store
}
beforeEach(() => {
Object.defineProperty(process, 'platform', { value: 'win32' })
mocks.stat.mockReset()
mocks.stat.mockImplementation(async () => {
expect(isWslUserActFor('Ubuntu')).toBe(true)
return { isDirectory: () => true }
})
mocks.readWslDistroRunningState.mockReset()
mocks.readWslDistroRunningState.mockResolvedValue('stopped')
})
afterEach(() => {
Object.defineProperty(process, 'platform', { value: originalPlatform })
})
describe('a folder workspace on a stopped WSL distro', () => {
it('reports the folder unavailable to a background status read without touching it', async () => {
await expect(
getFolderWorkspacePathStatus(makeStore(), {
scope: 'folder-workspace',
folderWorkspaceId: 'folder-1'
})
).resolves.toEqual({ path: FOLDER, exists: false, reason: 'unavailable' })
expect(mocks.stat).not.toHaveBeenCalled()
})
it('holds a passive restore with Start instead of checking the folder', async () => {
await expect(
assertWorkspaceAcceptsNewPty(makeStore(), WORKTREE_ID, 'session-1', undefined, true)
).rejects.toThrow(wslDistroHoldMessage('Ubuntu', 'stopped'))
expect(mocks.stat).not.toHaveBeenCalled()
})
it('checks the folder as the user act for a spawn the user asked for', async () => {
await expect(
assertWorkspaceAcceptsNewPty(makeStore(), WORKTREE_ID, undefined, undefined, false)
).resolves.toBeUndefined()
expect(mocks.stat).toHaveBeenCalledExactlyOnceWith(FOLDER)
})
it('lets a passive restore check the folder once the distro runs', async () => {
mocks.readWslDistroRunningState.mockResolvedValue('running')
mocks.stat.mockResolvedValue({ isDirectory: () => true })
await expect(
assertWorkspaceAcceptsNewPty(makeStore(), WORKTREE_ID, 'session-1', undefined, true)
).resolves.toBeUndefined()
expect(mocks.stat).toHaveBeenCalledExactlyOnceWith(FOLDER)
})
})
+42 -7
View File
@@ -8,7 +8,8 @@ import {
} from '../../../../shared/terminal-startup-cwd'
import {
assertFolderWorkspacePathUsable,
getFolderWorkspacePathStatus
getFolderWorkspacePathStatus,
resolveFolderWorkspaceStatusPath
} from '../../../project-groups/folder-workspace-path-status'
import {
isWslMoveCwdFenced,
@@ -18,6 +19,12 @@ import {
WslMoveProjectFencedError
} from '../../../persistence/moving-wsl-projects/wsl-move-scope-guard'
import { listLiveDaemonPtyIds } from '../../../daemon/daemon-provider-state'
import { wslDistroHoldMessage } from '../../../../shared/wsl-distro-stopped-hold'
import {
blockedWslDistroForPath,
runAsWslUserAct,
wslDistroForPath
} from '../../../wsl/wsl-unc-access-gate'
/**
* A new PTY needs a workspace no WSL move is taking, and a folder that still exists. Reattaching to
@@ -29,15 +36,16 @@ export function assertWorkspaceAcceptsNewPty(
store: Store | undefined,
worktreeId: string | undefined,
sessionId?: string,
cwd?: string
cwd?: string,
passiveWslRestore?: boolean
): Promise<void> | void {
const fencedByCwd = !!cwd && isWslMoveCwdFenced(cwd)
if (fencedByCwd || isWslMoveWorkspaceBlocked(store, worktreeId)) {
return assertReattachesLiveSession(sessionId, worktreeId).then(() =>
assertFolderWorkspacePtyPathUsable(store, worktreeId)
assertFolderWorkspacePtyPathUsable(store, worktreeId, passiveWslRestore)
)
}
return assertFolderWorkspacePtyPathUsable(store, worktreeId)
return assertFolderWorkspacePtyPathUsable(store, worktreeId, passiveWslRestore)
}
/** Only a spawn on this machine starts in a folder a WSL move can be taking. */
@@ -64,16 +72,43 @@ async function assertReattachesLiveSession(
export function assertFolderWorkspacePtyPathUsable(
store: Store | undefined,
worktreeId: string | undefined
worktreeId: string | undefined,
passiveWslRestore = false
): Promise<void> | void {
const workspaceScope = typeof worktreeId === 'string' ? parseWorkspaceKey(worktreeId) : null
if (!store || workspaceScope?.type !== 'folder') {
return
}
return getFolderWorkspacePathStatus(store, {
const request = {
scope: 'folder-workspace',
folderWorkspaceId: workspaceScope.folderWorkspaceId
}).then(assertFolderWorkspacePathUsable)
} as const
return checkFolderWorkspacePtyPath(store, request, passiveWslRestore)
}
/**
* A spawn the user asked for may start the folder's stopped WSL distro (as before): its check runs
* as their act. A passive restore holds with Start instead, the same as a bridge pane's (E9).
*/
async function checkFolderWorkspacePtyPath(
store: Store,
request: { scope: 'folder-workspace'; folderWorkspaceId: string },
passiveWslRestore: boolean
): Promise<void> {
const { folderPath } = resolveFolderWorkspaceStatusPath({ store, request })
if (passiveWslRestore) {
const blocked = await blockedWslDistroForPath(folderPath)
if (blocked) {
throw new Error(wslDistroHoldMessage(blocked.distro, blocked.state))
}
assertFolderWorkspacePathUsable(await getFolderWorkspacePathStatus(store, request))
return
}
assertFolderWorkspacePathUsable(
await runAsWslUserAct(wslDistroForPath(folderPath), () =>
getFolderWorkspacePathStatus(store, request)
)
)
}
export function resolvePtySpawnStartupCwd(
+4 -2
View File
@@ -34,7 +34,8 @@ export async function preparePtyIpcSpawnPreflight(ctx: PtyIpcSpawnState): Promis
const admission = ctx.deps.assertWorkspaceAcceptsNewPty(
args.worktreeId,
args.sessionId,
localPtySpawnCwd(args)
localPtySpawnCwd(args),
args.wslPassiveRestore === true
)
if (admission) {
await admission
@@ -47,7 +48,8 @@ export async function preparePtyIpcSpawnPreflight(ctx: PtyIpcSpawnState): Promis
const admission = ctx.deps.assertWorkspaceAcceptsNewPty(
args.worktreeId,
args.sessionId,
localPtySpawnCwd(args)
localPtySpawnCwd(args),
args.wslPassiveRestore === true
)
if (admission) {
await admission
+2 -1
View File
@@ -120,7 +120,8 @@ export type PtySpawnIpcDeps = PtySpawnCodexResumeDeps & {
assertWorkspaceAcceptsNewPty: (
worktreeId: string | undefined,
sessionId?: string,
cwd?: string
cwd?: string,
passiveWslRestore?: boolean
) => Promise<void> | void
resolvePtySpawnStartupCwd: (
worktreeId: string | undefined,
+4 -2
View File
@@ -218,8 +218,10 @@ export function registerPtyHandlers(
const assertWorkspaceAcceptsNewPty = (
worktreeId: string | undefined,
sessionId?: string,
cwd?: string
): Promise<void> | void => assertWorkspaceAcceptsNewPtyImpl(store, worktreeId, sessionId, cwd)
cwd?: string,
passiveWslRestore?: boolean
): Promise<void> | void =>
assertWorkspaceAcceptsNewPtyImpl(store, worktreeId, sessionId, cwd, passiveWslRestore)
const resolvePtySpawnStartupCwd = (
worktreeId: string | undefined,
cwd: string | undefined,
+2 -1
View File
@@ -25,7 +25,8 @@ export type PtyRuntimeControllerDeps = PtySpawnCodexResumeDeps & {
assertWorkspaceAcceptsNewPty: (
worktreeId: string | undefined,
sessionId?: string,
cwd?: string
cwd?: string,
passiveWslRestore?: boolean
) => Promise<void> | void
resolvePtySpawnStartupCwd: (
worktreeId: string | undefined,
@@ -8,6 +8,7 @@ import { getProjectGroupSubtreeIds } from '../../shared/project-groups'
import type { FolderWorkspace } from '../../shared/folder-workspace-types'
import type { ProjectGroup } from '../../shared/project-group-types'
import type { Repo } from '../../shared/repo-types'
import { blockedWslDistroForPath } from '../wsl/wsl-unc-access-gate'
type FolderWorkspacePathStatusStore = {
getRepos: () => Repo[]
@@ -111,6 +112,11 @@ async function statFolderPath(
return { path, exists: false, reason: 'unavailable' }
}
// A stat of a stopped distro's \\wsl.localhost folder boots it (E9): unknown, never missing.
// A user act on the distro (runAsWslUserAct) passes and may start it.
if (await blockedWslDistroForPath(path)) {
return { path, exists: false, reason: 'unavailable' }
}
try {
const stats = await statLocalPath(path)
return stats.isDirectory()