@gobing-ai/ts-runtime 0.4.57 → 0.4.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/process-executor.d.ts +36 -4
- package/dist/process-executor.d.ts.map +1 -1
- package/dist/process-executor.js +182 -38
- package/package.json +4 -4
- package/src/index.ts +1 -0
- package/src/process-executor.ts +257 -41
package/README.md
CHANGED
|
@@ -598,17 +598,41 @@ unsub();
|
|
|
598
598
|
```
|
|
599
599
|
|
|
600
600
|
`rejectOnError: true` throws on non-zero exits. `OutputPolicy` controls
|
|
601
|
-
buffered vs streamed output. `ProcessOptions` supports timeout
|
|
601
|
+
buffered vs streamed output. `ProcessOptions` supports `timeout`, env, cwd,
|
|
602
602
|
maxOutput, forceBuffered, an optional synchronous `onOutput({ stream, chunk,
|
|
603
603
|
timestamp })` observer, and registry fields (`source`, `teamId`, `agentId`).
|
|
604
604
|
`onOutput` observes live chunks while the returned `ProcessResult` retains the
|
|
605
605
|
complete buffered stdout/stderr; observers must enqueue and return, and thrown
|
|
606
|
-
observer errors are isolated from the child.
|
|
607
|
-
commands run in an isolated process group on Unix so abort terminates descendants
|
|
608
|
-
that could otherwise retain the output pipes; Windows falls back to direct-child
|
|
609
|
-
cancellation. Inject the same `ProcessRegistry` into every executor that should
|
|
606
|
+
observer errors are isolated from the child. Inject the same `ProcessRegistry` into every executor that should
|
|
610
607
|
appear in one watch list; without a registry, behavior is unchanged.
|
|
611
608
|
|
|
609
|
+
#### Deadlines, cancellation, and process-group containment (Unix)
|
|
610
|
+
|
|
611
|
+
On Unix, a finite `timeout` or a supplied `signal` puts the run under executor-owned
|
|
612
|
+
process-group containment: the child is spawned detached as a process-group leader, the
|
|
613
|
+
executor runs one termination sequence (group `SIGTERM`, a `killGraceMs` grace — default
|
|
614
|
+
5000 ms — then group `SIGKILL`), and escalation is driven by group liveness, never by the
|
|
615
|
+
leader exiting first. Completion therefore reaps descendants that survive the leader while
|
|
616
|
+
holding inherited output pipes or a SQLite write transaction, so `run()` completion is a
|
|
617
|
+
containment barrier: after it resolves, no owned descendant remains (best-effort for
|
|
618
|
+
SIGKILL-immune members — escalation gives up after a bounded settle). `runStreaming` keeps
|
|
619
|
+
direct-child semantics and provides no group containment.
|
|
620
|
+
|
|
621
|
+
- `timeout: number` — positive-integer deadline in ms; expiry reports `outcome: 'timeout'`.
|
|
622
|
+
- `timeout: null` — unlimited; overrides a finite `defaultTimeout` from `ProcessExecutorConfig`.
|
|
623
|
+
- omitted — inherits `defaultTimeout` (which may itself be `null`/unlimited).
|
|
624
|
+
- `timeout: 0`, negative, fractional, `NaN`, `±Infinity`, or beyond the native timer range —
|
|
625
|
+
rejected with `TypeError` before spawn; `killGraceMs` must be a non-negative integer.
|
|
626
|
+
- abort `signal` — reports `outcome: 'cancelled'`; a deadline is never a cancellation and
|
|
627
|
+
vice versa. Deadline and abort feed one escalation path, so callers must not arm a
|
|
628
|
+
competing watchdog over the same child.
|
|
629
|
+
|
|
630
|
+
`ProcessResult.outcome` (`'exit' | 'timeout' | 'cancelled' | 'signal' | 'error'`, aliased as
|
|
631
|
+
`ProcessExitReason`) states why the run ended; `process.exited` events carry the same reason.
|
|
632
|
+
On Windows (and when no deadline or signal is supplied), execa's direct-child timeout and
|
|
633
|
+
cancellation semantics apply unchanged: only the direct child is killed, and the outcome is
|
|
634
|
+
classified from the result.
|
|
635
|
+
|
|
612
636
|
Cloudflare Workers do not expose process execution; check
|
|
613
637
|
`factory.capabilities.hasProcessExecution` first.
|
|
614
638
|
|
package/dist/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export { createNodeFileSystem, findProjectRoot } from './file-system-node';
|
|
|
8
8
|
export { atomicWriteFile, atomicWriteJson, createLogStream, ensureDirForFile, readJsonFile, walkDir, writeJsonFile, } from './fs';
|
|
9
9
|
export * from './path';
|
|
10
10
|
export { _resetRuntimeFactory, isCloudflareWorkerRuntime, loadRuntimeFactory } from './platform';
|
|
11
|
-
export type { OutputPolicy, PipeProcess, PipeProcessOptions, ProcessEventDetail, ProcessEventSink, ProcessEvents, ProcessExecutorConfig, ProcessExitReason, ProcessOptions, ProcessOutputChunk, ProcessResult, ProcessSignal, TracerPort, } from './process-executor';
|
|
11
|
+
export type { OutputPolicy, PipeProcess, PipeProcessOptions, ProcessEventDetail, ProcessEventSink, ProcessEvents, ProcessExecutorConfig, ProcessExitReason, ProcessOptions, ProcessOutcome, ProcessOutputChunk, ProcessResult, ProcessSignal, TracerPort, } from './process-executor';
|
|
12
12
|
export { NodeProcessExecutor, ProcessExecutor } from './process-executor';
|
|
13
13
|
export type { InMemoryProcessRegistryOptions, ProcessExecution, ProcessExecutionBegin, ProcessExecutionComplete, ProcessExecutionFilter, ProcessExecutionSource, ProcessExecutionStatus, ProcessRegistry, ProcessRegistryEvent, } from './process-registry';
|
|
14
14
|
export { createInMemoryProcessRegistry, InMemoryProcessRegistry } from './process-registry';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,+BAA+B,EAAE,MAAM,WAAW,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAC9E,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC3E,OAAO,EACH,eAAe,EACf,eAAe,EACf,eAAe,EACf,gBAAgB,EAChB,YAAY,EACZ,OAAO,EACP,aAAa,GAChB,MAAM,MAAM,CAAC;AACd,cAAc,QAAQ,CAAC;AACvB,OAAO,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACjG,YAAY,EACR,YAAY,EACZ,WAAW,EACX,kBAAkB,EAClB,kBAAkB,EAClB,gBAAgB,EAChB,aAAa,EACb,qBAAqB,EACrB,iBAAiB,EACjB,cAAc,EACd,kBAAkB,EAClB,aAAa,EACb,aAAa,EACb,UAAU,GACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC1E,YAAY,EACR,8BAA8B,EAC9B,gBAAgB,EAChB,qBAAqB,EACrB,wBAAwB,EACxB,sBAAsB,EACtB,sBAAsB,EACtB,sBAAsB,EACtB,eAAe,EACf,oBAAoB,GACvB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,6BAA6B,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAC5F,OAAO,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AACxD,YAAY,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAC1E,YAAY,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,cAAc,qBAAqB,CAAC;AACpC,cAAc,SAAS,CAAC;AAIxB,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAEnF;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GAAG,YAAY,CAAC,cAAc,oBAAoB,EAAE,sBAAsB,CAAC,CAAC;AAE3G;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,YAAY,CAAC,cAAc,oBAAoB,EAAE,qBAAqB,CAAC,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,+BAA+B,EAAE,MAAM,WAAW,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAC9E,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC3E,OAAO,EACH,eAAe,EACf,eAAe,EACf,eAAe,EACf,gBAAgB,EAChB,YAAY,EACZ,OAAO,EACP,aAAa,GAChB,MAAM,MAAM,CAAC;AACd,cAAc,QAAQ,CAAC;AACvB,OAAO,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACjG,YAAY,EACR,YAAY,EACZ,WAAW,EACX,kBAAkB,EAClB,kBAAkB,EAClB,gBAAgB,EAChB,aAAa,EACb,qBAAqB,EACrB,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,aAAa,EACb,aAAa,EACb,UAAU,GACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC1E,YAAY,EACR,8BAA8B,EAC9B,gBAAgB,EAChB,qBAAqB,EACrB,wBAAwB,EACxB,sBAAsB,EACtB,sBAAsB,EACtB,sBAAsB,EACtB,eAAe,EACf,oBAAoB,GACvB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,6BAA6B,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAC5F,OAAO,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AACxD,YAAY,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAC1E,YAAY,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,cAAc,qBAAqB,CAAC;AACpC,cAAc,SAAS,CAAC;AAIxB,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAEnF;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GAAG,YAAY,CAAC,cAAc,oBAAoB,EAAE,sBAAsB,CAAC,CAAC;AAE3G;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,YAAY,CAAC,cAAc,oBAAoB,EAAE,qBAAqB,CAAC,CAAC"}
|
|
@@ -24,7 +24,11 @@ export type OutputPolicy = {
|
|
|
24
24
|
};
|
|
25
25
|
/** Shared configuration for a process executor (default timeout, output buffering, output policy). */
|
|
26
26
|
export interface ProcessExecutorConfig {
|
|
27
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Default execution deadline inherited by runs that omit `timeout`. `null` makes the
|
|
29
|
+
* default policy unlimited; value validation follows `ProcessOptions.timeout`.
|
|
30
|
+
*/
|
|
31
|
+
defaultTimeout?: number | null;
|
|
28
32
|
defaultMaxOutput?: number;
|
|
29
33
|
output?: OutputPolicy;
|
|
30
34
|
events?: ProcessEventSink;
|
|
@@ -55,7 +59,23 @@ export interface ProcessOptions {
|
|
|
55
59
|
* `run` and `runStreaming` share one contract.
|
|
56
60
|
*/
|
|
57
61
|
envMode?: 'merge' | 'replace';
|
|
58
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Execution deadline in milliseconds. Omitted inherits
|
|
64
|
+
* {@link ProcessExecutorConfig.defaultTimeout}; explicit `null` runs without a deadline
|
|
65
|
+
* (a supplied `signal` still cancels). On Unix, a finite deadline activates owned
|
|
66
|
+
* process-group containment: the executor arms its own timer — callers must not race a
|
|
67
|
+
* second watchdog — and expiry reaps the whole group with SIGTERM-to-SIGKILL escalation.
|
|
68
|
+
* Zero, negative, fractional, non-finite, or beyond-timer-range values are rejected
|
|
69
|
+
* before spawn; `null` replaces the previous `0`-disables behavior.
|
|
70
|
+
*/
|
|
71
|
+
timeout?: number | null;
|
|
72
|
+
/**
|
|
73
|
+
* Grace in milliseconds between the owned group SIGTERM and the SIGKILL escalation
|
|
74
|
+
* (Unix group containment; see {@link ProcessOptions.timeout}). `0` escalates
|
|
75
|
+
* immediately; omitted defaults to 5000 ms. Out-of-range values are rejected before
|
|
76
|
+
* spawn. Without a finite deadline or `signal`, this option has no effect.
|
|
77
|
+
*/
|
|
78
|
+
killGraceMs?: number;
|
|
59
79
|
maxOutput?: number;
|
|
60
80
|
label?: string;
|
|
61
81
|
rejectOnError?: boolean;
|
|
@@ -98,6 +118,12 @@ export interface ProcessOutputChunk {
|
|
|
98
118
|
readonly chunk: string;
|
|
99
119
|
readonly timestamp: string;
|
|
100
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Terminal classification of how a run ended: normal `exit`, deadline `timeout`, external
|
|
123
|
+
* `cancelled` abort, termination by an outside `signal`, or `error` (spawn/runtime failure).
|
|
124
|
+
* Reaping surviving owned descendants after a natural leader exit keeps the outcome `exit`.
|
|
125
|
+
*/
|
|
126
|
+
export type ProcessOutcome = 'exit' | 'timeout' | 'cancelled' | 'signal' | 'error';
|
|
101
127
|
/** Result of a completed child process, including exit code, captured output, and duration. */
|
|
102
128
|
export interface ProcessResult {
|
|
103
129
|
command: string;
|
|
@@ -107,9 +133,15 @@ export interface ProcessResult {
|
|
|
107
133
|
stderr: string;
|
|
108
134
|
signal?: string;
|
|
109
135
|
durationMs: number;
|
|
136
|
+
/**
|
|
137
|
+
* How the run terminated, distinguishing deadline expiry from external cancellation,
|
|
138
|
+
* an outside signal, runtime failure, and normal completion. Optional for backward
|
|
139
|
+
* compatibility with hand-built results; `NodeProcessExecutor` always sets it.
|
|
140
|
+
*/
|
|
141
|
+
outcome?: ProcessOutcome;
|
|
110
142
|
}
|
|
111
|
-
/** Reason a process completion event was emitted. */
|
|
112
|
-
export type ProcessExitReason =
|
|
143
|
+
/** Reason a process completion event was emitted (mirrors {@link ProcessOutcome}). */
|
|
144
|
+
export type ProcessExitReason = ProcessOutcome;
|
|
113
145
|
/** Payload emitted for process execution observability. */
|
|
114
146
|
export interface ProcessEventDetail {
|
|
115
147
|
command: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"process-executor.d.ts","sourceRoot":"","sources":["../src/process-executor.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,sBAAsB,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAClF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAIpD;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,YAAY,GAAG;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzG,sGAAsG;AACtG,MAAM,WAAW,qBAAqB;IAClC,cAAc,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"process-executor.d.ts","sourceRoot":"","sources":["../src/process-executor.ts"],"names":[],"mappings":"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"}
|
package/dist/process-executor.js
CHANGED
|
@@ -4,7 +4,7 @@ import { getProcessEnv } from './config.js';
|
|
|
4
4
|
function processEventSeverity(reason, exitCode) {
|
|
5
5
|
if (reason === 'error')
|
|
6
6
|
return 'error';
|
|
7
|
-
if (reason === 'timeout')
|
|
7
|
+
if (reason === 'timeout' || reason === 'cancelled')
|
|
8
8
|
return 'warning';
|
|
9
9
|
if (exitCode !== null && exitCode !== 0)
|
|
10
10
|
return 'warning';
|
|
@@ -48,15 +48,25 @@ export class NodeProcessExecutor {
|
|
|
48
48
|
}
|
|
49
49
|
async runUntraced(options) {
|
|
50
50
|
const args = options.args ?? [];
|
|
51
|
+
// Validate before spawn and before any registry/event side effects: an invalid
|
|
52
|
+
// deadline or grace is caller configuration error, not a process outcome.
|
|
53
|
+
const deadline = resolveDeadline(options.timeout, this.config.defaultTimeout);
|
|
54
|
+
const killGraceMs = resolveKillGraceMs(options.killGraceMs);
|
|
55
|
+
// Unix group containment activates on a finite deadline or a caller abort signal:
|
|
56
|
+
// the executor then owns group isolation, SIGTERM, escalation, output settlement
|
|
57
|
+
// and the final outcome, so callers must not arm a competing watchdog. Other
|
|
58
|
+
// platforms keep execa's direct-child timeout/cancellation semantics explicit.
|
|
59
|
+
const groupOwned = process.platform !== 'win32' && (typeof deadline === 'number' || options.signal !== undefined);
|
|
51
60
|
const execaOptions = buildExecaOptions({
|
|
52
61
|
cwd: options.cwd ?? this.config.paths?.cwd,
|
|
53
62
|
env: options.env,
|
|
54
|
-
timeout:
|
|
63
|
+
timeout: typeof deadline === 'number' ? deadline : undefined,
|
|
55
64
|
maxOutput: options.maxOutput ?? this.config.defaultMaxOutput,
|
|
56
65
|
rejectOnError: options.rejectOnError ?? false,
|
|
57
66
|
outputPolicy: this.config.output,
|
|
58
67
|
forceBuffered: options.forceBuffered ?? false,
|
|
59
68
|
signal: options.signal,
|
|
69
|
+
groupOwned,
|
|
60
70
|
});
|
|
61
71
|
const startedAt = Date.now();
|
|
62
72
|
const startedIso = new Date(startedAt).toISOString();
|
|
@@ -77,8 +87,17 @@ export class NodeProcessExecutor {
|
|
|
77
87
|
severity: 'info',
|
|
78
88
|
...(options.label !== undefined ? { label: options.label } : {}),
|
|
79
89
|
});
|
|
90
|
+
let ownership;
|
|
80
91
|
try {
|
|
81
92
|
const subprocess = execa(options.command, args, execaOptions);
|
|
93
|
+
if (groupOwned && subprocess.pid !== undefined) {
|
|
94
|
+
ownership = ownProcessGroupLifecycle({
|
|
95
|
+
pid: subprocess.pid,
|
|
96
|
+
deadlineMs: typeof deadline === 'number' ? deadline : undefined,
|
|
97
|
+
signal: options.signal,
|
|
98
|
+
graceMs: killGraceMs,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
82
101
|
// execa@9's resolved Result carries no pid, but the live handle does.
|
|
83
102
|
// Publish it while the child is running so observers can identify the
|
|
84
103
|
// subprocess, and record it against the registry entry opened above.
|
|
@@ -93,10 +112,14 @@ export class NodeProcessExecutor {
|
|
|
93
112
|
}
|
|
94
113
|
}
|
|
95
114
|
}
|
|
96
|
-
const
|
|
115
|
+
const stopOwnedTermination = () => ownership?.stop();
|
|
97
116
|
observeOutput(subprocess.stdout, 'stdout', options.onOutput);
|
|
98
117
|
observeOutput(subprocess.stderr, 'stderr', options.onOutput);
|
|
99
|
-
const result = await subprocess.finally(
|
|
118
|
+
const result = await subprocess.finally(stopOwnedTermination);
|
|
119
|
+
// Settle the owned termination path before reporting: a deadline or abort that
|
|
120
|
+
// fired keeps escalating until the group is empty, and a natural leader exit
|
|
121
|
+
// reaps surviving owned descendants so completion cannot leak pipes or locks.
|
|
122
|
+
const ownedOutcome = ownership !== undefined ? await ownership.finish() : undefined;
|
|
100
123
|
const processResult = {
|
|
101
124
|
command: options.command,
|
|
102
125
|
args,
|
|
@@ -105,6 +128,7 @@ export class NodeProcessExecutor {
|
|
|
105
128
|
stderr: asString(result.stderr),
|
|
106
129
|
...(result.signalDescription !== undefined ? { signal: result.signalDescription } : {}),
|
|
107
130
|
durationMs: result.durationMs,
|
|
131
|
+
outcome: ownedOutcome ?? (result.signalDescription !== undefined ? 'signal' : 'exit'),
|
|
108
132
|
};
|
|
109
133
|
this.completeRegistry(registryId, processResult.exitCode);
|
|
110
134
|
this.emitExitedFromResult(options, processResult, result);
|
|
@@ -112,6 +136,9 @@ export class NodeProcessExecutor {
|
|
|
112
136
|
}
|
|
113
137
|
catch (error) {
|
|
114
138
|
const failed = error;
|
|
139
|
+
// Termination containment settles before the failure is reported — and before
|
|
140
|
+
// rejectOnError rethrows — so a rejected run never leaks owned descendants.
|
|
141
|
+
const ownedOutcome = ownership !== undefined ? await ownership.finish() : undefined;
|
|
115
142
|
const processResult = {
|
|
116
143
|
command: options.command,
|
|
117
144
|
args,
|
|
@@ -124,6 +151,7 @@ export class NodeProcessExecutor {
|
|
|
124
151
|
? { signal: failed.signal }
|
|
125
152
|
: {}),
|
|
126
153
|
durationMs: failed.durationMs ?? Date.now() - startedAt,
|
|
154
|
+
outcome: ownedOutcome ?? classifyFailedCompletion(failed),
|
|
127
155
|
};
|
|
128
156
|
this.completeRegistry(registryId, processResult.exitCode);
|
|
129
157
|
this.emitExitedFromResult(options, processResult, error, error);
|
|
@@ -227,13 +255,8 @@ export class NodeProcessExecutor {
|
|
|
227
255
|
});
|
|
228
256
|
}
|
|
229
257
|
emitExitedFromResult(options, result, completion, error) {
|
|
230
|
-
const reason =
|
|
231
|
-
? 'timeout'
|
|
232
|
-
: result.signal !== undefined
|
|
233
|
-
? 'signal'
|
|
234
|
-
: error
|
|
235
|
-
? 'error'
|
|
236
|
-
: 'exit';
|
|
258
|
+
const reason = result.outcome ??
|
|
259
|
+
(isTimedOut(completion) ? 'timeout' : result.signal !== undefined ? 'signal' : error ? 'error' : 'exit');
|
|
237
260
|
this.emitProcessEvent('process.exited', {
|
|
238
261
|
command: result.command,
|
|
239
262
|
args: result.args,
|
|
@@ -401,37 +424,158 @@ function buildExecaOptions(opts) {
|
|
|
401
424
|
: { all: true }),
|
|
402
425
|
...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),
|
|
403
426
|
...(opts.env !== undefined ? { env: opts.env } : {}),
|
|
404
|
-
...(opts.timeout !== undefined ? { timeout: opts.timeout } : {}),
|
|
427
|
+
...(opts.timeout !== undefined && !opts.groupOwned ? { timeout: opts.timeout } : {}),
|
|
405
428
|
...(opts.maxOutput !== undefined ? { maxBuffer: opts.maxOutput } : {}),
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
}
|
|
413
|
-
: {}),
|
|
429
|
+
// Group-owned runs make the child lead a distinct process group so negative-pid
|
|
430
|
+
// signaling can reach the whole tree; termination itself runs through the
|
|
431
|
+
// executor's own escalation path (deadline and abort feed one sequence).
|
|
432
|
+
// Platforms without Unix group semantics cancel the direct child via execa.
|
|
433
|
+
...(opts.groupOwned ? { detached: true } : {}),
|
|
434
|
+
...(opts.signal !== undefined && !opts.groupOwned ? { cancelSignal: opts.signal } : {}),
|
|
414
435
|
};
|
|
415
436
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
437
|
+
// ── Deadline and process-group containment ───────────────────────────
|
|
438
|
+
/**
|
|
439
|
+
* Largest delay accepted by native timers; anything larger would overflow onto an
|
|
440
|
+
* immediate timer, so deadline/grace values beyond it are rejected before spawn.
|
|
441
|
+
*/
|
|
442
|
+
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
443
|
+
/** Default grace between the owned group SIGTERM and the SIGKILL escalation. */
|
|
444
|
+
const DEFAULT_KILL_GRACE_MS = 5000;
|
|
445
|
+
/** Poll cadence while waiting for an owned process group to empty. */
|
|
446
|
+
const GROUP_POLL_INTERVAL_MS = 25;
|
|
447
|
+
/** Bounded patience after group SIGKILL before giving up on unkillable (D-state) members. */
|
|
448
|
+
const GROUP_KILL_PATIENCE_MS = 2000;
|
|
449
|
+
/**
|
|
450
|
+
* Resolve the effective deadline: omitted inherits the executor default (which may itself
|
|
451
|
+
* be unlimited), explicit `null` is unlimited and overrides any finite default, and a
|
|
452
|
+
* finite value must be a positive integer within the native timer range. Invalid values
|
|
453
|
+
* are rejected before spawn — never clamped and never overflowed onto a timer.
|
|
454
|
+
*/
|
|
455
|
+
function resolveDeadline(optionTimeout, defaultTimeout) {
|
|
456
|
+
if (optionTimeout === null)
|
|
457
|
+
return null;
|
|
458
|
+
const candidate = optionTimeout === undefined ? defaultTimeout : optionTimeout;
|
|
459
|
+
if (candidate === undefined || candidate === null)
|
|
460
|
+
return candidate;
|
|
461
|
+
if (!Number.isInteger(candidate) || candidate <= 0 || candidate > MAX_TIMEOUT_MS) {
|
|
462
|
+
throw new TypeError(`Process timeout must be a positive integer within the native timer range (1-${MAX_TIMEOUT_MS} ms), got ${String(candidate)}`);
|
|
463
|
+
}
|
|
464
|
+
return candidate;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Resolve the SIGTERM→SIGKILL escalation grace: omitted uses {@link DEFAULT_KILL_GRACE_MS},
|
|
468
|
+
* `0` escalates immediately, and anything outside the native timer range is rejected.
|
|
469
|
+
*/
|
|
470
|
+
function resolveKillGraceMs(optionGraceMs) {
|
|
471
|
+
if (optionGraceMs === undefined)
|
|
472
|
+
return DEFAULT_KILL_GRACE_MS;
|
|
473
|
+
if (!Number.isInteger(optionGraceMs) || optionGraceMs < 0 || optionGraceMs > MAX_TIMEOUT_MS) {
|
|
474
|
+
throw new TypeError(`Process killGraceMs must be a non-negative integer within the native timer range (0-${MAX_TIMEOUT_MS} ms), got ${String(optionGraceMs)}`);
|
|
475
|
+
}
|
|
476
|
+
return optionGraceMs;
|
|
477
|
+
}
|
|
478
|
+
/** Whether any member of the owned process group is still alive (signal 0 probe on `-pid`). */
|
|
479
|
+
function groupExists(pid) {
|
|
480
|
+
try {
|
|
481
|
+
process.kill(-pid, 0);
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
catch (error) {
|
|
485
|
+
// EPERM means the group exists but is not signallable from here.
|
|
486
|
+
return error.code === 'EPERM';
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
/** Signal the owned process group; `false` when the group is already gone (ESRCH). */
|
|
490
|
+
function signalGroup(pid, signal) {
|
|
491
|
+
try {
|
|
492
|
+
process.kill(-pid, signal);
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
async function waitForGroupExit(pid, budgetMs) {
|
|
500
|
+
const giveUpAt = Date.now() + budgetMs;
|
|
501
|
+
while (groupExists(pid)) {
|
|
502
|
+
if (Date.now() >= giveUpAt)
|
|
503
|
+
return false;
|
|
504
|
+
await new Promise((resolve) => setTimeout(resolve, GROUP_POLL_INTERVAL_MS));
|
|
505
|
+
}
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* The one termination sequence for an owned Unix process group: group SIGTERM, wait the
|
|
510
|
+
* grace for the whole group to empty, then group SIGKILL and a bounded settle wait.
|
|
511
|
+
* Group liveness — not direct-child exit — decides completion, so escalation is never
|
|
512
|
+
* cancelled because the leader exited first while descendants retain pipes or locks.
|
|
513
|
+
* Resolves `true` only when a group signal was actually delivered.
|
|
514
|
+
*/
|
|
515
|
+
async function reapProcessGroup(pid, graceMs) {
|
|
516
|
+
if (!signalGroup(pid, 'SIGTERM'))
|
|
517
|
+
return false;
|
|
518
|
+
if (await waitForGroupExit(pid, graceMs))
|
|
519
|
+
return true;
|
|
520
|
+
signalGroup(pid, 'SIGKILL');
|
|
521
|
+
await waitForGroupExit(pid, GROUP_KILL_PATIENCE_MS);
|
|
522
|
+
return true;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Own one detached child's process group on Unix. A deadline timer and the caller's abort
|
|
526
|
+
* signal both feed a single termination path — group SIGTERM, grace, SIGKILL — and after a
|
|
527
|
+
* natural leader exit surviving owned descendants are reaped with the same sequence, so
|
|
528
|
+
* completion cannot leak group members holding inherited pipes or database write locks.
|
|
529
|
+
*/
|
|
530
|
+
function ownProcessGroupLifecycle(ownership) {
|
|
531
|
+
const { pid, graceMs } = ownership;
|
|
532
|
+
let trigger;
|
|
533
|
+
let termination;
|
|
534
|
+
let timer;
|
|
535
|
+
const initiate = (reason) => {
|
|
536
|
+
if (trigger !== undefined)
|
|
537
|
+
return;
|
|
538
|
+
trigger = reason;
|
|
539
|
+
termination = reapProcessGroup(pid, graceMs);
|
|
429
540
|
};
|
|
430
|
-
if (
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
541
|
+
if (ownership.deadlineMs !== undefined) {
|
|
542
|
+
timer = setTimeout(() => initiate('timeout'), ownership.deadlineMs);
|
|
543
|
+
}
|
|
544
|
+
const onAbort = () => initiate('cancelled');
|
|
545
|
+
if (ownership.signal !== undefined) {
|
|
546
|
+
if (ownership.signal.aborted)
|
|
547
|
+
onAbort();
|
|
548
|
+
else
|
|
549
|
+
ownership.signal.addEventListener('abort', onAbort, { once: true });
|
|
550
|
+
}
|
|
551
|
+
return {
|
|
552
|
+
finish: async () => {
|
|
553
|
+
if (trigger === undefined) {
|
|
554
|
+
// Natural completion: reap stragglers (no-op when the group is already empty).
|
|
555
|
+
await reapProcessGroup(pid, graceMs);
|
|
556
|
+
return undefined;
|
|
557
|
+
}
|
|
558
|
+
// An undeliverable trigger (group already gone — the child finished first)
|
|
559
|
+
// yields undefined so the outcome falls back to the natural classification.
|
|
560
|
+
return (await termination) ? trigger : undefined;
|
|
561
|
+
},
|
|
562
|
+
stop: () => {
|
|
563
|
+
if (timer !== undefined)
|
|
564
|
+
clearTimeout(timer);
|
|
565
|
+
ownership.signal?.removeEventListener('abort', onAbort);
|
|
566
|
+
},
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
/** Classify an execa rejection on paths where the executor did not own the termination. */
|
|
570
|
+
function classifyFailedCompletion(error) {
|
|
571
|
+
if (error.timedOut === true)
|
|
572
|
+
return 'timeout';
|
|
573
|
+
if (error.isCanceled === true)
|
|
574
|
+
return 'cancelled';
|
|
575
|
+
if (error.isTerminated === true || error.signalDescription !== undefined || error.signal !== undefined) {
|
|
576
|
+
return 'signal';
|
|
577
|
+
}
|
|
578
|
+
return 'error';
|
|
435
579
|
}
|
|
436
580
|
function observeOutput(stream, name, observer) {
|
|
437
581
|
if (!stream || !observer)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/ts-runtime",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.59",
|
|
4
4
|
"description": "@gobing-ai/ts-runtime — Runtime abstractions for Bun, Node, and Cloudflare Workers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -58,17 +58,17 @@
|
|
|
58
58
|
"release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-runtime-v<version> && git push --tags' && exit 1"
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@gobing-ai/ts-utils": "^0.4.
|
|
61
|
+
"@gobing-ai/ts-utils": "^0.4.59",
|
|
62
62
|
"execa": "^9.5.0",
|
|
63
63
|
"yaml": "^2.7.0",
|
|
64
64
|
"zod": "^4.1.0"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
|
-
"@gobing-ai/ts-db": "^0.4.
|
|
67
|
+
"@gobing-ai/ts-db": "^0.4.59",
|
|
68
68
|
"@types/bun": "1.3.14"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
|
-
"@gobing-ai/ts-db": "^0.4.
|
|
71
|
+
"@gobing-ai/ts-db": "^0.4.59"
|
|
72
72
|
},
|
|
73
73
|
"peerDependenciesMeta": {
|
|
74
74
|
"@gobing-ai/ts-db": {
|
package/src/index.ts
CHANGED
package/src/process-executor.ts
CHANGED
|
@@ -24,7 +24,11 @@ export type OutputPolicy = { mode: 'buffered' } | { mode: 'stream'; isTTY?: bool
|
|
|
24
24
|
|
|
25
25
|
/** Shared configuration for a process executor (default timeout, output buffering, output policy). */
|
|
26
26
|
export interface ProcessExecutorConfig {
|
|
27
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Default execution deadline inherited by runs that omit `timeout`. `null` makes the
|
|
29
|
+
* default policy unlimited; value validation follows `ProcessOptions.timeout`.
|
|
30
|
+
*/
|
|
31
|
+
defaultTimeout?: number | null;
|
|
28
32
|
defaultMaxOutput?: number;
|
|
29
33
|
output?: OutputPolicy;
|
|
30
34
|
events?: ProcessEventSink;
|
|
@@ -56,7 +60,23 @@ export interface ProcessOptions {
|
|
|
56
60
|
* `run` and `runStreaming` share one contract.
|
|
57
61
|
*/
|
|
58
62
|
envMode?: 'merge' | 'replace';
|
|
59
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Execution deadline in milliseconds. Omitted inherits
|
|
65
|
+
* {@link ProcessExecutorConfig.defaultTimeout}; explicit `null` runs without a deadline
|
|
66
|
+
* (a supplied `signal` still cancels). On Unix, a finite deadline activates owned
|
|
67
|
+
* process-group containment: the executor arms its own timer — callers must not race a
|
|
68
|
+
* second watchdog — and expiry reaps the whole group with SIGTERM-to-SIGKILL escalation.
|
|
69
|
+
* Zero, negative, fractional, non-finite, or beyond-timer-range values are rejected
|
|
70
|
+
* before spawn; `null` replaces the previous `0`-disables behavior.
|
|
71
|
+
*/
|
|
72
|
+
timeout?: number | null;
|
|
73
|
+
/**
|
|
74
|
+
* Grace in milliseconds between the owned group SIGTERM and the SIGKILL escalation
|
|
75
|
+
* (Unix group containment; see {@link ProcessOptions.timeout}). `0` escalates
|
|
76
|
+
* immediately; omitted defaults to 5000 ms. Out-of-range values are rejected before
|
|
77
|
+
* spawn. Without a finite deadline or `signal`, this option has no effect.
|
|
78
|
+
*/
|
|
79
|
+
killGraceMs?: number;
|
|
60
80
|
maxOutput?: number;
|
|
61
81
|
label?: string;
|
|
62
82
|
rejectOnError?: boolean;
|
|
@@ -101,6 +121,13 @@ export interface ProcessOutputChunk {
|
|
|
101
121
|
readonly timestamp: string;
|
|
102
122
|
}
|
|
103
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Terminal classification of how a run ended: normal `exit`, deadline `timeout`, external
|
|
126
|
+
* `cancelled` abort, termination by an outside `signal`, or `error` (spawn/runtime failure).
|
|
127
|
+
* Reaping surviving owned descendants after a natural leader exit keeps the outcome `exit`.
|
|
128
|
+
*/
|
|
129
|
+
export type ProcessOutcome = 'exit' | 'timeout' | 'cancelled' | 'signal' | 'error';
|
|
130
|
+
|
|
104
131
|
/** Result of a completed child process, including exit code, captured output, and duration. */
|
|
105
132
|
export interface ProcessResult {
|
|
106
133
|
command: string;
|
|
@@ -110,14 +137,20 @@ export interface ProcessResult {
|
|
|
110
137
|
stderr: string;
|
|
111
138
|
signal?: string;
|
|
112
139
|
durationMs: number;
|
|
140
|
+
/**
|
|
141
|
+
* How the run terminated, distinguishing deadline expiry from external cancellation,
|
|
142
|
+
* an outside signal, runtime failure, and normal completion. Optional for backward
|
|
143
|
+
* compatibility with hand-built results; `NodeProcessExecutor` always sets it.
|
|
144
|
+
*/
|
|
145
|
+
outcome?: ProcessOutcome;
|
|
113
146
|
}
|
|
114
147
|
|
|
115
|
-
/** Reason a process completion event was emitted. */
|
|
116
|
-
export type ProcessExitReason =
|
|
148
|
+
/** Reason a process completion event was emitted (mirrors {@link ProcessOutcome}). */
|
|
149
|
+
export type ProcessExitReason = ProcessOutcome;
|
|
117
150
|
|
|
118
151
|
function processEventSeverity(reason: ProcessExitReason, exitCode: number | null): ProcessEventDetail['severity'] {
|
|
119
152
|
if (reason === 'error') return 'error';
|
|
120
|
-
if (reason === 'timeout') return 'warning';
|
|
153
|
+
if (reason === 'timeout' || reason === 'cancelled') return 'warning';
|
|
121
154
|
if (exitCode !== null && exitCode !== 0) return 'warning';
|
|
122
155
|
return 'info';
|
|
123
156
|
}
|
|
@@ -263,15 +296,26 @@ export class NodeProcessExecutor implements ProcessExecutor {
|
|
|
263
296
|
|
|
264
297
|
private async runUntraced(options: ProcessOptions): Promise<ProcessResult> {
|
|
265
298
|
const args = options.args ?? [];
|
|
299
|
+
// Validate before spawn and before any registry/event side effects: an invalid
|
|
300
|
+
// deadline or grace is caller configuration error, not a process outcome.
|
|
301
|
+
const deadline = resolveDeadline(options.timeout, this.config.defaultTimeout);
|
|
302
|
+
const killGraceMs = resolveKillGraceMs(options.killGraceMs);
|
|
303
|
+
// Unix group containment activates on a finite deadline or a caller abort signal:
|
|
304
|
+
// the executor then owns group isolation, SIGTERM, escalation, output settlement
|
|
305
|
+
// and the final outcome, so callers must not arm a competing watchdog. Other
|
|
306
|
+
// platforms keep execa's direct-child timeout/cancellation semantics explicit.
|
|
307
|
+
const groupOwned =
|
|
308
|
+
process.platform !== 'win32' && (typeof deadline === 'number' || options.signal !== undefined);
|
|
266
309
|
const execaOptions = buildExecaOptions({
|
|
267
310
|
cwd: options.cwd ?? this.config.paths?.cwd,
|
|
268
311
|
env: options.env,
|
|
269
|
-
timeout:
|
|
312
|
+
timeout: typeof deadline === 'number' ? deadline : undefined,
|
|
270
313
|
maxOutput: options.maxOutput ?? this.config.defaultMaxOutput,
|
|
271
314
|
rejectOnError: options.rejectOnError ?? false,
|
|
272
315
|
outputPolicy: this.config.output,
|
|
273
316
|
forceBuffered: options.forceBuffered ?? false,
|
|
274
317
|
signal: options.signal,
|
|
318
|
+
groupOwned,
|
|
275
319
|
});
|
|
276
320
|
const startedAt = Date.now();
|
|
277
321
|
const startedIso = new Date(startedAt).toISOString();
|
|
@@ -293,8 +337,17 @@ export class NodeProcessExecutor implements ProcessExecutor {
|
|
|
293
337
|
...(options.label !== undefined ? { label: options.label } : {}),
|
|
294
338
|
});
|
|
295
339
|
|
|
340
|
+
let ownership: ProcessGroupOwnership | undefined;
|
|
296
341
|
try {
|
|
297
342
|
const subprocess = execa(options.command, args, execaOptions);
|
|
343
|
+
if (groupOwned && subprocess.pid !== undefined) {
|
|
344
|
+
ownership = ownProcessGroupLifecycle({
|
|
345
|
+
pid: subprocess.pid,
|
|
346
|
+
deadlineMs: typeof deadline === 'number' ? deadline : undefined,
|
|
347
|
+
signal: options.signal,
|
|
348
|
+
graceMs: killGraceMs,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
298
351
|
// execa@9's resolved Result carries no pid, but the live handle does.
|
|
299
352
|
// Publish it while the child is running so observers can identify the
|
|
300
353
|
// subprocess, and record it against the registry entry opened above.
|
|
@@ -308,10 +361,14 @@ export class NodeProcessExecutor implements ProcessExecutor {
|
|
|
308
361
|
}
|
|
309
362
|
}
|
|
310
363
|
}
|
|
311
|
-
const
|
|
364
|
+
const stopOwnedTermination = (): void => ownership?.stop();
|
|
312
365
|
observeOutput(subprocess.stdout, 'stdout', options.onOutput);
|
|
313
366
|
observeOutput(subprocess.stderr, 'stderr', options.onOutput);
|
|
314
|
-
const result = await subprocess.finally(
|
|
367
|
+
const result = await subprocess.finally(stopOwnedTermination);
|
|
368
|
+
// Settle the owned termination path before reporting: a deadline or abort that
|
|
369
|
+
// fired keeps escalating until the group is empty, and a natural leader exit
|
|
370
|
+
// reaps surviving owned descendants so completion cannot leak pipes or locks.
|
|
371
|
+
const ownedOutcome = ownership !== undefined ? await ownership.finish() : undefined;
|
|
315
372
|
const processResult = {
|
|
316
373
|
command: options.command,
|
|
317
374
|
args,
|
|
@@ -320,6 +377,8 @@ export class NodeProcessExecutor implements ProcessExecutor {
|
|
|
320
377
|
stderr: asString(result.stderr),
|
|
321
378
|
...(result.signalDescription !== undefined ? { signal: result.signalDescription } : {}),
|
|
322
379
|
durationMs: result.durationMs,
|
|
380
|
+
outcome:
|
|
381
|
+
ownedOutcome ?? (result.signalDescription !== undefined ? ('signal' as const) : ('exit' as const)),
|
|
323
382
|
};
|
|
324
383
|
this.completeRegistry(registryId, processResult.exitCode);
|
|
325
384
|
this.emitExitedFromResult(options, processResult, result);
|
|
@@ -333,8 +392,13 @@ export class NodeProcessExecutor implements ProcessExecutor {
|
|
|
333
392
|
signal?: string;
|
|
334
393
|
durationMs?: number;
|
|
335
394
|
timedOut?: boolean;
|
|
395
|
+
isCanceled?: boolean;
|
|
396
|
+
isTerminated?: boolean;
|
|
336
397
|
message?: string;
|
|
337
398
|
};
|
|
399
|
+
// Termination containment settles before the failure is reported — and before
|
|
400
|
+
// rejectOnError rethrows — so a rejected run never leaks owned descendants.
|
|
401
|
+
const ownedOutcome = ownership !== undefined ? await ownership.finish() : undefined;
|
|
338
402
|
const processResult = {
|
|
339
403
|
command: options.command,
|
|
340
404
|
args,
|
|
@@ -347,6 +411,7 @@ export class NodeProcessExecutor implements ProcessExecutor {
|
|
|
347
411
|
? { signal: failed.signal }
|
|
348
412
|
: {}),
|
|
349
413
|
durationMs: failed.durationMs ?? Date.now() - startedAt,
|
|
414
|
+
outcome: ownedOutcome ?? classifyFailedCompletion(failed),
|
|
350
415
|
};
|
|
351
416
|
this.completeRegistry(registryId, processResult.exitCode);
|
|
352
417
|
this.emitExitedFromResult(options, processResult, error, error);
|
|
@@ -465,13 +530,9 @@ export class NodeProcessExecutor implements ProcessExecutor {
|
|
|
465
530
|
completion: unknown,
|
|
466
531
|
error?: unknown,
|
|
467
532
|
): void {
|
|
468
|
-
const reason =
|
|
469
|
-
|
|
470
|
-
: result.signal !== undefined
|
|
471
|
-
? 'signal'
|
|
472
|
-
: error
|
|
473
|
-
? 'error'
|
|
474
|
-
: 'exit';
|
|
533
|
+
const reason =
|
|
534
|
+
result.outcome ??
|
|
535
|
+
(isTimedOut(completion) ? 'timeout' : result.signal !== undefined ? 'signal' : error ? 'error' : 'exit');
|
|
475
536
|
this.emitProcessEvent('process.exited', {
|
|
476
537
|
command: result.command,
|
|
477
538
|
args: result.args,
|
|
@@ -678,6 +739,7 @@ function buildExecaOptions(opts: {
|
|
|
678
739
|
outputPolicy: OutputPolicy | undefined;
|
|
679
740
|
forceBuffered: boolean;
|
|
680
741
|
signal?: AbortSignal;
|
|
742
|
+
groupOwned: boolean;
|
|
681
743
|
}): ExecaOptions {
|
|
682
744
|
const mode = opts.forceBuffered ? 'buffered' : (opts.outputPolicy?.mode ?? 'buffered');
|
|
683
745
|
const streamToTerminal =
|
|
@@ -695,37 +757,191 @@ function buildExecaOptions(opts: {
|
|
|
695
757
|
: { all: true }),
|
|
696
758
|
...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),
|
|
697
759
|
...(opts.env !== undefined ? { env: opts.env } : {}),
|
|
698
|
-
...(opts.timeout !== undefined ? { timeout: opts.timeout } : {}),
|
|
760
|
+
...(opts.timeout !== undefined && !opts.groupOwned ? { timeout: opts.timeout } : {}),
|
|
699
761
|
...(opts.maxOutput !== undefined ? { maxBuffer: opts.maxOutput } : {}),
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
}
|
|
707
|
-
: {}),
|
|
762
|
+
// Group-owned runs make the child lead a distinct process group so negative-pid
|
|
763
|
+
// signaling can reach the whole tree; termination itself runs through the
|
|
764
|
+
// executor's own escalation path (deadline and abort feed one sequence).
|
|
765
|
+
// Platforms without Unix group semantics cancel the direct child via execa.
|
|
766
|
+
...(opts.groupOwned ? { detached: true } : {}),
|
|
767
|
+
...(opts.signal !== undefined && !opts.groupOwned ? { cancelSignal: opts.signal } : {}),
|
|
708
768
|
};
|
|
709
769
|
}
|
|
710
770
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
771
|
+
// ── Deadline and process-group containment ───────────────────────────
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Largest delay accepted by native timers; anything larger would overflow onto an
|
|
775
|
+
* immediate timer, so deadline/grace values beyond it are rejected before spawn.
|
|
776
|
+
*/
|
|
777
|
+
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
778
|
+
|
|
779
|
+
/** Default grace between the owned group SIGTERM and the SIGKILL escalation. */
|
|
780
|
+
const DEFAULT_KILL_GRACE_MS = 5000;
|
|
781
|
+
|
|
782
|
+
/** Poll cadence while waiting for an owned process group to empty. */
|
|
783
|
+
const GROUP_POLL_INTERVAL_MS = 25;
|
|
784
|
+
|
|
785
|
+
/** Bounded patience after group SIGKILL before giving up on unkillable (D-state) members. */
|
|
786
|
+
const GROUP_KILL_PATIENCE_MS = 2000;
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Resolve the effective deadline: omitted inherits the executor default (which may itself
|
|
790
|
+
* be unlimited), explicit `null` is unlimited and overrides any finite default, and a
|
|
791
|
+
* finite value must be a positive integer within the native timer range. Invalid values
|
|
792
|
+
* are rejected before spawn — never clamped and never overflowed onto a timer.
|
|
793
|
+
*/
|
|
794
|
+
function resolveDeadline(
|
|
795
|
+
optionTimeout: number | null | undefined,
|
|
796
|
+
defaultTimeout: number | null | undefined,
|
|
797
|
+
): number | null | undefined {
|
|
798
|
+
if (optionTimeout === null) return null;
|
|
799
|
+
const candidate = optionTimeout === undefined ? defaultTimeout : optionTimeout;
|
|
800
|
+
if (candidate === undefined || candidate === null) return candidate;
|
|
801
|
+
if (!Number.isInteger(candidate) || candidate <= 0 || candidate > MAX_TIMEOUT_MS) {
|
|
802
|
+
throw new TypeError(
|
|
803
|
+
`Process timeout must be a positive integer within the native timer range (1-${MAX_TIMEOUT_MS} ms), got ${String(candidate)}`,
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
return candidate;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Resolve the SIGTERM→SIGKILL escalation grace: omitted uses {@link DEFAULT_KILL_GRACE_MS},
|
|
811
|
+
* `0` escalates immediately, and anything outside the native timer range is rejected.
|
|
812
|
+
*/
|
|
813
|
+
function resolveKillGraceMs(optionGraceMs: number | undefined): number {
|
|
814
|
+
if (optionGraceMs === undefined) return DEFAULT_KILL_GRACE_MS;
|
|
815
|
+
if (!Number.isInteger(optionGraceMs) || optionGraceMs < 0 || optionGraceMs > MAX_TIMEOUT_MS) {
|
|
816
|
+
throw new TypeError(
|
|
817
|
+
`Process killGraceMs must be a non-negative integer within the native timer range (0-${MAX_TIMEOUT_MS} ms), got ${String(optionGraceMs)}`,
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
return optionGraceMs;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/** Whether any member of the owned process group is still alive (signal 0 probe on `-pid`). */
|
|
824
|
+
function groupExists(pid: number): boolean {
|
|
825
|
+
try {
|
|
826
|
+
process.kill(-pid, 0);
|
|
827
|
+
return true;
|
|
828
|
+
} catch (error) {
|
|
829
|
+
// EPERM means the group exists but is not signallable from here.
|
|
830
|
+
return (error as NodeJS.ErrnoException).code === 'EPERM';
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/** Signal the owned process group; `false` when the group is already gone (ESRCH). */
|
|
835
|
+
function signalGroup(pid: number, signal: NodeJS.Signals | number): boolean {
|
|
836
|
+
try {
|
|
837
|
+
process.kill(-pid, signal);
|
|
838
|
+
return true;
|
|
839
|
+
} catch {
|
|
840
|
+
return false;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
async function waitForGroupExit(pid: number, budgetMs: number): Promise<boolean> {
|
|
845
|
+
const giveUpAt = Date.now() + budgetMs;
|
|
846
|
+
while (groupExists(pid)) {
|
|
847
|
+
if (Date.now() >= giveUpAt) return false;
|
|
848
|
+
await new Promise<void>((resolve) => setTimeout(resolve, GROUP_POLL_INTERVAL_MS));
|
|
849
|
+
}
|
|
850
|
+
return true;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* The one termination sequence for an owned Unix process group: group SIGTERM, wait the
|
|
855
|
+
* grace for the whole group to empty, then group SIGKILL and a bounded settle wait.
|
|
856
|
+
* Group liveness — not direct-child exit — decides completion, so escalation is never
|
|
857
|
+
* cancelled because the leader exited first while descendants retain pipes or locks.
|
|
858
|
+
* Resolves `true` only when a group signal was actually delivered.
|
|
859
|
+
*/
|
|
860
|
+
async function reapProcessGroup(pid: number, graceMs: number): Promise<boolean> {
|
|
861
|
+
if (!signalGroup(pid, 'SIGTERM')) return false;
|
|
862
|
+
if (await waitForGroupExit(pid, graceMs)) return true;
|
|
863
|
+
signalGroup(pid, 'SIGKILL');
|
|
864
|
+
await waitForGroupExit(pid, GROUP_KILL_PATIENCE_MS);
|
|
865
|
+
return true;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/** Handle for the executor-owned termination of one spawned process group. */
|
|
869
|
+
interface ProcessGroupOwnership {
|
|
870
|
+
/**
|
|
871
|
+
* Settle the run's termination: resolve the outcome delivered by the executor's own
|
|
872
|
+
* path (deadline `timeout` or external `cancelled`) after the escalation completed, or
|
|
873
|
+
* reap surviving owned descendants after a natural leader exit (outcome stays `exit`).
|
|
874
|
+
* Call exactly once, after the execa promise has settled; bounded, never hangs.
|
|
875
|
+
*/
|
|
876
|
+
finish(): Promise<ProcessOutcome | undefined>;
|
|
877
|
+
/** Release the deadline timer and abort listener once the execa promise has settled. */
|
|
878
|
+
stop(): void;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* Own one detached child's process group on Unix. A deadline timer and the caller's abort
|
|
883
|
+
* signal both feed a single termination path — group SIGTERM, grace, SIGKILL — and after a
|
|
884
|
+
* natural leader exit surviving owned descendants are reaped with the same sequence, so
|
|
885
|
+
* completion cannot leak group members holding inherited pipes or database write locks.
|
|
886
|
+
*/
|
|
887
|
+
function ownProcessGroupLifecycle(ownership: {
|
|
888
|
+
pid: number;
|
|
889
|
+
deadlineMs: number | undefined;
|
|
890
|
+
signal: AbortSignal | undefined;
|
|
891
|
+
graceMs: number;
|
|
892
|
+
}): ProcessGroupOwnership {
|
|
893
|
+
const { pid, graceMs } = ownership;
|
|
894
|
+
let trigger: 'timeout' | 'cancelled' | undefined;
|
|
895
|
+
let termination: Promise<boolean> | undefined;
|
|
896
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
897
|
+
|
|
898
|
+
const initiate = (reason: 'timeout' | 'cancelled'): void => {
|
|
899
|
+
if (trigger !== undefined) return;
|
|
900
|
+
trigger = reason;
|
|
901
|
+
termination = reapProcessGroup(pid, graceMs);
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
if (ownership.deadlineMs !== undefined) {
|
|
905
|
+
timer = setTimeout(() => initiate('timeout'), ownership.deadlineMs);
|
|
906
|
+
}
|
|
907
|
+
const onAbort = (): void => initiate('cancelled');
|
|
908
|
+
if (ownership.signal !== undefined) {
|
|
909
|
+
if (ownership.signal.aborted) onAbort();
|
|
910
|
+
else ownership.signal.addEventListener('abort', onAbort, { once: true });
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
return {
|
|
914
|
+
finish: async () => {
|
|
915
|
+
if (trigger === undefined) {
|
|
916
|
+
// Natural completion: reap stragglers (no-op when the group is already empty).
|
|
917
|
+
await reapProcessGroup(pid, graceMs);
|
|
918
|
+
return undefined;
|
|
919
|
+
}
|
|
920
|
+
// An undeliverable trigger (group already gone — the child finished first)
|
|
921
|
+
// yields undefined so the outcome falls back to the natural classification.
|
|
922
|
+
return (await termination) ? trigger : undefined;
|
|
923
|
+
},
|
|
924
|
+
stop: () => {
|
|
925
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
926
|
+
ownership.signal?.removeEventListener('abort', onAbort);
|
|
927
|
+
},
|
|
725
928
|
};
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/** Classify an execa rejection on paths where the executor did not own the termination. */
|
|
932
|
+
function classifyFailedCompletion(error: {
|
|
933
|
+
timedOut?: boolean;
|
|
934
|
+
isCanceled?: boolean;
|
|
935
|
+
isTerminated?: boolean;
|
|
936
|
+
signalDescription?: string;
|
|
937
|
+
signal?: string;
|
|
938
|
+
}): ProcessOutcome {
|
|
939
|
+
if (error.timedOut === true) return 'timeout';
|
|
940
|
+
if (error.isCanceled === true) return 'cancelled';
|
|
941
|
+
if (error.isTerminated === true || error.signalDescription !== undefined || error.signal !== undefined) {
|
|
942
|
+
return 'signal';
|
|
943
|
+
}
|
|
944
|
+
return 'error';
|
|
729
945
|
}
|
|
730
946
|
|
|
731
947
|
function observeOutput(
|