|
|
@@ -159,6 +159,22 @@ interface DirectRange {
|
|
|
signal(signal: 'SIGTERM' | 'SIGKILL'): void
|
|
|
}
|
|
|
|
|
|
+class LinuxScopeStartup {
|
|
|
+ readonly terminationSignals = new Set<NodeJS.Signals>()
|
|
|
+
|
|
|
+ constructor(readonly files: LinuxLaunchFiles, private readonly kind: 'subprocess' | 'terminal') {}
|
|
|
+
|
|
|
+ resolveOutcome(outcome: SubprocessOutcome): SubprocessOutcome {
|
|
|
+ const startup = readLinuxStartupError(this.files.startupErrorPath)
|
|
|
+ if (startup !== undefined) throw deserializeRunnerError(startup.error)
|
|
|
+ if (existsSync(this.files.requestPath)
|
|
|
+ && !(outcome.signal !== null && this.terminationSignals.has(outcome.signal))) {
|
|
|
+ throw new Error(`${this.kind} scope exited before its bootstrap consumed the launch request`)
|
|
|
+ }
|
|
|
+ return outcome
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
class SystemdScopeOwner implements BoundProcessOwner {
|
|
|
private establishment: 'pending' | 'established' = 'pending'
|
|
|
private stopped = false
|
|
|
@@ -169,7 +185,7 @@ class SystemdScopeOwner implements BoundProcessOwner {
|
|
|
|
|
|
constructor(
|
|
|
private readonly unit: string,
|
|
|
- private readonly files: LinuxLaunchFiles,
|
|
|
+ private readonly startup: LinuxScopeStartup,
|
|
|
private readonly direct: DirectRange,
|
|
|
private readonly systemctl: string,
|
|
|
private readonly runSync: typeof spawnSync,
|
|
|
@@ -179,6 +195,7 @@ class SystemdScopeOwner implements BoundProcessOwner {
|
|
|
|
|
|
signal(signal: 'SIGTERM' | 'SIGKILL'): void {
|
|
|
if (this.stopped) return
|
|
|
+ if (this.direct.running()) this.startup.terminationSignals.add(signal)
|
|
|
this.observeRequestConsumption()
|
|
|
const directFallbackRequired = this.establishment === 'pending'
|
|
|
if (directFallbackRequired && this.direct.running()) this.direct.signal(signal)
|
|
|
@@ -224,7 +241,7 @@ class SystemdScopeOwner implements BoundProcessOwner {
|
|
|
}
|
|
|
|
|
|
private observeRequestConsumption(): void {
|
|
|
- if (this.establishment === 'pending' && !existsSync(this.files.requestPath)) {
|
|
|
+ if (this.establishment === 'pending' && !existsSync(this.startup.files.requestPath)) {
|
|
|
this.establishment = 'established'
|
|
|
}
|
|
|
}
|
|
|
@@ -232,7 +249,7 @@ class SystemdScopeOwner implements BoundProcessOwner {
|
|
|
private absentUnit(): boolean {
|
|
|
this.observeRequestConsumption()
|
|
|
if (this.establishment === 'established') return false
|
|
|
- if (!this.direct.running() && existsSync(this.files.requestPath)) {
|
|
|
+ if (!this.direct.running() && existsSync(this.startup.files.requestPath)) {
|
|
|
return false
|
|
|
}
|
|
|
if (this.killFailure !== undefined) throw this.killFailure
|
|
|
@@ -337,7 +354,7 @@ class SystemdScopeOwner implements BoundProcessOwner {
|
|
|
}
|
|
|
|
|
|
cleanup(): void {
|
|
|
- cleanupLinuxLaunchFiles(this.files)
|
|
|
+ cleanupLinuxLaunchFiles(this.startup.files)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -358,7 +375,7 @@ function scopeArgs(unitBase: string, invocation: RunnerInvocation, argv: readonl
|
|
|
|
|
|
function directOutcome(
|
|
|
child: ReturnType<typeof spawn>,
|
|
|
- files: LinuxLaunchFiles,
|
|
|
+ startup: LinuxScopeStartup,
|
|
|
): Promise<SubprocessOutcome> {
|
|
|
return new Promise((resolveOutcome, rejectOutcome) => {
|
|
|
let settled = false
|
|
|
@@ -371,16 +388,7 @@ function directOutcome(
|
|
|
if (settled) return
|
|
|
settled = true
|
|
|
try {
|
|
|
- const startup = readLinuxStartupError(files.startupErrorPath)
|
|
|
- if (startup !== undefined) {
|
|
|
- rejectOutcome(deserializeRunnerError(startup.error))
|
|
|
- return
|
|
|
- }
|
|
|
- if (existsSync(files.requestPath)) {
|
|
|
- rejectOutcome(new Error('subprocess scope exited before its bootstrap consumed the launch request'))
|
|
|
- return
|
|
|
- }
|
|
|
- resolveOutcome({ exitCode, signal })
|
|
|
+ resolveOutcome(startup.resolveOutcome({ exitCode, signal }))
|
|
|
} catch (error) {
|
|
|
/* v8 ignore next -- Node filesystem operations throw Error instances. */
|
|
|
const failure = error instanceof Error ? error : new Error(String(error))
|
|
|
@@ -414,7 +422,7 @@ export interface LinuxTerminalScopeLaunch {
|
|
|
* @param spec - terminal target request.
|
|
|
* @param targetEnv - validated complete target environment.
|
|
|
* @param internals - optional runner and systemd seams used by tests.
|
|
|
- * @returns invocation facts and ownership callbacks for node-pty.
|
|
|
+ * @returns invocation and ownership callbacks; requested termination preserves the observed signal even before bootstrap consumption.
|
|
|
*/
|
|
|
export function prepareLinuxTerminalScope(
|
|
|
spec: SubprocessTerminalSpawnSpec,
|
|
|
@@ -423,6 +431,7 @@ export function prepareLinuxTerminalScope(
|
|
|
): LinuxTerminalScopeLaunch {
|
|
|
const invocation = internals.runnerInvocation ?? spawnRunnerInvocation()
|
|
|
const files = createLinuxLaunchFiles({ cwd: spec.cwd, env: targetEnv })
|
|
|
+ const startup = new LinuxScopeStartup(files, 'terminal')
|
|
|
const unitBase = unitStem('dsh-terminal')
|
|
|
return {
|
|
|
command: internals.systemdRun ?? 'systemd-run',
|
|
|
@@ -431,21 +440,14 @@ export function prepareLinuxTerminalScope(
|
|
|
env: runnerEnvironment(files.requestPath, invocation),
|
|
|
bindOwner: direct => new SystemdScopeOwner(
|
|
|
`${unitBase}.scope`,
|
|
|
- files,
|
|
|
+ startup,
|
|
|
direct,
|
|
|
internals.systemctl ?? 'systemctl',
|
|
|
internals.spawnSync ?? spawnSync,
|
|
|
internals.systemctlQuery ?? querySystemctl,
|
|
|
internals.sleep ?? sleepWithAbort,
|
|
|
),
|
|
|
- resolveOutcome: (outcome) => {
|
|
|
- const startup = readLinuxStartupError(files.startupErrorPath)
|
|
|
- if (startup !== undefined) throw deserializeRunnerError(startup.error)
|
|
|
- if (existsSync(files.requestPath)) {
|
|
|
- throw new Error('terminal scope exited before its bootstrap consumed the launch request')
|
|
|
- }
|
|
|
- return outcome
|
|
|
- },
|
|
|
+ resolveOutcome: outcome => startup.resolveOutcome(outcome),
|
|
|
cleanup: () => { cleanupLinuxLaunchFiles(files) },
|
|
|
}
|
|
|
}
|
|
|
@@ -455,7 +457,7 @@ export function prepareLinuxTerminalScope(
|
|
|
* @param spec - ordinary target request.
|
|
|
* @param targetEnv - validated complete target environment.
|
|
|
* @param internals - optional runner and systemd seams used by tests.
|
|
|
- * @returns direct streams, result, and managed-scope owner.
|
|
|
+ * @returns streams, result, and scope owner; requested termination preserves the observed signal even before bootstrap consumption.
|
|
|
*/
|
|
|
export function launchLinuxScope(
|
|
|
spec: SubprocessSpawnSpec,
|
|
|
@@ -464,6 +466,7 @@ export function launchLinuxScope(
|
|
|
): ManagedProcessLaunch {
|
|
|
const invocation = internals.runnerInvocation ?? spawnRunnerInvocation()
|
|
|
const files = createLinuxLaunchFiles({ cwd: spec.cwd, env: targetEnv })
|
|
|
+ const startup = new LinuxScopeStartup(files, 'subprocess')
|
|
|
const unitBase = unitStem('dsh-subprocess')
|
|
|
let child: ReturnType<typeof spawn>
|
|
|
try {
|
|
|
@@ -483,7 +486,7 @@ export function launchLinuxScope(
|
|
|
}
|
|
|
const owner = new SystemdScopeOwner(
|
|
|
`${unitBase}.scope`,
|
|
|
- files,
|
|
|
+ startup,
|
|
|
{
|
|
|
running: () => child.pid !== undefined && child.exitCode === null && child.signalCode === null,
|
|
|
signal: (signal) => { signalChildGroup(child, signal) },
|
|
|
@@ -497,7 +500,7 @@ export function launchLinuxScope(
|
|
|
stdin: child.stdin,
|
|
|
stdout: child.stdout,
|
|
|
stderr: child.stderr,
|
|
|
- direct: directOutcome(child, files),
|
|
|
+ direct: directOutcome(child, startup),
|
|
|
owner,
|
|
|
}
|
|
|
}
|