Skill update round trip / roundtrip (false, macos-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, macos-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, symlink, latest) (push) Canceled after 0s
Skill update round trip / roundtrip (false, windows-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, windows-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, macos-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, macos-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, ubuntu-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, ubuntu-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, windows-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, windows-latest, symlink, 1.5.17) (push) Canceled after 0s
287 lines
10 KiB
TypeScript
287 lines
10 KiB
TypeScript
import { performance } from 'node:perf_hooks'
|
|
import { readCurrentProcessMacSystemResolverHealth } from '../network/macos-system-resolver-health'
|
|
import type { ConnectedDaemonClient, DaemonClientConnections } from './daemon-client-connections'
|
|
import type { DaemonFileLog } from './daemon-file-log'
|
|
import type { DaemonPtySpawnPreparations } from './daemon-pty-spawn-preparations'
|
|
import type { DaemonServerLifecycle } from './daemon-server-lifecycle'
|
|
import type { DaemonSessionAttachments } from './daemon-session-attachments'
|
|
import type { DaemonSessionBackgroundRouting } from './daemon-session-background-routing'
|
|
import { recordDaemonStreamBacklogEvent } from './daemon-stream-backlog-probe'
|
|
import type { DaemonStreamDataBatcher } from './daemon-stream-data-batcher'
|
|
import type { DaemonTerminalAdmission } from './daemon-terminal-admission'
|
|
import type { TerminalHistorySeedTransferRegistry } from './terminal-history-seed-transfer-registry'
|
|
import type { TerminalHost } from './terminal-host'
|
|
import { SessionNotFoundError, type DaemonRequest } from './types'
|
|
|
|
type DaemonRequestRouterOptions = {
|
|
host: TerminalHost
|
|
connections: DaemonClientConnections
|
|
lifecycle: DaemonServerLifecycle
|
|
admission: DaemonTerminalAdmission
|
|
preparations: DaemonPtySpawnPreparations
|
|
attachments: DaemonSessionAttachments
|
|
historySeedTransfers: TerminalHistorySeedTransferRegistry
|
|
sessionBackgroundRouting: DaemonSessionBackgroundRouting
|
|
streamDataBatcher: DaemonStreamDataBatcher
|
|
ptySpawnHealthCheck: () => Promise<void>
|
|
log: DaemonFileLog
|
|
}
|
|
|
|
export class DaemonRequestRouter {
|
|
constructor(private readonly options: DaemonRequestRouterOptions) {}
|
|
|
|
async route(clientId: string, request: DaemonRequest): Promise<unknown> {
|
|
const client = this.options.connections.get(clientId)
|
|
switch (request.type) {
|
|
case 'startHistorySeedTransfer': {
|
|
if (!client?.authenticatedPairEstablished || client.streamSocket === null) {
|
|
throw new Error('Daemon client connection is incomplete; reconnect')
|
|
}
|
|
return {
|
|
transferId: this.options.historySeedTransfers.start(clientId, request.payload)
|
|
}
|
|
}
|
|
case 'appendHistorySeedTransfer':
|
|
this.options.historySeedTransfers.append(
|
|
clientId,
|
|
request.payload.transferId,
|
|
request.payload.index,
|
|
request.payload.data
|
|
)
|
|
return {}
|
|
case 'finishHistorySeedTransfer':
|
|
this.options.historySeedTransfers.finish(clientId, request.payload.transferId)
|
|
return {}
|
|
case 'abortHistorySeedTransfer':
|
|
this.options.historySeedTransfers.abort(clientId, request.payload.transferId)
|
|
return {}
|
|
case 'createOrAttach':
|
|
return this.options.admission.createOrAttach(clientId, request)
|
|
case 'cancelCreateOrAttach':
|
|
return {
|
|
canceled: this.options.preparations.cancel(request.payload.sessionId, {
|
|
clientId,
|
|
...(typeof request.payload.requestId === 'string'
|
|
? { requestId: request.payload.requestId }
|
|
: {})
|
|
})
|
|
}
|
|
case 'closeStartupQueryAuthority':
|
|
return {
|
|
appliedSeq: this.options.host.closeStartupQueryAuthority(request.payload.sessionId)
|
|
}
|
|
case 'write':
|
|
return this.write(client, request.payload.sessionId, request.payload.data)
|
|
case 'resize':
|
|
return this.resize(
|
|
client,
|
|
request.payload.sessionId,
|
|
request.payload.cols,
|
|
request.payload.rows
|
|
)
|
|
case 'pausePty':
|
|
this.options.host.pauseProducer(request.payload.sessionId)
|
|
return {}
|
|
case 'resumePty':
|
|
this.options.host.resumeProducer(request.payload.sessionId)
|
|
return {}
|
|
case 'setSessionBackground':
|
|
return this.options.sessionBackgroundRouting.setBackground(
|
|
request.payload.sessionId,
|
|
request.payload.background === true
|
|
)
|
|
case 'kill':
|
|
return this.kill(clientId, request.payload.sessionId, request.payload.immediate)
|
|
case 'signal':
|
|
this.options.host.signal(request.payload.sessionId, request.payload.signal)
|
|
return {}
|
|
case 'detach':
|
|
this.options.attachments.detachSessionForClient(request.payload.sessionId, clientId)
|
|
this.options.log.log('session-detached', { sessionId: request.payload.sessionId })
|
|
return {}
|
|
case 'getCwd':
|
|
return { cwd: await this.options.host.getCwd(request.payload.sessionId) }
|
|
case 'getForegroundProcess':
|
|
return {
|
|
foregroundProcess: this.options.host.getForegroundProcess(request.payload.sessionId)
|
|
}
|
|
case 'inspectProcess':
|
|
return this.options.host.inspectProcess(request.payload.sessionId)
|
|
case 'confirmForegroundProcess':
|
|
return {
|
|
foregroundProcess: await this.options.host.confirmForegroundProcess(
|
|
request.payload.sessionId
|
|
)
|
|
}
|
|
case 'confirmShellForeground':
|
|
return {
|
|
confirmed: await this.options.host.confirmShellForeground(request.payload.sessionId)
|
|
}
|
|
case 'clearScrollback':
|
|
this.options.host.clearScrollback(request.payload.sessionId)
|
|
return {}
|
|
case 'listSessions':
|
|
return { sessions: this.options.host.listSessions() }
|
|
case 'shutdownIfIdle':
|
|
return this.shutdownIfIdle(clientId, request.id)
|
|
case 'getSnapshot':
|
|
return this.getSnapshot(request.payload.sessionId, request.payload.scrollbackRows)
|
|
case 'getSize':
|
|
return { size: this.options.host.getAppliedSize(request.payload.sessionId) }
|
|
case 'takePendingOutput':
|
|
return this.options.host.takePendingOutput(
|
|
request.payload.sessionId,
|
|
request.payload.includeSnapshot === true,
|
|
{ teardownSnapshot: request.payload.teardownSnapshot === true }
|
|
)
|
|
case 'ping':
|
|
return { pong: true }
|
|
case 'systemResolverHealth':
|
|
return { health: await readCurrentProcessMacSystemResolverHealth() }
|
|
case 'ptySpawnHealth':
|
|
await this.options.ptySpawnHealthCheck()
|
|
return { healthy: true }
|
|
case 'shutdown':
|
|
return this.shutdown(clientId, request.id, request.payload.killSessions)
|
|
}
|
|
throw new Error(`Unknown request type: ${(request as { type: string }).type}`)
|
|
}
|
|
|
|
private write(
|
|
client: ConnectedDaemonClient | undefined,
|
|
sessionId: string,
|
|
data: string
|
|
): Record<string, never> {
|
|
try {
|
|
this.options.attachments.recordInput(sessionId)
|
|
this.options.host.write(sessionId, data)
|
|
} catch (error) {
|
|
this.options.attachments.clearInput(sessionId)
|
|
if (error instanceof SessionNotFoundError) {
|
|
this.sendExitEvent(client, sessionId, -1)
|
|
}
|
|
throw error
|
|
}
|
|
return {}
|
|
}
|
|
|
|
private resize(
|
|
client: ConnectedDaemonClient | undefined,
|
|
sessionId: string,
|
|
cols: number,
|
|
rows: number
|
|
): Record<string, never> {
|
|
try {
|
|
this.options.host.resize(sessionId, cols, rows)
|
|
} catch (error) {
|
|
if (error instanceof SessionNotFoundError) {
|
|
this.sendExitEvent(client, sessionId, -1)
|
|
}
|
|
throw error
|
|
}
|
|
return {}
|
|
}
|
|
|
|
private async kill(
|
|
clientId: string,
|
|
sessionId: string,
|
|
immediate: boolean | undefined
|
|
): Promise<Record<string, never>> {
|
|
const canceledPendingSpawn = this.options.preparations.cancel(sessionId)
|
|
this.options.attachments.clearInput(sessionId)
|
|
const attribution = { sessionId, immediate: immediate === true, clientId }
|
|
try {
|
|
await this.options.host.kill(sessionId, { immediate })
|
|
} catch (error) {
|
|
if (!(canceledPendingSpawn && error instanceof SessionNotFoundError)) {
|
|
this.options.log.log('session-kill-failed', attribution)
|
|
throw error
|
|
}
|
|
}
|
|
this.options.log.log('session-killed', attribution)
|
|
return {}
|
|
}
|
|
|
|
private shutdownIfIdle(clientId: string, requestId: string): { retiring: boolean } {
|
|
const client = this.options.connections.get(clientId)
|
|
const retiring =
|
|
client !== undefined &&
|
|
client.streamSocket !== null &&
|
|
this.options.connections.size === 1 &&
|
|
this.options.admission.inFlight === 0 &&
|
|
this.options.host.listSessions().length === 0 &&
|
|
this.options.connections.hasOnlyTransportsFor(client)
|
|
if (retiring) {
|
|
this.options.lifecycle.retireAfterIdleReply(clientId, requestId, client.controlSocket)
|
|
}
|
|
return { retiring }
|
|
}
|
|
|
|
private async getSnapshot(sessionId: string, requestedRows: unknown): Promise<unknown> {
|
|
const startedAt = performance.now()
|
|
const scrollbackRows =
|
|
typeof requestedRows === 'number' && Number.isFinite(requestedRows)
|
|
? Math.max(0, Math.min(50_000, Math.floor(requestedRows)))
|
|
: undefined
|
|
const snapshot = await this.options.host.getSettledSnapshot(sessionId, { scrollbackRows })
|
|
const snapshotMs = performance.now() - startedAt
|
|
if (snapshotMs >= 25) {
|
|
recordDaemonStreamBacklogEvent('slowGetSnapshot', {
|
|
sessionIdSuffix: sessionId.slice(-10),
|
|
snapshotMs: Math.round(snapshotMs)
|
|
})
|
|
}
|
|
return { snapshot }
|
|
}
|
|
|
|
private async shutdown(
|
|
clientId: string,
|
|
requestId: string,
|
|
killSessions: boolean
|
|
): Promise<Record<string, never>> {
|
|
this.options.log.log('shutdown', {
|
|
reason: 'rpc',
|
|
killSessions: killSessions === true
|
|
})
|
|
const serverClose = this.options.lifecycle.beginOrdinaryShutdownFence()
|
|
if (killSessions) {
|
|
try {
|
|
await this.options.host.dispose()
|
|
} catch (error) {
|
|
this.options.log.log('shutdown-dispose-failed', {
|
|
error: error instanceof Error ? error.message : String(error)
|
|
})
|
|
}
|
|
}
|
|
const controlSocket = this.options.connections.get(clientId)?.controlSocket
|
|
if (controlSocket) {
|
|
this.options.lifecycle.deferRpcShutdownUntilReply(
|
|
clientId,
|
|
requestId,
|
|
controlSocket,
|
|
serverClose
|
|
)
|
|
} else {
|
|
this.options.lifecycle.finishRpcShutdownWithoutReply(serverClose)
|
|
}
|
|
return {}
|
|
}
|
|
|
|
private sendExitEvent(
|
|
client: ConnectedDaemonClient | undefined,
|
|
sessionId: string,
|
|
code: number
|
|
): void {
|
|
if (!client?.streamSocket) {
|
|
return
|
|
}
|
|
this.options.streamDataBatcher.enqueueControlEvent(client.clientId, sessionId, {
|
|
type: 'event',
|
|
event: 'exit',
|
|
sessionId,
|
|
payload: { code }
|
|
})
|
|
this.options.streamDataBatcher.flush(client.clientId)
|
|
}
|
|
}
|