|
|
@@ -51,16 +51,6 @@ export function spawnFailure(child: ChildProcess): Promise<Error> {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * Resolve once the child process exits (any code/signal); immediate if it is
|
|
|
- * already gone.
|
|
|
- * @param child - the child process to await.
|
|
|
- */
|
|
|
-function waitForExit(child: ChildProcess): Promise<void> {
|
|
|
- if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve()
|
|
|
- return new Promise<void>(resolve => child.once('exit', () => { resolve() }))
|
|
|
-}
|
|
|
-
|
|
|
/**
|
|
|
* Race the child's exit against a timer. Neither outcome leaves anything
|
|
|
* behind on the child: the exit listener is removed on timeout and the timer
|
|
|
@@ -104,10 +94,49 @@ export interface DisposeLadderGraces {
|
|
|
* headroom.
|
|
|
*/
|
|
|
disposeEofGraceMs: number
|
|
|
- /** POSIX tier-2 window (ms): between `SIGTERM` and the `SIGKILL` escalation. */
|
|
|
+ /**
|
|
|
+ * Termination confirmation window (ms): POSIX applies it after `SIGTERM` and again after
|
|
|
+ * `SIGKILL`; Windows applies it after the direct forced termination.
|
|
|
+ */
|
|
|
disposeGraceMs: number
|
|
|
}
|
|
|
|
|
|
+/** Force-terminate a child and reject if no exit edge arrives within the configured grace. */
|
|
|
+function forceTerminateWithin(child: ChildProcess, ms: number): Promise<void> {
|
|
|
+ if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve()
|
|
|
+ return new Promise<void>((resolve, reject) => {
|
|
|
+ let accepted = false
|
|
|
+ let settled = false
|
|
|
+ const cleanup = (): void => {
|
|
|
+ clearTimeout(timer)
|
|
|
+ child.off('exit', onExit)
|
|
|
+ child.off('error', onError)
|
|
|
+ }
|
|
|
+ const settle = (complete: () => void): void => {
|
|
|
+ if (settled) return
|
|
|
+ settled = true
|
|
|
+ cleanup()
|
|
|
+ complete()
|
|
|
+ }
|
|
|
+ const onExit = (): void => { settle(resolve) }
|
|
|
+ const onError = (error: Error): void => { settle(() => { reject(error) }) }
|
|
|
+ child.once('exit', onExit)
|
|
|
+ child.once('error', onError)
|
|
|
+ const timer = setTimeout(() => {
|
|
|
+ const disposition = accepted ? 'accepted' : 'refused'
|
|
|
+ settle(() => {
|
|
|
+ reject(new Error(`child process did not exit within ${ms}ms after SIGKILL was ${disposition}`))
|
|
|
+ })
|
|
|
+ }, ms).unref()
|
|
|
+ try {
|
|
|
+ accepted = child.kill('SIGKILL')
|
|
|
+ if (child.exitCode !== null || child.signalCode !== null) settle(resolve)
|
|
|
+ } catch (error: unknown) {
|
|
|
+ settle(() => { reject(new Error('SIGKILL failed', { cause: error })) })
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
/**
|
|
|
* Tear a child process down to quiescence, resolving only after exit: close stdin and allow
|
|
|
* cooperative flush, then use the host's graceful and forced termination semantics. POSIX
|
|
|
@@ -117,6 +146,8 @@ export interface DisposeLadderGraces {
|
|
|
* @param child - the child process to tear down.
|
|
|
* @param graces - the two grace periods, from the consuming plugin's Config.
|
|
|
* @param platform - the host platform, injectable for unit coverage.
|
|
|
+ * @throws When forced termination errors or the child does not report exit within
|
|
|
+ * `disposeGraceMs`.
|
|
|
*/
|
|
|
export async function disposeChildProcess(
|
|
|
child: ChildProcess,
|
|
|
@@ -133,9 +164,8 @@ export async function disposeChildProcess(
|
|
|
child.kill('SIGTERM')
|
|
|
if (await exitsWithin(child, graces.disposeGraceMs)) return
|
|
|
}
|
|
|
- // 3. Force-kill and await the (now-certain) exit.
|
|
|
- child.kill('SIGKILL')
|
|
|
- await waitForExit(child)
|
|
|
+ // 3. Force-kill and await a bounded exit edge.
|
|
|
+ await forceTerminateWithin(child, graces.disposeGraceMs)
|
|
|
}
|
|
|
|
|
|
/**
|