Files
deepseek-harness/packages/subprocess/subprocess-local/src/linux-scope.ts
T

229 lines
7.6 KiB
TypeScript

/** Linux user-systemd scope launch and managed-range ownership. */
import { randomBytes } from 'node:crypto'
import { execFile, spawn, spawnSync } from 'node:child_process'
import type { ChildProcess } from 'node:child_process'
import { setTimeout as sleepMs } from 'node:timers/promises'
import type { SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
import type { BoundProcessOwner, ManagedProcessLaunch } from './managed-owner.ts'
import { observeChildClose, waitWithAbort } from './managed-owner.ts'
import { childEnv } from './spawn.ts'
import {
cleanupAfterRunner,
runnerDirectResult,
runnerFiles,
runnerStdio,
spawnRunnerInvocation,
} from './runner-launch.ts'
/** Test seams for systemd command execution. */
export interface LinuxScopeInternals {
spawn?: typeof spawn
spawnSync?: typeof spawnSync
systemctlQuery?: (command: string, args: readonly string[]) => Promise<SystemctlResult>
systemdRun?: string
systemctl?: string
runnerInvocation?: string[]
}
interface SystemctlResult {
status: number | null
stdout: string
stderr: string
error?: Error
}
const SYSTEMCTL_TIMEOUT_MS = 5_000
const SCOPE_POLL_INTERVAL_MS = 200
function querySystemctl(command: string, args: readonly string[]): Promise<SystemctlResult> {
return new Promise((resolve) => {
execFile(command, [...args], { encoding: 'utf8', timeout: SYSTEMCTL_TIMEOUT_MS }, (error, stdout, stderr) => {
const code = error === null ? 0 : (error as Error & { code?: string | number }).code
resolve({
status: typeof code === 'number' ? code : error === null ? 0 : null,
stdout,
stderr,
...error === null ? {} : { error },
})
})
})
}
function syncQuerySystemctl(
runSync: typeof spawnSync,
command: string,
args: readonly string[],
): Promise<SystemctlResult> {
const result = runSync(command, [...args], { encoding: 'utf8', timeout: SYSTEMCTL_TIMEOUT_MS })
return Promise.resolve({
status: result.status,
stdout: typeof result.stdout === 'string' ? result.stdout : '',
stderr: typeof result.stderr === 'string' ? result.stderr : '',
...result.error === undefined ? {} : { error: result.error },
})
}
function unitStem(prefix: string): string {
return `${prefix}-${process.pid}-${randomBytes(6).toString('hex')}`
}
/**
* Confirm a modern readable user manager and literal-argument scope launch.
* @param internals - injected command paths and runners.
* @returns true only before any user command is selected for native launch.
*/
export function probeLinuxScope(internals: LinuxScopeInternals = {}): boolean {
const runSync = internals.spawnSync ?? spawnSync
const systemdRun = internals.systemdRun ?? 'systemd-run'
const systemctl = internals.systemctl ?? 'systemctl'
const timeout = 5_000
const manager = runSync(systemctl, ['--user', 'show-environment'], {
encoding: 'utf8',
stdio: 'ignore',
timeout,
})
if (manager.error !== undefined || manager.status !== 0) return false
const probe = runSync(systemdRun, [
'--user',
'--scope',
'--quiet',
'--collect',
'--pipe',
'--expand-environment=no',
`--unit=${unitStem('dsh-subprocess-probe')}`,
'--',
process.execPath,
'-e',
'',
], {
env: childEnv(),
stdio: 'ignore',
timeout,
})
return probe.error === undefined && probe.status === 0
}
class SystemdScopeOwner implements BoundProcessOwner {
private stopped = false
private observation: Promise<void> | undefined
private killConfirmed = false
private killFailure: Error | undefined
constructor(
private readonly unit: string,
private readonly systemctl: string,
private readonly runSync: typeof spawnSync,
private readonly query: (command: string, args: readonly string[]) => Promise<SystemctlResult>,
private readonly runner: ChildProcess,
) {}
signal(signal: NodeJS.Signals): void {
if (this.stopped) return
const result = this.runSync(this.systemctl, [
'--user',
'kill',
'--kill-whom=all',
`--signal=${signal}`,
this.unit,
], { encoding: 'utf8', timeout: SYSTEMCTL_TIMEOUT_MS })
if (result.error === undefined && result.status === 0) {
if (signal === 'SIGKILL') this.killConfirmed = true
return
}
const output = `${result.stdout}\n${result.stderr}`
if (/not found|could not be found|no such/iu.test(output)) {
this.stopped = true
return
}
if (signal === 'SIGKILL') {
this.killFailure = result.error ?? new Error(
`systemctl could not signal ${this.unit}: ${output.trim() || `exit ${String(result.status)}`}`,
)
}
}
private async active(): Promise<boolean> {
if (this.killFailure !== undefined) throw this.killFailure
const result = await this.query(this.systemctl, [
'--user',
'show',
this.unit,
'--property=ActiveState',
'--value',
])
const output = `${result.stdout}\n${result.stderr}`
if (result.status !== 0) {
if (/not found|could not be found|no such/iu.test(output)) {
return this.runner.exitCode === null && this.runner.signalCode === null
}
if (result.error !== undefined) throw result.error
throw new Error(`systemctl could not read ${this.unit}: ${output.trim() || `exit ${String(result.status)}`}`)
}
const state = result.stdout.trim()
if (state === 'inactive' || state === 'failed') return false
if (state === 'active' || state === 'activating' || state === 'deactivating') return true
throw new Error(`systemctl returned unknown ActiveState for ${this.unit}: ${JSON.stringify(state)}`)
}
async waitForExit(signal?: AbortSignal): Promise<boolean> {
if (this.stopped) return true
this.observation ??= (async () => {
while (await this.active()) await sleepMs(SCOPE_POLL_INTERVAL_MS)
this.stopped = true
})()
return waitWithAbort(this.observation, signal)
}
forcedOutcome(): { exitCode: null; signal: 'SIGKILL' } | undefined {
return this.killConfirmed ? { exitCode: null, signal: 'SIGKILL' } : undefined
}
}
/**
* Launch one direct command inside a transient user scope.
* @param spec - exact target argv, cwd, stdio, environment, and lifecycle settings.
* @param internals - injected command runners used by platform tests.
* @returns wrapper streams, target outcome, and the bound scope owner.
*/
export function launchLinuxScope(
spec: SubprocessSpawnSpec,
internals: LinuxScopeInternals = {},
): ManagedProcessLaunch {
const run = internals.spawn ?? spawn
const runSync = internals.spawnSync ?? spawnSync
const query = internals.systemctlQuery ?? (internals.spawnSync === undefined
? querySystemctl
: (command, args) => syncQuerySystemctl(runSync, command, args))
const systemdRun = internals.systemdRun ?? 'systemd-run'
const systemctl = internals.systemctl ?? 'systemctl'
const invocation = internals.runnerInvocation ?? spawnRunnerInvocation()
const files = runnerFiles(spec)
const unitBase = unitStem('dsh-subprocess')
const child = run(systemdRun, [
'--user',
'--scope',
'--quiet',
'--collect',
'--pipe',
'--expand-environment=no',
`--unit=${unitBase}`,
'--',
...invocation,
'--mode',
'node',
'--request',
files.requestPath,
'--events',
files.eventsPath,
], {
env: childEnv(),
stdio: runnerStdio(spec),
})
const closed = observeChildClose(child)
const owner = new SystemdScopeOwner(`${unitBase}.scope`, systemctl, runSync, query, child)
const result = runnerDirectResult(child, files, closed, () => owner.forcedOutcome())
cleanupAfterRunner(files, result.direct, closed)
return { child, pid: result.pid, direct: result.direct, closed, owner }
}