procband 0.3.2 → 0.3.4
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/dist/index.d.mts +6 -2
- package/dist/index.mjs +36 -8
- package/docs/context.md +11 -5
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -50,7 +50,10 @@ interface ProcessConfig {
|
|
|
50
50
|
/**
|
|
51
51
|
* Whether to spawn the child in a detached process group/session.
|
|
52
52
|
*
|
|
53
|
-
* This is passed through to Node.js `spawn()` unchanged.
|
|
53
|
+
* This is passed through to Node.js `spawn()` unchanged. On Unix-like
|
|
54
|
+
* platforms, shutdown also signals the detached child's process group so
|
|
55
|
+
* same-group descendants are cleaned up even if they are no longer reachable
|
|
56
|
+
* through the child process tree.
|
|
54
57
|
*/
|
|
55
58
|
detached?: boolean;
|
|
56
59
|
/**
|
|
@@ -263,7 +266,8 @@ interface ProcbandProcess extends ChildProcess, PromiseLike<ProcessResult> {
|
|
|
263
266
|
* Disable future restarts and terminate the active process tree.
|
|
264
267
|
*
|
|
265
268
|
* For supervised processes, `kill()` targets the current child process and
|
|
266
|
-
* any descendants it spawned instead of only the direct child.
|
|
269
|
+
* any descendants it spawned instead of only the direct child. Detached
|
|
270
|
+
* Unix-like children are also stopped by process group.
|
|
267
271
|
* `kill(0)` preserves the normal `ChildProcess` existence-check behavior.
|
|
268
272
|
*/
|
|
269
273
|
kill(signal?: KillSignal): boolean;
|
package/dist/index.mjs
CHANGED
|
@@ -111,7 +111,7 @@ var MatchRegistry = class {
|
|
|
111
111
|
}
|
|
112
112
|
waitFor(pattern, options) {
|
|
113
113
|
if (this.closedError) return Promise.reject(this.closedError);
|
|
114
|
-
|
|
114
|
+
const promise = new Promise((resolve, reject) => {
|
|
115
115
|
const subscription = {
|
|
116
116
|
active: true,
|
|
117
117
|
kind: "promise",
|
|
@@ -125,6 +125,9 @@ var MatchRegistry = class {
|
|
|
125
125
|
this.unsubscribe(subscription);
|
|
126
126
|
reject(error);
|
|
127
127
|
},
|
|
128
|
+
suppressUnhandledRejection: () => {
|
|
129
|
+
promise.catch(() => {});
|
|
130
|
+
},
|
|
128
131
|
timer: null
|
|
129
132
|
};
|
|
130
133
|
if (options?.timeoutMs != null) subscription.timer = setTimeout(() => {
|
|
@@ -132,6 +135,7 @@ var MatchRegistry = class {
|
|
|
132
135
|
}, options.timeoutMs);
|
|
133
136
|
this.subscriptions.add(subscription);
|
|
134
137
|
});
|
|
138
|
+
return promise;
|
|
135
139
|
}
|
|
136
140
|
emit(stream, line) {
|
|
137
141
|
if (this.closedError) return;
|
|
@@ -155,11 +159,17 @@ var MatchRegistry = class {
|
|
|
155
159
|
subscription.resolve(event);
|
|
156
160
|
}
|
|
157
161
|
}
|
|
162
|
+
hasPendingWait() {
|
|
163
|
+
for (const subscription of this.subscriptions) if (subscription.kind === "promise") return true;
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
158
166
|
close(error) {
|
|
159
167
|
if (this.closedError) return;
|
|
160
168
|
this.closedError = error;
|
|
161
|
-
for (const subscription of [...this.subscriptions]) if (subscription.kind === "promise")
|
|
162
|
-
|
|
169
|
+
for (const subscription of [...this.subscriptions]) if (subscription.kind === "promise") {
|
|
170
|
+
subscription.suppressUnhandledRejection();
|
|
171
|
+
subscription.reject(error);
|
|
172
|
+
} else this.unsubscribe(subscription);
|
|
163
173
|
}
|
|
164
174
|
unsubscribe(subscription) {
|
|
165
175
|
if (!subscription.active) return;
|
|
@@ -280,18 +290,23 @@ function unregisterCleanupTarget(target) {
|
|
|
280
290
|
liveTargets.delete(target);
|
|
281
291
|
if (liveTargets.size === 0) uninstallParentCleanup();
|
|
282
292
|
}
|
|
283
|
-
function killTreeBestEffort(child, signal) {
|
|
293
|
+
function killTreeBestEffort(child, signal, options = {}) {
|
|
294
|
+
if (options.detached) try {
|
|
295
|
+
killProcessGroup(child, signal);
|
|
296
|
+
} catch {}
|
|
284
297
|
try {
|
|
285
298
|
treeKillSync(child, signal);
|
|
286
299
|
} catch {}
|
|
287
300
|
}
|
|
288
|
-
async function stopChildTree(child, close, isClosed, signal, killAfterMs) {
|
|
301
|
+
async function stopChildTree(child, close, isClosed, signal, killAfterMs, options = {}) {
|
|
302
|
+
if (options.detached) killProcessGroup(child, signal);
|
|
289
303
|
try {
|
|
290
304
|
await treeKill(child, signal);
|
|
291
305
|
} catch (error) {
|
|
292
306
|
if (!isMissingProcessError(error)) throw error;
|
|
293
307
|
}
|
|
294
308
|
if (!await Promise.race([close.then(() => true), setTimeout$1(killAfterMs, false)]) && !isClosed()) {
|
|
309
|
+
if (options.detached) killProcessGroup(child, "SIGKILL");
|
|
295
310
|
try {
|
|
296
311
|
await treeKill(child, "SIGKILL");
|
|
297
312
|
} catch (error) {
|
|
@@ -300,6 +315,14 @@ async function stopChildTree(child, close, isClosed, signal, killAfterMs) {
|
|
|
300
315
|
await close;
|
|
301
316
|
}
|
|
302
317
|
}
|
|
318
|
+
function killProcessGroup(child, signal) {
|
|
319
|
+
if (process.platform === "win32" || !child.pid) return;
|
|
320
|
+
try {
|
|
321
|
+
process.kill(-child.pid, signal);
|
|
322
|
+
} catch (error) {
|
|
323
|
+
if (!isMissingProcessError(error)) throw error;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
303
326
|
function installParentCleanup() {
|
|
304
327
|
if (parentCleanupInstalled) return;
|
|
305
328
|
parentCleanupInstalled = true;
|
|
@@ -498,7 +521,7 @@ var ProcbandProcessImpl = class extends EventEmitter {
|
|
|
498
521
|
cleanupFromExit() {
|
|
499
522
|
this.prepareShutdown();
|
|
500
523
|
const child = this.currentChild;
|
|
501
|
-
if (child) killTreeBestEffort(child, this.getParentCleanupSignal());
|
|
524
|
+
if (child) killTreeBestEffort(child, this.getParentCleanupSignal(), { detached: this.config.detached });
|
|
502
525
|
}
|
|
503
526
|
async cleanupFromSignal(signal) {
|
|
504
527
|
await this.beginShutdown(this.getParentCleanupSignal(signal), 1e3);
|
|
@@ -509,7 +532,7 @@ var ProcbandProcessImpl = class extends EventEmitter {
|
|
|
509
532
|
await this.finalPromise;
|
|
510
533
|
return;
|
|
511
534
|
}
|
|
512
|
-
await stopChildTree(attempt.child, attempt.close, () => attempt.closed, signal, killAfterMs);
|
|
535
|
+
await stopChildTree(attempt.child, attempt.close, () => attempt.closed, signal, killAfterMs, { detached: this.config.detached });
|
|
513
536
|
}
|
|
514
537
|
getParentCleanupSignal(fallback) {
|
|
515
538
|
return this.config.parentExitSignal ?? fallback;
|
|
@@ -639,11 +662,16 @@ var ProcbandProcessImpl = class extends EventEmitter {
|
|
|
639
662
|
liveProcesses.delete(this);
|
|
640
663
|
if (liveProcesses.size === 0) propagatedFailure = false;
|
|
641
664
|
if (this.shouldPropagateFailure(result)) propagateFailure(result.exitCode);
|
|
642
|
-
this.
|
|
665
|
+
this.closeMatches();
|
|
643
666
|
this.unbindStderrSink();
|
|
644
667
|
unregisterCleanupTarget(this);
|
|
645
668
|
this.finalResolve(result);
|
|
646
669
|
}
|
|
670
|
+
closeMatches() {
|
|
671
|
+
const error = /* @__PURE__ */ new Error(`Process "${this.name}" exited before a matching line was observed`);
|
|
672
|
+
if (this.matches.hasPendingWait()) writePrefixedLine(process.stderr, this.label, stderrColor, error.message, true);
|
|
673
|
+
this.matches.close(error);
|
|
674
|
+
}
|
|
647
675
|
markTerminalResultObserved() {
|
|
648
676
|
this.terminalResultObserved = true;
|
|
649
677
|
}
|
package/docs/context.md
CHANGED
|
@@ -12,7 +12,7 @@ Supervision adds five behaviors on top of raw `spawn()`:
|
|
|
12
12
|
- prefixed `stdout` and `stderr`
|
|
13
13
|
- line-based matching for future output
|
|
14
14
|
- optional restart policy with failure suppression
|
|
15
|
-
-
|
|
15
|
+
- shutdown for the child and descendants, including detached Unix process groups
|
|
16
16
|
- parent-exit propagation for unobserved terminal failures
|
|
17
17
|
|
|
18
18
|
# When to Use
|
|
@@ -96,6 +96,9 @@ Supervision adds five behaviors on top of raw `spawn()`:
|
|
|
96
96
|
# Recommended Patterns
|
|
97
97
|
|
|
98
98
|
- Use `proc.kill()` for deliberate shutdown initiated by your own script.
|
|
99
|
+
- Use `detached: true` when a child must run in its own process group/session.
|
|
100
|
+
On Unix-like platforms, `procband` uses that process group during shutdown in
|
|
101
|
+
addition to process-tree cleanup.
|
|
99
102
|
- Reserve `parentExitSignal` for children that expect a specific signal from
|
|
100
103
|
their supervisor during parent-driven cleanup.
|
|
101
104
|
- Await `proc` or call `proc.wait()` when your script intends to own failure
|
|
@@ -130,12 +133,15 @@ Supervision adds five behaviors on top of raw `spawn()`:
|
|
|
130
133
|
- The wrapper survives restarts, but inherited `pid`, `stdin`, `stdout`,
|
|
131
134
|
`stderr`, and related `ChildProcess` fields always refer to the current active
|
|
132
135
|
child attempt. `stdin` is `null` unless `ProcessConfig.stdin` is enabled.
|
|
133
|
-
- `kill()` disables future restarts and kills the full process tree
|
|
134
|
-
`
|
|
136
|
+
- `kill()` disables future restarts and kills the full process tree. For
|
|
137
|
+
`detached: true` children on Unix-like platforms, shutdown also signals the
|
|
138
|
+
detached child's process group so same-group descendants are cleaned up even
|
|
139
|
+
when they are no longer reachable by parent PID. `kill(0)` only checks
|
|
140
|
+
whether the current child attempt exists.
|
|
135
141
|
- Parent cleanup installs both `SIGINT` and `SIGTERM` handlers while any live
|
|
136
142
|
supervised process exists. Set `ProcessConfig.parentExitSignal` to override
|
|
137
|
-
which signal is sent
|
|
138
|
-
|
|
143
|
+
which signal is sent during parent-driven cleanup. This does not change the
|
|
144
|
+
signal used by explicit `proc.kill()` calls.
|
|
139
145
|
- `stderr` prefixes always use the reserved red, even when a custom process
|
|
140
146
|
color is configured.
|
|
141
147
|
- `ProcessConfig.name` is optional. When omitted, it falls back to the
|
package/package.json
CHANGED