Files
orca/src/main/git/command-runner/wsl-host-failure.ts
T
Neil d3501f7ad6 fix(worktrees): stop a failed worktree scan from being recorded as an authoritative empty listing (#18456)
* fix(worktrees): stop a failed worktree scan from pruning as an authoritative empty listing

A `git worktree list` that could not run at all — a WSL distro that stopped
resolving, a hung mount, a git binary that errored — was softened to `[]` by the
lenient listing path, so the detected scan published it as
`authoritative: true, worktrees: []`. That disables the #1158 retention guard,
drops every persisted tab for the repo, and the pruned session is written back to
disk on the next launch. The loss is permanent, not a transient glitch.

Route the detected scan through a strict listing that still reports the two
genuinely empty states (repo path gone, not a Git repo) as `[]`. Everything else
rejects, so the existing catch answers `authoritative: false` and the destructive
halves (`rememberLocalWorktreeRoots`, `pruneLineageForMissingRepoWorktrees`)
never see a failed scan.

Also make the failure readable: wsl.exe reports its own launch failures as exit
0xFFFFFFFF with an EMPTY stderr and the `Wsl/Service/WSL_E_*` line on stdout as
UTF-16LE, which is why the field bundle carried a git error with no text. Set
WSL_UTF8 for WSL-routed git (matching the wsl runner, #9010) and attach that
stdout diagnostic to the error so `git.exec` spans name the cause.

* fix(worktrees): surface a failed worktree scan's cause on the repo header with a retry

A failed scan now travels with its reason (optional unavailableReason on
DetectedWorktreeListResult), the repo header shows it with click-to-retry,
and the WSL deleted-guest-directory shape measured on a real Windows host is
pinned as retained-not-pruned.
2026-09-04 15:23:09 -07:00

56 lines
2.2 KiB
TypeScript

import type { ResolvedCommand } from './wsl-command-resolution'
/** wsl.exe's own launch-failure exit, distinct from any status the guest process can return. */
export const WSL_HOST_FAILURE_EXIT_CODE = 0xffffffff
function outputText(value: unknown): string {
if (typeof value === 'string') {
return value
}
return Buffer.isBuffer(value) ? value.toString('utf8') : ''
}
/**
* The message wsl.exe prints when it — not the guest — failed: a distro that was renamed or
* removed, or a VM that would not start.
*
* Why this needs decoding at all: wsl.exe exits 0xFFFFFFFF, leaves stderr EMPTY, and writes
* `Error code: Wsl/Service/WSL_E_*` to stdout, so every caller that reports stderr reports nothing.
* NULs are stripped because a wsl.exe that ignores WSL_UTF8 writes that line as UTF-16LE (#9010).
*/
export function readWslHostFailureDiagnostic(
error: unknown,
command: ResolvedCommand
): string | null {
if (!command.wsl || !error || typeof error !== 'object') {
return null
}
const { code, status, stdout, stderr } = error as {
code?: unknown
status?: unknown
stdout?: unknown
stderr?: unknown
}
const exitCode = typeof code === 'number' ? code : typeof status === 'number' ? status : null
// A guest failure always explains itself on stderr; an empty one plus this exit is the host.
if (exitCode !== WSL_HOST_FAILURE_EXIT_CODE || outputText(stderr).trim().length > 0) {
return null
}
const diagnostic = outputText(stdout).replaceAll('\u0000', '').trim()
return diagnostic.length > 0 ? diagnostic : 'wsl.exe reported no diagnostic.'
}
/**
* Move a wsl.exe host failure into the error's message, which is what `git.exec` spans record ahead
* of the stack. Left untouched when the failure came from git itself.
*/
export function annotateWslHostFailure(error: unknown, command: ResolvedCommand): unknown {
const diagnostic = readWslHostFailureDiagnostic(error, command)
if (diagnostic === null || !(error instanceof Error) || !command.wsl) {
return error
}
const distro = command.wsl.distro
error.message = `wsl.exe host failure (distro "${distro}"): ${diagnostic}\n${error.message}`
return Object.assign(error, { wslHostFailure: true, wslDistro: distro })
}