mirror of
https://github.com/stablyai/orca.git
synced 2026-10-25 16:03:53 +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
108 lines
3.0 KiB
TypeScript
108 lines
3.0 KiB
TypeScript
import type { Socket } from 'node:net'
|
|
import { encodeNdjson } from './ndjson'
|
|
import { DaemonProtocolError } from './types'
|
|
import type { DaemonPendingRequests } from './daemon-client-pending-requests'
|
|
|
|
type DaemonRpcRequestOptions = {
|
|
socket: Socket
|
|
pendingRequests: DaemonPendingRequests
|
|
id: string
|
|
type: string
|
|
payload: unknown
|
|
timeoutMs: number
|
|
signal?: AbortSignal
|
|
onCreateCancellationFailure: () => void
|
|
settleCreateCancellation: (sessionId: string, requestId: string) => Promise<{ canceled: boolean }>
|
|
}
|
|
|
|
export function requestDaemonRpc<T>(opts: DaemonRpcRequestOptions): Promise<T> {
|
|
const { payload, type } = opts
|
|
const createSessionId =
|
|
type === 'createOrAttach' && payload !== null && typeof payload === 'object'
|
|
? Reflect.get(payload, 'sessionId')
|
|
: null
|
|
const requestPayload =
|
|
type === 'createOrAttach' && payload !== null && typeof payload === 'object'
|
|
? { ...payload, cancelAfterMs: Math.max(1, opts.timeoutMs - 100) }
|
|
: payload
|
|
const encoded = encodeNdjson({
|
|
id: opts.id,
|
|
type,
|
|
...(requestPayload !== undefined ? { payload: requestPayload } : {})
|
|
})
|
|
|
|
return new Promise<T>((resolve, reject) => {
|
|
let sent = false
|
|
let cancellationStarted = false
|
|
let settled = false
|
|
const removeAbortListener = (): void => opts.signal?.removeEventListener('abort', onAbort)
|
|
const rejectAndDrop = (error: Error): void => {
|
|
if (settled) {
|
|
return
|
|
}
|
|
settled = true
|
|
opts.pendingRequests.drop(opts.id)
|
|
removeAbortListener()
|
|
clearTimeout(timer)
|
|
reject(error)
|
|
}
|
|
const cancelCreate = (error: Error): void => {
|
|
if (cancellationStarted) {
|
|
return
|
|
}
|
|
cancellationStarted = true
|
|
clearTimeout(timer)
|
|
if (!sent || typeof createSessionId !== 'string') {
|
|
rejectAndDrop(error)
|
|
return
|
|
}
|
|
void opts
|
|
.settleCreateCancellation(createSessionId, opts.id)
|
|
.then((result) => {
|
|
if (result.canceled) {
|
|
rejectAndDrop(error)
|
|
}
|
|
})
|
|
.catch(() => {
|
|
if (!settled) {
|
|
opts.onCreateCancellationFailure()
|
|
}
|
|
})
|
|
}
|
|
const timer = setTimeout(() => {
|
|
const error = new DaemonProtocolError(`Request ${type} timed out after ${opts.timeoutMs}ms`)
|
|
if (typeof createSessionId === 'string') {
|
|
cancelCreate(error)
|
|
} else {
|
|
rejectAndDrop(error)
|
|
}
|
|
}, opts.timeoutMs)
|
|
const onAbort = (): void => {
|
|
removeAbortListener()
|
|
cancelCreate(new Error('client_disconnected'))
|
|
}
|
|
|
|
opts.pendingRequests.add(opts.id, {
|
|
resolve: (value) => {
|
|
settled = true
|
|
removeAbortListener()
|
|
resolve(value as T)
|
|
},
|
|
reject: (error) => {
|
|
settled = true
|
|
removeAbortListener()
|
|
reject(error)
|
|
},
|
|
timer
|
|
})
|
|
|
|
opts.signal?.addEventListener('abort', onAbort, { once: true })
|
|
if (opts.signal?.aborted) {
|
|
onAbort()
|
|
return
|
|
}
|
|
sent = true
|
|
opts.socket.write(encoded)
|
|
})
|
|
}
|