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 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
- return new Promise((resolve, reject) => {
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") subscription.reject(error);
162
- else this.unsubscribe(subscription);
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.matches.close(/* @__PURE__ */ new Error(`Process "${this.name}" exited before a matching line was observed`));
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
- - tree-aware shutdown for the child and its descendants
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, except for
134
- `kill(0)`, which only checks whether the current child attempt exists.
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 to the child tree during parent-driven cleanup. This
138
- does not change the signal used by explicit `proc.kill()` calls.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "procband",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
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",