mirror of
https://github.com/stablyai/orca.git
synced 2026-10-24 00:04:01 +00:00
fix(relay): assign Windows kill-on-close job
This commit is contained in:
@@ -3,6 +3,7 @@ import { readLaunchVersion } from './relay-handshake'
|
||||
import type { RelayLaunchOptions } from './relay-launch-options'
|
||||
import { RELAY_EMPTY_DETACHED_STARTUP_GRACE_MS, RELAY_IDLE_GRACE_MS } from './relay-launch-options'
|
||||
import { relayLogLine } from './relay-diagnostic-log'
|
||||
import { assignRelayProcessToKillOnCloseJob } from './relay-windows-pty-job'
|
||||
import { RelayPrimaryChannel } from './relay-primary-channel'
|
||||
import { RelayRuntimeServices } from './relay-runtime-services'
|
||||
import { RelayAgentHookRuntime } from './relay-agent-hook-runtime'
|
||||
@@ -18,6 +19,7 @@ export async function runRelayDaemon(
|
||||
if (options.detached && options.logFile) {
|
||||
installRelayLogRotation(options.logFile)
|
||||
}
|
||||
assignRelayProcessToKillOnCloseJob()
|
||||
|
||||
const socketOwnership = new RelaySocketOwnership(options.sockPath)
|
||||
process.on('uncaughtException', (error) => {
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { assignRelayProcessToKillOnCloseJob } from './relay-windows-pty-job'
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
function captureRelayLog(): { lines: () => string } {
|
||||
const write = vi.spyOn(process.stderr, 'write').mockImplementation(() => true)
|
||||
return {
|
||||
lines: () => write.mock.calls.map(([line]) => String(line)).join('')
|
||||
}
|
||||
}
|
||||
|
||||
describe('assignRelayProcessToKillOnCloseJob', () => {
|
||||
it('reports successful Windows assignment without a degradation log', () => {
|
||||
const log = captureRelayLog()
|
||||
const assignCurrentProcessToJob = vi.fn().mockReturnValue(true)
|
||||
|
||||
expect(assignRelayProcessToKillOnCloseJob('win32', () => ({ assignCurrentProcessToJob }))).toBe(
|
||||
'assigned'
|
||||
)
|
||||
expect(assignCurrentProcessToJob).toHaveBeenCalledOnce()
|
||||
expect(log.lines()).toBe('')
|
||||
})
|
||||
|
||||
it('keeps startup available and logs when the native export is absent', () => {
|
||||
const log = captureRelayLog()
|
||||
|
||||
expect(assignRelayProcessToKillOnCloseJob('win32', () => ({}))).toBe('unavailable')
|
||||
expect(log.lines()).toContain(
|
||||
'[relay] Windows kill-on-close job unavailable: ConPTY addon does not export assignCurrentProcessToJob'
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps startup available and logs when Windows refuses assignment', () => {
|
||||
const log = captureRelayLog()
|
||||
|
||||
expect(
|
||||
assignRelayProcessToKillOnCloseJob('win32', () => ({
|
||||
assignCurrentProcessToJob: () => false
|
||||
}))
|
||||
).toBe('unavailable')
|
||||
expect(log.lines()).toContain(
|
||||
'[relay] Windows kill-on-close job unavailable: assignment was refused'
|
||||
)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['native load', () => new Error('binding unavailable')],
|
||||
['native assignment', () => new Error('job nesting denied')]
|
||||
])('keeps startup available when %s throws', (phase, createError) => {
|
||||
const log = captureRelayLog()
|
||||
const loader =
|
||||
phase === 'native load'
|
||||
? () => {
|
||||
throw createError()
|
||||
}
|
||||
: () => ({
|
||||
assignCurrentProcessToJob: () => {
|
||||
throw createError()
|
||||
}
|
||||
})
|
||||
|
||||
expect(assignRelayProcessToKillOnCloseJob('win32', loader)).toBe('unavailable')
|
||||
expect(log.lines()).toContain(createError().message)
|
||||
})
|
||||
|
||||
it.each(['darwin', 'linux'] as const)('does nothing on %s', (platform) => {
|
||||
const log = captureRelayLog()
|
||||
const loadNative = vi.fn()
|
||||
|
||||
expect(assignRelayProcessToKillOnCloseJob(platform, loadNative)).toBe('not-applicable')
|
||||
expect(loadNative).not.toHaveBeenCalled()
|
||||
expect(log.lines()).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('relay daemon startup ordering', () => {
|
||||
it('assigns exactly once before relay ownership or request handling begins', () => {
|
||||
const source = readFileSync(new URL('./relay-daemon.ts', import.meta.url), 'utf8')
|
||||
const body = source.slice(
|
||||
source.indexOf('export async function runRelayDaemon'),
|
||||
source.indexOf('function registerRelayStatus')
|
||||
)
|
||||
const assignment = 'assignRelayProcessToKillOnCloseJob()'
|
||||
const assignmentIndex = body.indexOf(assignment)
|
||||
|
||||
expect(body.split(assignment)).toHaveLength(2)
|
||||
expect(assignmentIndex).toBeGreaterThan(
|
||||
body.indexOf('installRelayLogRotation(options.logFile)')
|
||||
)
|
||||
for (const initialization of [
|
||||
'new RelaySocketOwnership(',
|
||||
'new RelayPrimaryChannel(',
|
||||
'new RelayRuntimeServices(',
|
||||
'new RelayReconnectListener('
|
||||
]) {
|
||||
expect(assignmentIndex).toBeLessThan(body.indexOf(initialization))
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,53 @@
|
||||
import { createRequire } from 'node:module'
|
||||
import { relayLogLine } from './relay-diagnostic-log'
|
||||
|
||||
type ConptyNative = {
|
||||
assignCurrentProcessToJob: () => boolean
|
||||
}
|
||||
|
||||
export type RelayWindowsPtyJobOutcome = 'assigned' | 'unavailable' | 'not-applicable'
|
||||
|
||||
const requireFromRelay = createRequire(__filename)
|
||||
|
||||
function loadConptyNative(): unknown {
|
||||
const { loadNativeModule } = requireFromRelay('node-pty/lib/utils') as {
|
||||
loadNativeModule: (name: string) => { module: unknown }
|
||||
}
|
||||
return loadNativeModule('conpty').module
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
|
||||
function reportUnavailable(reason: string): RelayWindowsPtyJobOutcome {
|
||||
relayLogLine(`[relay] Windows kill-on-close job unavailable: ${reason}`)
|
||||
return 'unavailable'
|
||||
}
|
||||
|
||||
export function assignRelayProcessToKillOnCloseJob(
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
loadNative: () => unknown = loadConptyNative
|
||||
): RelayWindowsPtyJobOutcome {
|
||||
if (platform !== 'win32') {
|
||||
return 'not-applicable'
|
||||
}
|
||||
|
||||
let native: Partial<ConptyNative> | null
|
||||
try {
|
||||
native = loadNative() as Partial<ConptyNative> | null
|
||||
} catch (error) {
|
||||
return reportUnavailable(`ConPTY addon failed to load: ${errorMessage(error)}`)
|
||||
}
|
||||
if (typeof native?.assignCurrentProcessToJob !== 'function') {
|
||||
return reportUnavailable('ConPTY addon does not export assignCurrentProcessToJob')
|
||||
}
|
||||
|
||||
try {
|
||||
return native.assignCurrentProcessToJob()
|
||||
? 'assigned'
|
||||
: reportUnavailable('assignment was refused')
|
||||
} catch (error) {
|
||||
return reportUnavailable(`assignment failed: ${errorMessage(error)}`)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user