fix(relay): assign Windows kill-on-close job

This commit is contained in:
Brennan Benson
2026-08-28 08:04:03 -07:00
parent 94e7586665
commit 0bd6f220df
3 changed files with 158 additions and 0 deletions
+2
View File
@@ -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) => {
+103
View File
@@ -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))
}
})
})
+53
View File
@@ -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)}`)
}
}