mirror of
https://github.com/stablyai/orca.git
synced 2026-10-17 08:03:14 +00:00
fix(daemon): bound legacy probes and keep host under lint limit
This commit is contained in:
@@ -845,6 +845,53 @@ describe('daemon-init: runRestartDaemon (7-step sequence)', () => {
|
||||
expect(killStaleDaemonMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('bounds an unresponsive legacy session query and fail-safe adopts the daemon', async () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const mod = await importFresh()
|
||||
probeSocketExistsMock.mockImplementation(
|
||||
(p?: string) => p?.endsWith('daemon-v9.sock') ?? false
|
||||
)
|
||||
netConnectMock.mockImplementation(() => {
|
||||
const handlers: Record<string, (() => void)[]> = { connect: [], error: [] }
|
||||
return {
|
||||
on(event: string, cb: () => void) {
|
||||
handlers[event]?.push(cb)
|
||||
if (event === 'connect') {
|
||||
queueMicrotask(() => cb())
|
||||
}
|
||||
return this
|
||||
},
|
||||
removeListener(event: string, cb: () => void) {
|
||||
handlers[event] = handlers[event]?.filter((handler) => handler !== cb) ?? []
|
||||
return this
|
||||
},
|
||||
destroy() {}
|
||||
}
|
||||
})
|
||||
const disconnectMock = vi.fn()
|
||||
daemonClientMock.mockImplementationOnce(function MockHungLegacyDaemonClient() {
|
||||
return {
|
||||
ensureConnected: vi.fn(async () => {}),
|
||||
request: vi.fn(() => new Promise<never>(() => {})),
|
||||
disconnect: disconnectMock
|
||||
}
|
||||
})
|
||||
|
||||
const initPromise = mod.initDaemonPtyProvider()
|
||||
await vi.advanceTimersByTimeAsync(1000)
|
||||
await initPromise
|
||||
|
||||
const { DaemonPtyRouter } = await import('./daemon-pty-router')
|
||||
expect(mod.getDaemonProvider()).toBeInstanceOf(DaemonPtyRouter)
|
||||
expect(adapterInstances.some((instance) => instance.protocolVersion === 9)).toBe(true)
|
||||
expect(disconnectMock).toHaveBeenCalledOnce()
|
||||
expect(killStaleDaemonMock).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('restart path with no legacy adapters yields a bare DaemonPtyAdapter (not wrapped in a router)', async () => {
|
||||
const mod = await importFresh()
|
||||
await mod.initDaemonPtyProvider()
|
||||
|
||||
@@ -74,6 +74,7 @@ let adapter: DaemonProvider | null = null
|
||||
// the second entry would read the already-disposed current adapter and
|
||||
// race cleanupDaemonForProtocol against a half-spawned replacement.
|
||||
let restartInFlight: Promise<RestartDaemonResult> | null = null
|
||||
const LEGACY_SESSION_QUERY_TIMEOUT_MS = 1000
|
||||
|
||||
function getRuntimeDir(): string {
|
||||
const dir = join(app.getPath('userData'), 'daemon')
|
||||
@@ -160,13 +161,31 @@ async function getAliveDaemonSessionCount(
|
||||
protocolVersion = PROTOCOL_VERSION
|
||||
): Promise<number | null> {
|
||||
const client = new DaemonClient({ socketPath, tokenPath, protocolVersion })
|
||||
let timeout: ReturnType<typeof setTimeout> | null = null
|
||||
try {
|
||||
await client.ensureConnected()
|
||||
const result = await client.request<ListSessionsResult>('listSessions', undefined)
|
||||
// Why: legacy versions are checked serially during startup. A daemon that
|
||||
// accepts sockets but stops answering RPCs must fail-safe to adoption
|
||||
// promptly instead of multiplying the client's 30s timeout by versions.
|
||||
const result = await Promise.race([
|
||||
(async () => {
|
||||
await client.ensureConnected()
|
||||
return client.request<ListSessionsResult>('listSessions', undefined)
|
||||
})(),
|
||||
new Promise<null>((resolve) => {
|
||||
timeout = setTimeout(() => resolve(null), LEGACY_SESSION_QUERY_TIMEOUT_MS)
|
||||
timeout.unref?.()
|
||||
})
|
||||
])
|
||||
if (!result) {
|
||||
return null
|
||||
}
|
||||
return result.sessions.filter((session) => session.isAlive).length
|
||||
} catch {
|
||||
return null
|
||||
} finally {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
client.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { StartupCommandDelivery } from '../../shared/codex-startup-delivery'
|
||||
import type { SubprocessHandle } from './session'
|
||||
import type { ShellReadyState, TakePendingOutputResult, TerminalSnapshot } from './types'
|
||||
|
||||
export type CreateOrAttachOptions = {
|
||||
sessionId: string
|
||||
cols: number
|
||||
rows: number
|
||||
cwd?: string
|
||||
env?: Record<string, string>
|
||||
envToDelete?: string[]
|
||||
command?: string
|
||||
startupCommandDelivery?: StartupCommandDelivery
|
||||
/** Explicit shell the renderer asked for (e.g. 'wsl.exe' for "New WSL
|
||||
* terminal" from the "+" menu). Forwarded to the subprocess spawner so the
|
||||
* daemon path honors per-tab shell selection the same way LocalPtyProvider
|
||||
* does. */
|
||||
shellOverride?: string
|
||||
terminalWindowsWslDistro?: string | null
|
||||
terminalWindowsPowerShellImplementation?: 'auto' | 'powershell.exe' | 'pwsh.exe'
|
||||
shellReadySupported?: boolean
|
||||
shellReadyTimeoutMs?: number
|
||||
historySeed?: string
|
||||
streamClient: { onData: (data: string) => void; onExit: (code: number) => void }
|
||||
}
|
||||
|
||||
export type CreateOrAttachResult = {
|
||||
isNew: boolean
|
||||
snapshot: TerminalSnapshot | null
|
||||
pid: number | null
|
||||
shellState: ShellReadyState
|
||||
historySeeded?: boolean
|
||||
attachToken: symbol
|
||||
}
|
||||
|
||||
export type TerminalHostOptions = {
|
||||
spawnSubprocess: (opts: {
|
||||
sessionId: string
|
||||
cols: number
|
||||
rows: number
|
||||
cwd?: string
|
||||
env?: Record<string, string>
|
||||
envToDelete?: string[]
|
||||
command?: string
|
||||
startupCommandDelivery?: StartupCommandDelivery
|
||||
shellOverride?: string
|
||||
terminalWindowsWslDistro?: string | null
|
||||
terminalWindowsPowerShellImplementation?: 'auto' | 'powershell.exe' | 'pwsh.exe'
|
||||
}) => SubprocessHandle
|
||||
// Why: on graceful shutdown, the host writes final checkpoints for all live
|
||||
// sessions before killing them. This bypasses the RPC round-trip — the daemon
|
||||
// writes checkpoints in-process, guaranteeing completion before teardown.
|
||||
onFinalCheckpoint?: (
|
||||
sessionId: string,
|
||||
snapshot: TerminalSnapshot,
|
||||
records: TakePendingOutputResult['records']
|
||||
) => void
|
||||
// Why: production keeps a large cap, but tests need a small deterministic cap
|
||||
// without spawning thousands of full terminal sessions.
|
||||
maxTombstones?: number
|
||||
// Why: lets the daemon server re-evaluate idle-exit eligibility on every
|
||||
// session-map transition instead of polling the host.
|
||||
onSessionCountChange?: () => void
|
||||
}
|
||||
@@ -1,79 +1,17 @@
|
||||
import { Session, type SubprocessHandle } from './session'
|
||||
import { Session } from './session'
|
||||
import { normalizePtySize } from './daemon-pty-size'
|
||||
import { resolveProcessCwd } from '../providers/process-cwd'
|
||||
import type { StartupCommandDelivery } from '../../shared/codex-startup-delivery'
|
||||
import { buildStartupCommandSubmission } from '../../shared/startup-command-submission'
|
||||
import type {
|
||||
SessionInfo,
|
||||
TakePendingOutputResult,
|
||||
TerminalSnapshot,
|
||||
ShellReadyState
|
||||
} from './types'
|
||||
import type { SessionInfo, TakePendingOutputResult, TerminalSnapshot } from './types'
|
||||
import { SessionNotFoundError } from './types'
|
||||
import type {
|
||||
CreateOrAttachOptions,
|
||||
CreateOrAttachResult,
|
||||
TerminalHostOptions
|
||||
} from './terminal-host-contract'
|
||||
|
||||
const DEFAULT_MAX_TOMBSTONES = 1000
|
||||
|
||||
export type CreateOrAttachOptions = {
|
||||
sessionId: string
|
||||
cols: number
|
||||
rows: number
|
||||
cwd?: string
|
||||
env?: Record<string, string>
|
||||
envToDelete?: string[]
|
||||
command?: string
|
||||
startupCommandDelivery?: StartupCommandDelivery
|
||||
/** Explicit shell the renderer asked for (e.g. 'wsl.exe' for "New WSL
|
||||
* terminal" from the "+" menu). Forwarded to the subprocess spawner so the
|
||||
* daemon path honors per-tab shell selection the same way LocalPtyProvider
|
||||
* does. */
|
||||
shellOverride?: string
|
||||
terminalWindowsWslDistro?: string | null
|
||||
terminalWindowsPowerShellImplementation?: 'auto' | 'powershell.exe' | 'pwsh.exe'
|
||||
shellReadySupported?: boolean
|
||||
shellReadyTimeoutMs?: number
|
||||
historySeed?: string
|
||||
streamClient: { onData: (data: string) => void; onExit: (code: number) => void }
|
||||
}
|
||||
|
||||
export type CreateOrAttachResult = {
|
||||
isNew: boolean
|
||||
snapshot: TerminalSnapshot | null
|
||||
pid: number | null
|
||||
shellState: ShellReadyState
|
||||
historySeeded?: boolean
|
||||
attachToken: symbol
|
||||
}
|
||||
|
||||
export type TerminalHostOptions = {
|
||||
spawnSubprocess: (opts: {
|
||||
sessionId: string
|
||||
cols: number
|
||||
rows: number
|
||||
cwd?: string
|
||||
env?: Record<string, string>
|
||||
envToDelete?: string[]
|
||||
command?: string
|
||||
startupCommandDelivery?: StartupCommandDelivery
|
||||
shellOverride?: string
|
||||
terminalWindowsWslDistro?: string | null
|
||||
terminalWindowsPowerShellImplementation?: 'auto' | 'powershell.exe' | 'pwsh.exe'
|
||||
}) => SubprocessHandle
|
||||
// Why: on graceful shutdown, the host writes final checkpoints for all live
|
||||
// sessions before killing them. This bypasses the RPC round-trip — the daemon
|
||||
// writes checkpoints in-process, guaranteeing completion before teardown.
|
||||
onFinalCheckpoint?: (
|
||||
sessionId: string,
|
||||
snapshot: TerminalSnapshot,
|
||||
records: TakePendingOutputResult['records']
|
||||
) => void
|
||||
// Why: production keeps a large cap, but tests need a small deterministic cap
|
||||
// without spawning thousands of full terminal sessions.
|
||||
maxTombstones?: number
|
||||
// Why: lets the daemon server re-evaluate idle-exit eligibility on every
|
||||
// session-map transition instead of polling the host.
|
||||
onSessionCountChange?: () => void
|
||||
}
|
||||
|
||||
export class TerminalHost {
|
||||
private sessions = new Map<string, Session>()
|
||||
private killedTombstones = new Map<string, number>()
|
||||
|
||||
Reference in New Issue
Block a user