procband 0.1.0 → 0.2.0
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 +3 -1
- package/dist/index.d.mts +8 -1
- package/dist/index.mjs +58 -21
- package/docs/context.md +20 -1
- package/examples/sequenced-processes.ts +29 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
`procband` supervises subprocesses from TypeScript. It prefixes child output,
|
|
6
6
|
matches future log lines, waits for terminal exit, optionally restarts failed
|
|
7
|
-
processes,
|
|
7
|
+
processes, tree-kills descendants during shutdown, and propagates unobserved
|
|
8
|
+
terminal failures to the parent process.
|
|
8
9
|
|
|
9
10
|
## Installation
|
|
10
11
|
|
|
@@ -34,4 +35,5 @@ console.log(result)
|
|
|
34
35
|
- Concepts and lifecycle: [docs/context.md](docs/context.md)
|
|
35
36
|
- Minimal usage example: [examples/basic-usage.ts](examples/basic-usage.ts)
|
|
36
37
|
- Restart example: [examples/restart-on-failure.ts](examples/restart-on-failure.ts)
|
|
38
|
+
- Sequencing example: [examples/sequenced-processes.ts](examples/sequenced-processes.ts)
|
|
37
39
|
- Exact exported signatures: [dist/index.d.mts](dist/index.d.mts)
|
package/dist/index.d.mts
CHANGED
|
@@ -190,6 +190,13 @@ interface ProcessResult {
|
|
|
190
190
|
* Final child exit code, or `null` when the process exited by signal.
|
|
191
191
|
*/
|
|
192
192
|
code: number | null;
|
|
193
|
+
/**
|
|
194
|
+
* Shell-style exit status for the final process outcome.
|
|
195
|
+
*
|
|
196
|
+
* This is equal to `code` for normal exits, or `128 + signalNumber` when the
|
|
197
|
+
* process exited by signal.
|
|
198
|
+
*/
|
|
199
|
+
exitCode: number;
|
|
193
200
|
/**
|
|
194
201
|
* Final terminating signal, or `null` when the process exited normally.
|
|
195
202
|
*/
|
|
@@ -258,7 +265,7 @@ interface ProcbandProcess extends ChildProcess, PromiseLike<ProcessResult> {
|
|
|
258
265
|
/**
|
|
259
266
|
* Start supervising a single subprocess.
|
|
260
267
|
*
|
|
261
|
-
* The returned `
|
|
268
|
+
* The returned `ProcbandProcess` starts immediately, prefixes child output,
|
|
262
269
|
* exposes line-based matching helpers, and resolves when the process reaches a
|
|
263
270
|
* terminal state with no further restart pending.
|
|
264
271
|
*
|
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
|
+
import { constants } from "node:os";
|
|
3
4
|
import process from "node:process";
|
|
4
5
|
import ansiStyles from "ansi-styles";
|
|
5
6
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
6
|
-
import treeKill from "@alloc/tree-kill";
|
|
7
|
+
import treeKill, { treeKillSync } from "@alloc/tree-kill";
|
|
7
8
|
//#region src/colors.ts
|
|
8
9
|
const prefixPalette = [
|
|
9
10
|
[
|
|
@@ -270,7 +271,7 @@ function isFailedExit(code, signal) {
|
|
|
270
271
|
//#region src/shutdown.ts
|
|
271
272
|
const liveTargets = /* @__PURE__ */ new Set();
|
|
272
273
|
let parentCleanupInstalled = false;
|
|
273
|
-
let
|
|
274
|
+
let handlingSignal = false;
|
|
274
275
|
function registerCleanupTarget(target) {
|
|
275
276
|
liveTargets.add(target);
|
|
276
277
|
installParentCleanup();
|
|
@@ -280,7 +281,9 @@ function unregisterCleanupTarget(target) {
|
|
|
280
281
|
if (liveTargets.size === 0) uninstallParentCleanup();
|
|
281
282
|
}
|
|
282
283
|
function killTreeBestEffort(child, signal) {
|
|
283
|
-
|
|
284
|
+
try {
|
|
285
|
+
treeKillSync(child, signal);
|
|
286
|
+
} catch {}
|
|
284
287
|
}
|
|
285
288
|
async function stopChildTree(child, close, isClosed, signal, killAfterMs) {
|
|
286
289
|
try {
|
|
@@ -301,29 +304,38 @@ function installParentCleanup() {
|
|
|
301
304
|
if (parentCleanupInstalled) return;
|
|
302
305
|
parentCleanupInstalled = true;
|
|
303
306
|
process.on("SIGINT", onParentSigint);
|
|
307
|
+
process.on("SIGTERM", onParentSigterm);
|
|
304
308
|
process.on("exit", onParentExit);
|
|
305
309
|
}
|
|
306
310
|
function uninstallParentCleanup() {
|
|
307
311
|
if (!parentCleanupInstalled) return;
|
|
308
312
|
parentCleanupInstalled = false;
|
|
309
313
|
process.off("SIGINT", onParentSigint);
|
|
314
|
+
process.off("SIGTERM", onParentSigterm);
|
|
310
315
|
process.off("exit", onParentExit);
|
|
311
316
|
}
|
|
312
317
|
function onParentExit() {
|
|
313
318
|
for (const target of [...liveTargets]) target.cleanupFromExit();
|
|
314
319
|
}
|
|
315
320
|
function onParentSigint() {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
321
|
+
onParentSignal("SIGINT");
|
|
322
|
+
}
|
|
323
|
+
function onParentSigterm() {
|
|
324
|
+
onParentSignal("SIGTERM");
|
|
325
|
+
}
|
|
326
|
+
function onParentSignal(signal) {
|
|
327
|
+
if (handlingSignal) return;
|
|
328
|
+
handlingSignal = true;
|
|
329
|
+
const handler = signal === "SIGINT" ? onParentSigint : onParentSigterm;
|
|
330
|
+
const pending = [...liveTargets].map((target) => target.cleanupFromSignal(signal));
|
|
319
331
|
Promise.allSettled(pending).finally(() => {
|
|
320
|
-
|
|
321
|
-
process.off(
|
|
322
|
-
if (process.listenerCount(
|
|
323
|
-
process.kill(process.pid,
|
|
332
|
+
handlingSignal = false;
|
|
333
|
+
process.off(signal, handler);
|
|
334
|
+
if (process.listenerCount(signal) === 0) {
|
|
335
|
+
process.kill(process.pid, signal);
|
|
324
336
|
return;
|
|
325
337
|
}
|
|
326
|
-
process.on(
|
|
338
|
+
process.on(signal, handler);
|
|
327
339
|
});
|
|
328
340
|
}
|
|
329
341
|
function isMissingProcessError(error) {
|
|
@@ -334,7 +346,7 @@ function isMissingProcessError(error) {
|
|
|
334
346
|
/**
|
|
335
347
|
* Start supervising a single subprocess.
|
|
336
348
|
*
|
|
337
|
-
* The returned `
|
|
349
|
+
* The returned `ProcbandProcess` starts immediately, prefixes child output,
|
|
338
350
|
* exposes line-based matching helpers, and resolves when the process reaches a
|
|
339
351
|
* terminal state with no further restart pending.
|
|
340
352
|
*
|
|
@@ -376,9 +388,11 @@ var ProcbandProcessImpl = class extends EventEmitter {
|
|
|
376
388
|
stderrSinkActive = true;
|
|
377
389
|
stderrSinkCleanup = null;
|
|
378
390
|
stopPromise = null;
|
|
391
|
+
shutdownRequested = false;
|
|
379
392
|
restartDisabled = false;
|
|
380
393
|
finalized = false;
|
|
381
394
|
lastResult = null;
|
|
395
|
+
terminalResultObserved = false;
|
|
382
396
|
constructor(config) {
|
|
383
397
|
super();
|
|
384
398
|
validateProcessConfig(config);
|
|
@@ -395,6 +409,7 @@ var ProcbandProcessImpl = class extends EventEmitter {
|
|
|
395
409
|
this.bindStderrSink();
|
|
396
410
|
this.spawnAttempt();
|
|
397
411
|
registerCleanupTarget(this);
|
|
412
|
+
liveProcesses.add(this);
|
|
398
413
|
}
|
|
399
414
|
get pid() {
|
|
400
415
|
return this.currentChild?.pid;
|
|
@@ -443,17 +458,13 @@ var ProcbandProcessImpl = class extends EventEmitter {
|
|
|
443
458
|
return this.matches.waitFor(pattern, options);
|
|
444
459
|
}
|
|
445
460
|
wait() {
|
|
461
|
+
this.markTerminalResultObserved();
|
|
446
462
|
return this.finalPromise;
|
|
447
463
|
}
|
|
448
464
|
then(onfulfilled, onrejected) {
|
|
465
|
+
this.markTerminalResultObserved();
|
|
449
466
|
return this.finalPromise.then(onfulfilled, onrejected);
|
|
450
467
|
}
|
|
451
|
-
catch(onrejected) {
|
|
452
|
-
return this.finalPromise.catch(onrejected);
|
|
453
|
-
}
|
|
454
|
-
finally(onfinally) {
|
|
455
|
-
return this.finalPromise.finally(onfinally);
|
|
456
|
-
}
|
|
457
468
|
kill(signal) {
|
|
458
469
|
const child = this.currentChild;
|
|
459
470
|
if (!child) return false;
|
|
@@ -472,26 +483,29 @@ var ProcbandProcessImpl = class extends EventEmitter {
|
|
|
472
483
|
}
|
|
473
484
|
async stop(options) {
|
|
474
485
|
if (this.stopPromise) return this.stopPromise;
|
|
486
|
+
this.shutdownRequested = true;
|
|
475
487
|
this.restartDisabled = true;
|
|
476
488
|
this.restart.cancelDelay();
|
|
477
489
|
this.stopPromise = this.stopActiveTree(options?.signal ?? "SIGTERM", options?.killAfterMs ?? 5e3);
|
|
478
490
|
return this.stopPromise;
|
|
479
491
|
}
|
|
480
492
|
cleanupFromExit() {
|
|
493
|
+
this.shutdownRequested = true;
|
|
481
494
|
this.restartDisabled = true;
|
|
482
495
|
this.restart.cancelDelay();
|
|
483
496
|
const child = this.currentChild;
|
|
484
497
|
if (child) killTreeBestEffort(child);
|
|
485
498
|
}
|
|
486
|
-
async
|
|
499
|
+
async cleanupFromSignal(signal) {
|
|
500
|
+
this.shutdownRequested = true;
|
|
487
501
|
this.restartDisabled = true;
|
|
488
502
|
this.restart.cancelDelay();
|
|
489
|
-
await this.stopActiveTree(
|
|
503
|
+
await this.stopActiveTree(signal, 1e3);
|
|
490
504
|
}
|
|
491
505
|
async stopActiveTree(signal, killAfterMs) {
|
|
492
506
|
const attempt = this.attempt;
|
|
493
507
|
if (!attempt || attempt.closed) {
|
|
494
|
-
await this.
|
|
508
|
+
await this.finalPromise;
|
|
495
509
|
return;
|
|
496
510
|
}
|
|
497
511
|
await stopChildTree(attempt.child, attempt.close, () => attempt.closed, signal, killAfterMs);
|
|
@@ -602,6 +616,7 @@ var ProcbandProcessImpl = class extends EventEmitter {
|
|
|
602
616
|
return {
|
|
603
617
|
name: this.name,
|
|
604
618
|
code,
|
|
619
|
+
exitCode: getResultExitCode(code, signal),
|
|
605
620
|
signal,
|
|
606
621
|
restarts: this.restart.restarts,
|
|
607
622
|
restartSuppressed: this.restart.restartSuppressed
|
|
@@ -612,11 +627,20 @@ var ProcbandProcessImpl = class extends EventEmitter {
|
|
|
612
627
|
this.finalized = true;
|
|
613
628
|
this.restart.cancelDelay();
|
|
614
629
|
this.lastResult = result;
|
|
630
|
+
liveProcesses.delete(this);
|
|
631
|
+
if (liveProcesses.size === 0) propagatedFailure = false;
|
|
632
|
+
if (this.shouldPropagateFailure(result)) propagateFailure(result.exitCode);
|
|
615
633
|
this.matches.close(/* @__PURE__ */ new Error(`Process "${this.name}" exited before a matching line was observed`));
|
|
616
634
|
this.unbindStderrSink();
|
|
617
635
|
unregisterCleanupTarget(this);
|
|
618
636
|
this.finalResolve(result);
|
|
619
637
|
}
|
|
638
|
+
markTerminalResultObserved() {
|
|
639
|
+
this.terminalResultObserved = true;
|
|
640
|
+
}
|
|
641
|
+
shouldPropagateFailure(result) {
|
|
642
|
+
return result.exitCode !== 0 && !this.shutdownRequested && !this.terminalResultObserved;
|
|
643
|
+
}
|
|
620
644
|
bindStderrSink() {
|
|
621
645
|
const sink = this.stderrSink;
|
|
622
646
|
if (!sink || typeof sink.on !== "function") return;
|
|
@@ -659,10 +683,23 @@ var ProcbandProcessImpl = class extends EventEmitter {
|
|
|
659
683
|
this.handleLine(stream, line, false);
|
|
660
684
|
}
|
|
661
685
|
};
|
|
686
|
+
const liveProcesses = /* @__PURE__ */ new Set();
|
|
687
|
+
let propagatedFailure = false;
|
|
662
688
|
function validateProcessConfig(config) {
|
|
663
689
|
if (!config.name) throw new Error("ProcessConfig.name is required");
|
|
664
690
|
if (!config.command) throw new Error("ProcessConfig.command is required");
|
|
665
691
|
validateProcessColor(config.color);
|
|
666
692
|
}
|
|
693
|
+
function getResultExitCode(code, signal) {
|
|
694
|
+
if (code != null) return code;
|
|
695
|
+
if (signal) return 128 + (constants.signals[signal] ?? 0);
|
|
696
|
+
return 1;
|
|
697
|
+
}
|
|
698
|
+
function propagateFailure(exitCode) {
|
|
699
|
+
if (propagatedFailure) return;
|
|
700
|
+
propagatedFailure = true;
|
|
701
|
+
if (process.exitCode == null || process.exitCode === 0) process.exitCode = exitCode;
|
|
702
|
+
for (const proc of liveProcesses) proc.stop().catch(() => {});
|
|
703
|
+
}
|
|
667
704
|
//#endregion
|
|
668
705
|
export { supervise };
|
package/docs/context.md
CHANGED
|
@@ -13,6 +13,7 @@ Supervision adds four behaviors on top of raw `spawn()`:
|
|
|
13
13
|
- line-based matching for future output
|
|
14
14
|
- optional restart policy with failure suppression
|
|
15
15
|
- tree-aware shutdown for the child and its descendants
|
|
16
|
+
- parent-exit propagation for unobserved terminal failures
|
|
16
17
|
|
|
17
18
|
# When to Use
|
|
18
19
|
|
|
@@ -55,7 +56,10 @@ Supervision adds four behaviors on top of raw `spawn()`:
|
|
|
55
56
|
`waitFor()`.
|
|
56
57
|
6. When a child exits, `procband` either finalizes or starts a new attempt,
|
|
57
58
|
depending on the restart policy.
|
|
58
|
-
7.
|
|
59
|
+
7. A terminal failed exit that nobody observed through `await proc` or
|
|
60
|
+
`proc.wait()` sets `process.exitCode` and begins
|
|
61
|
+
stopping any other live `procband` processes in the same parent script.
|
|
62
|
+
8. `await proc` or `await proc.wait()` resolves only after the process is
|
|
59
63
|
terminal and no further restart will happen.
|
|
60
64
|
|
|
61
65
|
# Common Tasks -> Recommended APIs
|
|
@@ -68,6 +72,10 @@ Supervision adds four behaviors on top of raw `spawn()`:
|
|
|
68
72
|
`proc.stop()`
|
|
69
73
|
- Inspect final exit state:
|
|
70
74
|
`await proc` or `await proc.wait()`
|
|
75
|
+
- Take ownership of a process failure:
|
|
76
|
+
`await proc` or `await proc.wait()`
|
|
77
|
+
- Let an unobserved terminal failure fail the parent script:
|
|
78
|
+
Do not call `wait()` or await the thenable result
|
|
71
79
|
- Capture raw child `stderr` in a file or custom stream:
|
|
72
80
|
`ProcessConfig.stderr`
|
|
73
81
|
- Retry failed exits with sane defaults:
|
|
@@ -85,11 +93,20 @@ Supervision adds four behaviors on top of raw `spawn()`:
|
|
|
85
93
|
observed.
|
|
86
94
|
- `await proc` resolves for both successful and failed exits. Inspect the
|
|
87
95
|
returned `ProcessResult`.
|
|
96
|
+
- `ProcessResult.exitCode` exposes the shell-style exit status for the final
|
|
97
|
+
outcome, including signal exits.
|
|
98
|
+
- Calling `proc.wait()` or awaiting the thenable process marks its terminal
|
|
99
|
+
result as observed and suppresses default parent-exit propagation.
|
|
100
|
+
- An unobserved terminal failure sets `process.exitCode` to the first failing
|
|
101
|
+
process's `ProcessResult.exitCode` and starts stopping other live `procband`
|
|
102
|
+
processes in the same parent script.
|
|
88
103
|
- The wrapper survives restarts, but inherited `pid`, `stdin`, `stdout`,
|
|
89
104
|
`stderr`, and related `ChildProcess` fields always refer to the current active
|
|
90
105
|
child attempt.
|
|
91
106
|
- `kill()` only signals the current direct child. `stop()` disables restart and
|
|
92
107
|
kills the full process tree.
|
|
108
|
+
- Parent cleanup installs both `SIGINT` and `SIGTERM` handlers while any live
|
|
109
|
+
supervised process exists.
|
|
93
110
|
- `stderr` prefixes always use the reserved red, even when a custom process
|
|
94
111
|
color is configured.
|
|
95
112
|
|
|
@@ -101,6 +118,8 @@ Supervision adds four behaviors on top of raw `spawn()`:
|
|
|
101
118
|
- A thrown `match()` callback only unsubscribes that callback.
|
|
102
119
|
- Errors from `ProcessConfig.stderr` stop teeing to that sink but do not stop
|
|
103
120
|
supervision.
|
|
121
|
+
- Unobserved terminal failures do not reject promises. They set the parent
|
|
122
|
+
`process.exitCode` and start stopping sibling `procband` processes.
|
|
104
123
|
- `stop()` may reject if tree-kill fails with a non-`ESRCH` error.
|
|
105
124
|
|
|
106
125
|
# Terminology
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import process from 'node:process'
|
|
2
|
+
import { supervise } from 'procband'
|
|
3
|
+
|
|
4
|
+
const api = supervise({
|
|
5
|
+
name: 'api',
|
|
6
|
+
command: process.execPath,
|
|
7
|
+
args: [
|
|
8
|
+
'-e',
|
|
9
|
+
[
|
|
10
|
+
'console.log("booting")',
|
|
11
|
+
'setTimeout(() => console.log("ready"), 20)',
|
|
12
|
+
'setInterval(() => {}, 1000)',
|
|
13
|
+
].join(';'),
|
|
14
|
+
],
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
await api.waitFor('ready')
|
|
18
|
+
|
|
19
|
+
supervise({
|
|
20
|
+
name: 'worker',
|
|
21
|
+
command: process.execPath,
|
|
22
|
+
args: [
|
|
23
|
+
'-e',
|
|
24
|
+
[
|
|
25
|
+
'console.log("watching")',
|
|
26
|
+
'setTimeout(() => process.exit(1), 60)',
|
|
27
|
+
].join(';'),
|
|
28
|
+
],
|
|
29
|
+
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "procband",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Supervise subprocesses from TypeScript: prefix logs, wait for output patterns, and manage lifecycle/restarts.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Alec Larson",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"vitest": "^4.1.2"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@alloc/tree-kill": "^1.
|
|
30
|
+
"@alloc/tree-kill": "^1.4.0",
|
|
31
31
|
"ansi-styles": "^6.2.3"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|