Files
orca/tests/e2e/fixtures/daemon-generation-entry.ts
T
Neil fd1dba9db9 fix(daemon): validate spawn cwd asynchronously so one dead share cannot freeze every terminal (#14848)
* fix(daemon): validate spawn cwd asynchronously so one dead share cannot freeze every terminal

createOrAttach validated the working directory synchronously on the daemon's
only thread. Measured on Windows 11 + Ubuntu-24.04:

  existsSync on an unreachable UNC share   21,022 ms
  wsl.exe probe, cold distro                1,266 ms
  wsl.exe probe, warm distro                   59 ms
  existsSync/statSync on healthy \\wsl.localhost  4 ms / 1 ms

A single unreachable share therefore blocks the whole RPC loop past the
client's 30s request ceiling, so every other terminal stalls behind it and
reports `DaemonProtocolError: Request createOrAttach timed out after 30000ms`.
The main process already validates asynchronously and passes prevalidatedCwd
(ipc/pty.ts); the daemon never got the same treatment.

Add validateWorkingDirectoryAsync (one stat, not exists-then-stat, so an
unreachable share is not paid for twice) and await it from the daemon spawn
preflights. spawnSubprocess now returns SubprocessHandle | Promise<...>, which
existing sync stubs still satisfy.

Deliberately not bounding the stat with a timeout: the 30s ceiling comes from
blocking the shared loop, not from the duration. A timeout cannot tell "slow
share" from "gone share", so it would fail spawns that succeed today at 3-8s
on a cold VPN mount, and trade an accurate "working directory does not exist"
for a guess.

The new await opened a race: it sits between the "already exists?" check and
the sessions.set that publishes the session, so two concurrent creates for one
session id both spawned. Gate creation per session id; distinct ids still spawn
in parallel.

STA-4470

* fix(daemon): fence async spawn lifecycle
2026-08-16 01:16:11 -07:00

91 lines
2.6 KiB
TypeScript

import process from 'node:process'
import { startDaemon, type DaemonHandle } from '../../../src/main/daemon/daemon-main'
import { createPtySubprocess } from '../../../src/main/daemon/pty-subprocess'
import { createDaemonFileLog } from '../../../src/main/daemon/daemon-file-log'
type FixtureArgs = {
protocolVersion: number
socketPath: string
tokenPath: string
logPath: string
refuseDispose: boolean
}
function parseFixtureArgs(argv: string[]): FixtureArgs {
const values = new Map<string, string>()
for (let index = 0; index < argv.length; index += 2) {
const key = argv[index]
const value = argv[index + 1]
if (!key || !value) {
throw new Error('Daemon generation fixture arguments must be key/value pairs')
}
values.set(key, value)
}
const protocolVersion = Number(values.get('--protocol'))
const socketPath = values.get('--socket')
const tokenPath = values.get('--token')
const logPath = values.get('--log')
const refuseDispose = values.get('--refuse-dispose') === 'true'
if (
!Number.isInteger(protocolVersion) ||
protocolVersion < 1 ||
!socketPath ||
!tokenPath ||
!logPath
) {
throw new Error(
'Usage: daemon-generation-entry --protocol <n> --socket <path> --token <path> --log <path>'
)
}
return { protocolVersion, socketPath, tokenPath, logPath, refuseDispose }
}
async function main(): Promise<void> {
const { protocolVersion, socketPath, tokenPath, logPath, refuseDispose } = parseFixtureArgs(
process.argv.slice(2)
)
let daemon: DaemonHandle | null = await startDaemon({
protocolVersion,
socketPath,
tokenPath,
log: createDaemonFileLog(logPath),
spawnSubprocess: async (options) => {
const subprocess = await createPtySubprocess(options)
if (refuseDispose) {
// Why: models an access-denied/unreapable Windows PTY while keeping the
// real child and ConPTY handle inside this disposable fixture tree.
subprocess.kill = () => {}
subprocess.forceKill = () => {}
}
return subprocess
}
})
let shuttingDown = false
const shutdown = async (): Promise<void> => {
if (shuttingDown) {
return
}
shuttingDown = true
try {
await daemon?.shutdown()
daemon = null
} finally {
process.exit(0)
}
}
process.on('SIGTERM', () => void shutdown())
process.on('SIGINT', () => void shutdown())
process.send?.({
type: 'ready',
protocolVersion,
startedAtMs: Date.now() - process.uptime() * 1000
})
}
void main().catch((error) => {
console.error(error)
process.exit(1)
})