mirror of
https://github.com/stablyai/orca.git
synced 2026-09-23 16:02:24 +00:00
* 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
91 lines
2.6 KiB
TypeScript
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)
|
|
})
|