mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
Recover from daemon hello timeout (#1179)
This commit is contained in:
@@ -51,6 +51,7 @@ describe('DaemonClient', () => {
|
||||
onControlMessage?: (msg: unknown) => string | null
|
||||
onStreamHello?: (msg: HelloMessage) => void
|
||||
rejectVersion?: boolean
|
||||
ignoreHello?: boolean
|
||||
}): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
server = createServer((socket) => {
|
||||
@@ -69,6 +70,9 @@ describe('DaemonClient', () => {
|
||||
|
||||
if (msg.type === 'hello') {
|
||||
const hello = msg as HelloMessage
|
||||
if (opts?.ignoreHello) {
|
||||
return
|
||||
}
|
||||
if (opts?.rejectVersion) {
|
||||
socket.write(encodeNdjson({ type: 'hello', ok: false, error: 'Version mismatch' }))
|
||||
return
|
||||
@@ -112,6 +116,13 @@ describe('DaemonClient', () => {
|
||||
client = new DaemonClient({ socketPath, tokenPath })
|
||||
await expect(client.ensureConnected()).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('rejects when the daemon accepts a socket but never answers hello', async () => {
|
||||
await startMockDaemon({ ignoreHello: true })
|
||||
|
||||
client = new DaemonClient({ socketPath, tokenPath, handshakeTimeoutMs: 10 })
|
||||
await expect(client.ensureConnected()).rejects.toThrow('Hello timed out')
|
||||
})
|
||||
})
|
||||
|
||||
describe('RPC', () => {
|
||||
|
||||
@@ -7,12 +7,14 @@ import type { HelloMessage, HelloResponse, RpcResponse, DaemonEvent } from './ty
|
||||
import { addNodePtyRecoveryHint } from './node-pty-error-hints'
|
||||
|
||||
const CONNECT_TIMEOUT_MS = 5000
|
||||
const HELLO_TIMEOUT_MS = 5000
|
||||
const REQUEST_TIMEOUT_MS = 30000
|
||||
|
||||
export type DaemonClientOptions = {
|
||||
socketPath: string
|
||||
tokenPath: string
|
||||
protocolVersion?: number
|
||||
handshakeTimeoutMs?: number
|
||||
}
|
||||
|
||||
type PendingRequest = {
|
||||
@@ -25,6 +27,7 @@ export class DaemonClient {
|
||||
private socketPath: string
|
||||
private tokenPath: string
|
||||
private protocolVersion: number
|
||||
private handshakeTimeoutMs: number
|
||||
private clientId = randomUUID()
|
||||
|
||||
private controlSocket: Socket | null = null
|
||||
@@ -50,6 +53,7 @@ export class DaemonClient {
|
||||
this.socketPath = opts.socketPath
|
||||
this.tokenPath = opts.tokenPath
|
||||
this.protocolVersion = opts.protocolVersion ?? PROTOCOL_VERSION
|
||||
this.handshakeTimeoutMs = opts.handshakeTimeoutMs ?? HELLO_TIMEOUT_MS
|
||||
}
|
||||
|
||||
isConnected(): boolean {
|
||||
@@ -215,6 +219,7 @@ export class DaemonClient {
|
||||
}
|
||||
|
||||
socket.removeListener('data', onData)
|
||||
clearTimeout(timer)
|
||||
const line = buffer.slice(0, newlineIdx)
|
||||
try {
|
||||
const response = JSON.parse(line) as HelloResponse
|
||||
@@ -230,6 +235,13 @@ export class DaemonClient {
|
||||
}
|
||||
}
|
||||
|
||||
// Why: a wedged daemon can accept the socket but never answer hello,
|
||||
// leaving terminal spawn permanently pending with a blank renderer pane.
|
||||
const timer = setTimeout(() => {
|
||||
socket.removeListener('data', onData)
|
||||
reject(new DaemonProtocolError('Hello timed out'))
|
||||
}, this.handshakeTimeoutMs)
|
||||
|
||||
socket.on('data', onData)
|
||||
socket.write(encodeNdjson(hello))
|
||||
})
|
||||
|
||||
@@ -660,8 +660,9 @@ export class DaemonPtyAdapter implements IPtyProvider {
|
||||
// unreachable (daemon died). Checking syscall avoids false positives from
|
||||
// token-file ENOENT (readFileSync), which has no syscall or syscall='open'.
|
||||
// "Connection lost" / "Not connected" mean the daemon died while we had an
|
||||
// active or stale connection. All indicate the daemon is gone and a respawn
|
||||
// should be attempted.
|
||||
// active or stale connection. "Hello timed out" means the daemon accepted
|
||||
// the socket but did not complete the protocol handshake, which is also a
|
||||
// wedged-daemon state. All indicate a respawn should be attempted.
|
||||
function isDaemonGoneError(err: unknown): boolean {
|
||||
if (!(err instanceof Error)) {
|
||||
return false
|
||||
@@ -671,5 +672,5 @@ function isDaemonGoneError(err: unknown): boolean {
|
||||
return true
|
||||
}
|
||||
const msg = err.message
|
||||
return msg === 'Connection lost' || msg === 'Not connected'
|
||||
return msg === 'Connection lost' || msg === 'Not connected' || msg === 'Hello timed out'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user