mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 10:08:13 +00:00
* fix(windows): stop recursive deletes from retry-storming when a file is held open Node's async recursive rm retries every nested entry separately and orphans sibling walkers when it rejects, so deleting a workspace holding a live browser profile multiplied filesystem work per directory level until the main process hung. Delete Windows host trees with a walker that retries whole passes from one shared budget and leaves nothing running on failure. Also bound and log detached worktree removal, and fail a file transaction whose proper-lockfile lock is compromised instead of throwing from its timer. * fix(windows): retry a locked delete as whole passes instead of a custom walker Replace the hand-rolled tree walker with Node's own recursive rm and no per-entry retries. Async rm gives each nested entry its own retry counter and never stops siblings, which is what multiplied the work; with retries off each pass is one bounded walk, and a transient Windows lock repeats the whole pass from the existing 250/500/1000/2000 ms budget, checking the job's signal between passes. A failed delete now names the file another program holds open instead of Node's EBUSY/EPERM text. Drop the 30-minute background deadline. The lost-lock error no longer embeds the library's message and says the write may or may not have been saved. * test: cover bounded deletion failure and recovery contracts * test: prune unchanged cleanup coverage * fix(worktrees): give truthful failed-delete cleanup advice * test: document fake-clock retry adapter --------- Co-authored-by: Orca Worker <orca-worker@localhost> Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
91 lines
3.6 KiB
TypeScript
91 lines
3.6 KiB
TypeScript
/** Translate Windows access only for the resource owner’s WSL host, using its registered distro. Pure desktop-side path work; SSH/paired/local hosts have no translation. */
|
||
import { parseRoutableExecutionHostId } from './execution-host'
|
||
import { parseWslUncPath, toWindowsWslUncPath } from './wsl-paths'
|
||
|
||
export type WindowsPathAccess = {
|
||
/** The distro as registered, which is the casing every emitted path uses. */
|
||
readonly wslDistro: string
|
||
/** The Windows spelling of an absolute Linux path on the host, or null when it has none. */
|
||
toWindows(linuxPath: string): string | null
|
||
/** The host's Linux path for a `\\wsl.localhost` / `\\wsl$` path into its distro, else null. */
|
||
fromWindows(windowsPath: string): string | null
|
||
}
|
||
|
||
export type WindowsPathAccessContext = {
|
||
platform: NodeJS.Platform | null
|
||
/** The distro behind a WSL host's runtime environment; null for every other environment. */
|
||
wslDistroForEnvironment: (environmentId: string) => string | null | undefined
|
||
}
|
||
|
||
/** Use resource ownership, never focused-host state, for Windows path guards. */
|
||
export function windowsPathAccess(
|
||
ownerHostId: string | null | undefined,
|
||
context: WindowsPathAccessContext
|
||
): WindowsPathAccess | null {
|
||
if (context.platform !== 'win32') {
|
||
return null
|
||
}
|
||
const host = parseRoutableExecutionHostId(ownerHostId)
|
||
if (host?.kind !== 'runtime') {
|
||
return null
|
||
}
|
||
const distro = context.wslDistroForEnvironment(host.environmentId)?.trim()
|
||
return distro ? wslDistroWindowsPathAccess(distro) : null
|
||
}
|
||
|
||
export function wslDistroWindowsPathAccess(distro: string): WindowsPathAccess {
|
||
return {
|
||
wslDistro: distro,
|
||
toWindows: (linuxPath) => {
|
||
const normalized = normalizeLinuxPath(linuxPath)
|
||
return normalized === null ? null : toWindowsWslUncPath(normalized, distro)
|
||
},
|
||
fromWindows: (windowsPath) => {
|
||
const parsed = parseWslUncPath(stripWin32LongPathPrefix(windowsPath.trim()))
|
||
// Why case-insensitive: Windows folds the distro in UNC paths (`\\wsl.localhost\ubuntu`).
|
||
if (!parsed || !sameWslDistroName(parsed.distro, distro)) {
|
||
return null
|
||
}
|
||
return normalizeLinuxPath(parsed.linuxPath)
|
||
}
|
||
}
|
||
}
|
||
|
||
export function sameWslDistroName(left: string, right: string): boolean {
|
||
return left.trim().toLowerCase() === right.trim().toLowerCase()
|
||
}
|
||
|
||
export type WindowsPathRejection =
|
||
| { kind: 'windows-drive' }
|
||
| { kind: 'other-distro'; distro: string }
|
||
| { kind: 'not-wsl' }
|
||
|
||
/** Why `fromWindows` refused a path, for copy that tells the user what to pick instead. */
|
||
export function classifyRejectedWindowsPath(windowsPath: string): WindowsPathRejection {
|
||
const trimmed = stripWin32LongPathPrefix(windowsPath.trim())
|
||
if (/^[A-Za-z]:([\\/]|$)/.test(trimmed)) {
|
||
return { kind: 'windows-drive' }
|
||
}
|
||
const parsed = parseWslUncPath(trimmed)
|
||
return parsed ? { kind: 'other-distro', distro: parsed.distro } : { kind: 'not-wsl' }
|
||
}
|
||
|
||
export function stripWin32LongPathPrefix(path: string): string {
|
||
if (/^\\\\\?\\UNC\\/i.test(path)) {
|
||
return `\\\\${path.slice('\\\\?\\UNC\\'.length)}`
|
||
}
|
||
return /^\\\\\?\\[A-Za-z]:/.test(path) ? path.slice('\\\\?\\'.length) : path
|
||
}
|
||
|
||
/** Absolute, no `..`/backslash/NUL (Windows would read those differently); trailing slash dropped. */
|
||
function normalizeLinuxPath(linuxPath: string): string | null {
|
||
if (!linuxPath.startsWith('/') || linuxPath.startsWith('//') || /[\\\0]/.test(linuxPath)) {
|
||
return null
|
||
}
|
||
const segments = linuxPath.split('/').filter((segment) => segment !== '' && segment !== '.')
|
||
if (segments.includes('..')) {
|
||
return null
|
||
}
|
||
return `/${segments.join('/')}`
|
||
}
|