mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-11 04:00:38 +00:00
229 lines
7.6 KiB
TypeScript
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 }
|
|
}
|