mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 08:03:20 +00:00
* fix(wsl): name an explicit Windows cwd for wsl.exe spawns Removing the worktree Orca was launched from broke every wsl.exe spawn for the rest of the session. The WSL command builders passed `cwd: undefined` meaning "the directory is inside the command" -- but CreateProcessW reads NULL as "inherit the parent's", and the parent's was a \\wsl.localhost path Linux had just deleted. Fixes #16463 * fix(wsl): name the spawn directory at the six remaining wsl.exe sites The first commit fixed the WSL command builders. Six spawn sites were left inheriting the process cwd, which is the same deletable `\\wsl.localhost` worktree: `wsl-availability` (both probes), the WSL filesystem watcher, the agent-hook relay launch, the UNC delete, and the local worktree filesystem. `wsl-availability` is the one that matters most, and it turns the bug into a latching false negative. `isRetryableWslProbeFailure` returns false for ENOENT, so a spawn that failed only because the inherited cwd was gone is cached as "WSL is not installed" on the 10-minute definitive TTL with exponential backoff up to 30 minutes. Git keeps working and Orca reports WSL unavailable -- worse than the bug being fixed. ENOENT stays non-retryable. It is answer-shaped for the reason it is meant to be -- wsl.exe is not on PATH -- and naming the directory is what removes the one cause that was not. Making it retryable would instead re-probe every non-WSL Windows machine on the short window, and would leave the false ENOENT in place for the other five sites, which have no cache to correct. Three of these are also on the `runWslProcess` W3 migration allowlist; this is the interim until they move, and matches what #17837 does inside the runner.
122 lines
4.1 KiB
TypeScript
122 lines
4.1 KiB
TypeScript
import { runProcess } from '../shared/child-process/run-process'
|
|
import { lstat, readFile } from 'node:fs/promises'
|
|
import { buildWslExecArgs, quotePosixShell } from '../shared/wsl-login-shell-command'
|
|
import { removeHostTree } from './host-tree-removal'
|
|
import { toLinuxPath } from './wsl'
|
|
import { resolveWslInteropSpawnCwd } from './wsl-interop-spawn-directory'
|
|
import type { ReadPath, StatPath } from './worktree-orphan-gitdir-proof'
|
|
|
|
export { toHostFilesystemPath, toHostRemovalPath } from './host-tree-removal'
|
|
|
|
export type LocalWorktreeFilesystemOptions = {
|
|
wslDistro?: string
|
|
}
|
|
|
|
type LocalWorktreePathAccess = {
|
|
statPath: StatPath
|
|
readPath: ReadPath
|
|
}
|
|
|
|
const WSL_FILE_OPERATION_TIMEOUT_MS = 30_000
|
|
/** The stat probe's explicit "missing path" branch. */
|
|
const WSL_MISSING_PATH_EXIT_CODE = 2
|
|
|
|
function shouldUseWslFilesystem(options: LocalWorktreeFilesystemOptions): boolean {
|
|
return process.platform === 'win32' && !!options.wslDistro?.trim()
|
|
}
|
|
|
|
/**
|
|
* Run a filesystem command inside the distro.
|
|
*
|
|
* Why no login shell: these are coreutils at standard paths plus shell builtins,
|
|
* and need nothing from the user's PATH. A login shell would only add its rc/motd
|
|
* output to the stdout these callers parse -- the banner problem -- so the fix is
|
|
* to not start one rather than to fence what it prints.
|
|
*/
|
|
async function runWslCommand(distro: string, command: string): Promise<string> {
|
|
const result = await runProcess({
|
|
program: 'wsl.exe',
|
|
args: buildWslExecArgs(distro, ['sh', '-c', command]),
|
|
// Why explicit (#16463): the guest path is inside `command`, so this only
|
|
// decides whether CreateProcessW succeeds -- and these calls run while a
|
|
// worktree is being removed, which is the cwd an inherited one would be.
|
|
cwd: resolveWslInteropSpawnCwd(),
|
|
timeoutMs: WSL_FILE_OPERATION_TIMEOUT_MS
|
|
})
|
|
if (result.timedOut) {
|
|
throw new Error(`WSL filesystem command timed out after ${WSL_FILE_OPERATION_TIMEOUT_MS}ms`)
|
|
}
|
|
if (result.code !== 0) {
|
|
throw Object.assign(new Error(result.stderr.trim() || `wsl.exe exited ${result.code}`), {
|
|
exitCode: result.code
|
|
})
|
|
}
|
|
return result.stdout
|
|
}
|
|
|
|
function isWslMissingPathError(error: unknown): boolean {
|
|
return (
|
|
typeof error === 'object' &&
|
|
error !== null &&
|
|
(error as { exitCode?: unknown }).exitCode === WSL_MISSING_PATH_EXIT_CODE
|
|
)
|
|
}
|
|
|
|
export function toLocalWorktreeRuntimePath(
|
|
targetPath: string,
|
|
options: LocalWorktreeFilesystemOptions = {}
|
|
): string {
|
|
return shouldUseWslFilesystem(options) ? toLinuxPath(targetPath) : targetPath
|
|
}
|
|
|
|
export function getLocalWorktreePathAccess(
|
|
options: LocalWorktreeFilesystemOptions = {}
|
|
): LocalWorktreePathAccess {
|
|
const distro = options.wslDistro?.trim()
|
|
if (!shouldUseWslFilesystem(options) || !distro) {
|
|
return {
|
|
statPath: lstat,
|
|
readPath: (path) => readFile(path, 'utf8')
|
|
}
|
|
}
|
|
|
|
return {
|
|
statPath: async (path) => {
|
|
const target = quotePosixShell(toLinuxPath(path))
|
|
const stdout = await runWslCommand(
|
|
distro,
|
|
[
|
|
`target=${target}`,
|
|
'if [ -L "$target" ]; then printf symlink; elif [ -f "$target" ]; then printf file; elif [ -d "$target" ]; then printf directory; else exit 2; fi'
|
|
].join('\n')
|
|
).catch((error) => {
|
|
if (isWslMissingPathError(error)) {
|
|
throw Object.assign(new Error(`missing ${path}`), { code: 'ENOENT' })
|
|
}
|
|
throw error
|
|
})
|
|
return { type: stdout.trim() }
|
|
},
|
|
readPath: async (path) => {
|
|
const target = quotePosixShell(toLinuxPath(path))
|
|
const stdout = await runWslCommand(distro, `cat -- ${target}`)
|
|
return stdout
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function removeLocalWorktreePath(
|
|
targetPath: string,
|
|
options: LocalWorktreeFilesystemOptions = {}
|
|
): Promise<void> {
|
|
const distro = options.wslDistro?.trim()
|
|
if (!shouldUseWslFilesystem(options) || !distro) {
|
|
await removeHostTree(targetPath)
|
|
return
|
|
}
|
|
|
|
// Why: WSL-owned worktree directories may be POSIX paths that Node on
|
|
// Windows cannot delete safely. Run the deletion inside the selected distro.
|
|
await runWslCommand(distro, `rm -rf -- ${quotePosixShell(toLinuxPath(targetPath))}`)
|
|
}
|