mirror of
https://github.com/stablyai/orca.git
synced 2026-11-03 18:47:40 +00:00
Extract subprocess execution and process contracts into @orca/process-host with explicit public entry points, independent checks, and one Node 18 compatible CommonJS runtime. Migrate application consumers and integrate package builds, dependency selection, import enforcement, and runtime resource staging. Preserve daemon startup ownership and profile isolation, and qualify development rebuild recovery and concurrent output publication. Remove unnecessary aliases, test forwarding exports, and a duplicate output scan. Validation: full typechecks, changed-code quality, package and artifact checks, native Windows builds/types plus 306 runtime tests, and independent native reviews. Matched full E2E controls on original main and unchanged reviewed head both pass all 17 applicable tests (nine skips). Earlier intermittent E2E failures are documented in the PR; no assertions were weakened. The final combined-tree audit preserves both branches with no import-boundary violations.
268 lines
7.8 KiB
TypeScript
268 lines
7.8 KiB
TypeScript
import { spawnProcess } from '@orca/process-host'
|
|
|
|
export const LINUX_LID_SLEEP_ASSERTION_RETRY_MS = 30_000
|
|
|
|
type Logger = Pick<Console, 'debug' | 'warn'>
|
|
|
|
type SystemdInhibitErrorListener = (error: Error & { code?: string }) => void
|
|
type SystemdInhibitExitListener = (code: number | null, signal: NodeJS.Signals | null) => void
|
|
|
|
type SystemdInhibitProcess = {
|
|
kill: () => boolean
|
|
stdin: {
|
|
destroy(): void
|
|
on(event: 'error', listener: SystemdInhibitErrorListener): void
|
|
} | null
|
|
on(event: 'error', listener: SystemdInhibitErrorListener): void
|
|
on(event: 'exit', listener: SystemdInhibitExitListener): void
|
|
on(event: 'close', listener: () => void): void
|
|
off(event: 'error', listener: SystemdInhibitErrorListener): void
|
|
off(event: 'exit', listener: SystemdInhibitExitListener): void
|
|
off(event: 'close', listener: () => void): void
|
|
pid?: number
|
|
}
|
|
|
|
type SystemdInhibitSpawn = (
|
|
command: string,
|
|
args: string[],
|
|
options: { stdio: ['pipe', 'ignore', 'ignore']; windowsHide: true; shell?: false }
|
|
) => SystemdInhibitProcess
|
|
|
|
type LinuxLidSleepAssertionOptions = {
|
|
logger?: Logger
|
|
now?: () => number
|
|
onUnexpectedFailure?: (reason: string) => void
|
|
platform?: NodeJS.Platform
|
|
spawn?: SystemdInhibitSpawn
|
|
}
|
|
|
|
export class LinuxLidSleepAssertion {
|
|
private readonly logger: Logger
|
|
private readonly now: () => number
|
|
private readonly onUnexpectedFailure: (reason: string) => void
|
|
private readonly platform: NodeJS.Platform
|
|
private readonly spawn: SystemdInhibitSpawn
|
|
private child: SystemdInhibitProcess | null = null
|
|
private retryNotBefore: number | null = null
|
|
private retryTimer: ReturnType<typeof setTimeout> | null = null
|
|
private systemdInhibitUnavailable = false
|
|
private lastFailureKey: string | null = null
|
|
private warnedForLastFailure = false
|
|
private readonly intentionalStops = new WeakSet<SystemdInhibitProcess>()
|
|
private readonly reportedFailures = new WeakSet<SystemdInhibitProcess>()
|
|
private readonly childCleanups = new WeakMap<SystemdInhibitProcess, () => void>()
|
|
|
|
constructor(options: LinuxLidSleepAssertionOptions = {}) {
|
|
this.logger = options.logger ?? console
|
|
this.now = options.now ?? Date.now
|
|
this.onUnexpectedFailure = options.onUnexpectedFailure ?? (() => {})
|
|
this.platform = options.platform ?? process.platform
|
|
this.spawn =
|
|
options.spawn ?? ((program, args, { stdio }) => spawnProcess({ program, args, stdio }))
|
|
}
|
|
|
|
start(reason: string): void {
|
|
if (this.platform !== 'linux' || this.child || this.systemdInhibitUnavailable) {
|
|
return
|
|
}
|
|
if (this.retryNotBefore !== null && this.now() < this.retryNotBefore) {
|
|
this.scheduleRetry()
|
|
return
|
|
}
|
|
|
|
let child: SystemdInhibitProcess
|
|
try {
|
|
// logind's lid switch handling ignores ordinary sleep inhibitors on many systems,
|
|
// so Linux needs both a sleep lock and a handle-lid-switch lock.
|
|
child = this.spawn(
|
|
'systemd-inhibit',
|
|
[
|
|
'--what=sleep:handle-lid-switch',
|
|
'--who=Orca',
|
|
'--why=Agents are working',
|
|
'--mode=block',
|
|
// EOF releases the inhibitor even when Orca is killed without running cleanup.
|
|
'cat'
|
|
],
|
|
{
|
|
stdio: ['pipe', 'ignore', 'ignore'],
|
|
windowsHide: true
|
|
}
|
|
)
|
|
} catch (error) {
|
|
this.handleFailure('spawn-error', reason, error, 'spawn-error')
|
|
return
|
|
}
|
|
|
|
this.child = child
|
|
const onError: SystemdInhibitErrorListener = (error) => {
|
|
this.handleChildFailure(
|
|
child,
|
|
`error:${String(error.code ?? error.message)}`,
|
|
'error',
|
|
reason,
|
|
error
|
|
)
|
|
}
|
|
const onExit: SystemdInhibitExitListener = (code, signal) => {
|
|
this.handleChildFailure(child, `exit:${String(code)}:${String(signal)}`, 'exit', reason, {
|
|
code,
|
|
signal
|
|
})
|
|
}
|
|
const onClose = (): void => this.detachChildListeners(child)
|
|
this.childCleanups.set(child, () => {
|
|
child.off('error', onError)
|
|
child.off('exit', onExit)
|
|
child.off('close', onClose)
|
|
})
|
|
child.on('error', onError)
|
|
child.on('exit', onExit)
|
|
child.on('close', onClose)
|
|
child.stdin?.on('error', onError)
|
|
this.resetRetrySuppression()
|
|
this.resetFailureStreak()
|
|
}
|
|
|
|
stop(_reason: string): void {
|
|
this.resetRetrySuppression()
|
|
this.resetFailureStreak()
|
|
if (!this.child) {
|
|
return
|
|
}
|
|
const child = this.child
|
|
this.child = null
|
|
// Pipe and child errors can arrive after stop; close ends both event sources.
|
|
this.intentionalStops.add(child)
|
|
try {
|
|
if (child.stdin) {
|
|
child.stdin.destroy()
|
|
} else {
|
|
child.kill()
|
|
}
|
|
} catch (error) {
|
|
if (!isEsrchError(error)) {
|
|
this.logger.warn('[agent-awake] failed to stop Linux lid sleep assertion', { error })
|
|
}
|
|
}
|
|
}
|
|
|
|
dispose(): void {
|
|
this.stop('dispose')
|
|
}
|
|
|
|
private handleChildFailure(
|
|
child: SystemdInhibitProcess,
|
|
failureKey: string,
|
|
failureType: 'error' | 'exit',
|
|
startReason: string,
|
|
details: unknown
|
|
): void {
|
|
child.stdin?.destroy()
|
|
if (this.intentionalStops.has(child)) {
|
|
return
|
|
}
|
|
if (this.reportedFailures.has(child)) {
|
|
return
|
|
}
|
|
this.reportedFailures.add(child)
|
|
if (this.child === child) {
|
|
this.child = null
|
|
}
|
|
this.handleFailure(failureKey, startReason, details, failureType)
|
|
}
|
|
|
|
private detachChildListeners(child: SystemdInhibitProcess): void {
|
|
const cleanup = this.childCleanups.get(child)
|
|
if (!cleanup) {
|
|
return
|
|
}
|
|
cleanup()
|
|
this.childCleanups.delete(child)
|
|
}
|
|
|
|
private handleFailure(
|
|
failureKey: string,
|
|
reason: string,
|
|
details: unknown,
|
|
failureType: 'error' | 'exit' | 'spawn-error'
|
|
): void {
|
|
if (isMissingSystemdInhibit(details)) {
|
|
this.systemdInhibitUnavailable = true
|
|
this.resetRetrySuppression()
|
|
this.logFailure('systemd-inhibit-missing', reason, details, failureType)
|
|
return
|
|
}
|
|
this.logFailure(failureKey, reason, details, failureType)
|
|
this.retryNotBefore = this.now() + LINUX_LID_SLEEP_ASSERTION_RETRY_MS
|
|
this.scheduleRetry()
|
|
this.onUnexpectedFailure('linux-lid-assertion-failure')
|
|
}
|
|
|
|
private logFailure(
|
|
failureKey: string,
|
|
reason: string,
|
|
details: unknown,
|
|
failureType: 'error' | 'exit' | 'spawn-error'
|
|
): void {
|
|
const payload = {
|
|
reason,
|
|
failureType,
|
|
details
|
|
}
|
|
if (this.lastFailureKey === failureKey && this.warnedForLastFailure) {
|
|
this.logger.debug('[agent-awake] Linux lid sleep assertion failed repeatedly', payload)
|
|
return
|
|
}
|
|
this.lastFailureKey = failureKey
|
|
this.warnedForLastFailure = true
|
|
this.logger.warn('[agent-awake] Linux lid sleep assertion failed', payload)
|
|
}
|
|
|
|
private resetFailureStreak(): void {
|
|
this.lastFailureKey = null
|
|
this.warnedForLastFailure = false
|
|
}
|
|
|
|
private scheduleRetry(): void {
|
|
if (this.retryNotBefore === null || this.retryTimer) {
|
|
return
|
|
}
|
|
const retryDelay = Math.max(0, this.retryNotBefore - this.now())
|
|
this.retryTimer = setTimeout(() => {
|
|
this.retryTimer = null
|
|
this.onUnexpectedFailure('linux-lid-assertion-retry')
|
|
}, retryDelay)
|
|
if (typeof this.retryTimer.unref === 'function') {
|
|
this.retryTimer.unref()
|
|
}
|
|
}
|
|
|
|
private resetRetrySuppression(): void {
|
|
this.retryNotBefore = null
|
|
if (!this.retryTimer) {
|
|
return
|
|
}
|
|
clearTimeout(this.retryTimer)
|
|
this.retryTimer = null
|
|
}
|
|
}
|
|
|
|
function isMissingSystemdInhibit(error: unknown): boolean {
|
|
return (
|
|
typeof error === 'object' &&
|
|
error !== null &&
|
|
'code' in error &&
|
|
(error as { code?: unknown }).code === 'ENOENT'
|
|
)
|
|
}
|
|
|
|
function isEsrchError(error: unknown): boolean {
|
|
return (
|
|
typeof error === 'object' &&
|
|
error !== null &&
|
|
'code' in error &&
|
|
(error as { code?: unknown }).code === 'ESRCH'
|
|
)
|
|
}
|