@gobing-ai/ts-runtime 0.5.4 → 0.5.5

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 CHANGED
@@ -641,6 +641,13 @@ The `ProcessExecutor` const (value alias for `NodeProcessExecutor`), `BunSyncPro
641
641
  and `BunPipeProcessSpawner` are kept as deprecated backward-compatible wrappers. Prefer
642
642
  `NodeProcessExecutor` or `nodeBunFactory.createProcessExecutor()` in new code.
643
643
 
644
+ **Deprecation status (task 0087 R7):** sync callers now default to the non-deprecated
645
+ `NodeSyncProcessExecutor` (node `spawnSync`) — `identity.ts` was migrated off the
646
+ `BunSyncProcessExecutor` default. `BunSyncProcessExecutor` and `BunPipeProcessSpawner`
647
+ remain exported-deprecated and are deleted in the next major version. The process-group
648
+ containment machinery lives in `src/process-group.ts` (extracted from
649
+ `process-executor.ts`, task 0087 R8 — pure move, no behavior change).
650
+
644
651
  ### SpanContext (for telemetry)
645
652
 
646
653
  ```ts
package/dist/index.d.ts CHANGED
@@ -19,12 +19,9 @@ export type { RuntimePaths } from './runtime-paths';
19
19
  export { ambientRuntimePaths } from './runtime-paths';
20
20
  export * from './schema-validation';
21
21
  export * from './types';
22
- export { BunPipeProcessSpawner, BunSyncProcessExecutor } from './process-executor';
23
- /**
24
- * @deprecated Use {@link ProcessExecutor} directly for async execution.
25
- * Use `Bun.spawnSync` or `child_process.spawnSync` for sync.
26
- */
27
- export type SyncProcessExecutor = InstanceType<typeof import('./process-executor').BunSyncProcessExecutor>;
22
+ /** Sync executor shape (task 0087 R7) — satisfied by NodeSyncProcessExecutor (preferred) and the deprecated BunSyncProcessExecutor. */
23
+ export type { SyncProcessExecutor } from './process-executor';
24
+ export { BunPipeProcessSpawner, BunSyncProcessExecutor, NodeSyncProcessExecutor } from './process-executor';
28
25
  /**
29
26
  * @deprecated Use {@link ProcessExecutor.runStreaming} instead.
30
27
  */
@@ -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,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,uIAAuI;AACvI,YAAY,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAE5G;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,YAAY,CAAC,cAAc,oBAAoB,EAAE,qBAAqB,CAAC,CAAC"}
package/dist/index.js CHANGED
@@ -14,5 +14,4 @@ export { _resetNodeFileSystem, nodeBunFactory } from './runtime-node-bun.js';
14
14
  export { ambientRuntimePaths } from './runtime-paths.js';
15
15
  export * from './schema-validation.js';
16
16
  export * from './types.js';
17
- // ── Deprecated re-exports (backward compatibility) ──────────────────────
18
- export { BunPipeProcessSpawner, BunSyncProcessExecutor } from './process-executor.js';
17
+ export { BunPipeProcessSpawner, BunSyncProcessExecutor, NodeSyncProcessExecutor } from './process-executor.js';
@@ -270,7 +270,24 @@ export declare const ProcessExecutor: typeof NodeProcessExecutor;
270
270
  * Synchronous process execution is no longer recommended from ts-runtime.
271
271
  * This class is kept for backward compatibility.
272
272
  */
273
- export declare class BunSyncProcessExecutor {
273
+ export declare class BunSyncProcessExecutor implements SyncProcessExecutor {
274
+ runSync(options: Omit<ProcessOptions, 'timeout'>): ProcessResult;
275
+ }
276
+ /**
277
+ * Sync process execution surface shared by the sync executors (task 0087 R7).
278
+ * Not deprecated — the deprecated part is the Bun-specific default, not the shape.
279
+ */
280
+ export interface SyncProcessExecutor {
281
+ runSync(options: Omit<ProcessOptions, 'timeout'>): ProcessResult;
282
+ }
283
+ /**
284
+ * Sync executor on `node:child_process.spawnSync` — the non-deprecated default
285
+ * for sync callers (task 0087 R7). Contract-compatible with
286
+ * {@link BunSyncProcessExecutor} for success and non-zero exits. One deliberate
287
+ * divergence: a spawn failure (e.g. missing binary) maps to exitCode 1 instead of
288
+ * throwing, so `getGitContextSync`-style probes see a plain non-zero exit.
289
+ */
290
+ export declare class NodeSyncProcessExecutor implements SyncProcessExecutor {
274
291
  runSync(options: Omit<ProcessOptions, 'timeout'>): ProcessResult;
275
292
  }
276
293
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"process-executor.d.ts","sourceRoot":"","sources":["../src/process-executor.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"process-executor.d.ts","sourceRoot":"","sources":["../src/process-executor.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,8 @@
1
+ import { spawnSync } from 'node:child_process';
1
2
  import { isatty } from 'node:tty';
2
3
  import { execa } from 'execa';
3
4
  import { getProcessEnv } from './config.js';
5
+ import { ownProcessGroupLifecycle, resolveDeadline, resolveKillGraceMs, } from './process-group.js';
4
6
  function processEventSeverity(reason, exitCode) {
5
7
  if (reason === 'error')
6
8
  return 'error';
@@ -168,7 +170,8 @@ export class NodeProcessExecutor {
168
170
  */
169
171
  runStreaming(options) {
170
172
  const args = options.args ?? [];
171
- void this.config.tracer?.traceAsync('process.runStreaming', async () => undefined).catch(() => undefined);
173
+ // Task 0086 R14: no lifetime span here — the empty fire-and-forget span
174
+ // carried no data and confused span consumers. Registry + events cover it.
172
175
  const startedAt = Date.now();
173
176
  const startedIso = new Date(startedAt).toISOString();
174
177
  // Begin registry before spawn so failed spawns still appear (then complete as error).
@@ -391,6 +394,36 @@ export class BunSyncProcessExecutor {
391
394
  };
392
395
  }
393
396
  }
397
+ /**
398
+ * Sync executor on `node:child_process.spawnSync` — the non-deprecated default
399
+ * for sync callers (task 0087 R7). Contract-compatible with
400
+ * {@link BunSyncProcessExecutor} for success and non-zero exits. One deliberate
401
+ * divergence: a spawn failure (e.g. missing binary) maps to exitCode 1 instead of
402
+ * throwing, so `getGitContextSync`-style probes see a plain non-zero exit.
403
+ */
404
+ export class NodeSyncProcessExecutor {
405
+ runSync(options) {
406
+ const args = options.args ?? [];
407
+ const startedAt = Date.now();
408
+ const result = spawnSync(options.command, args, {
409
+ stdio: ['ignore', 'pipe', 'pipe'],
410
+ ...(options.cwd !== undefined ? { cwd: options.cwd } : {}),
411
+ ...(options.env !== undefined ? { env: options.env } : {}),
412
+ });
413
+ const exitCode = result.status ?? 1;
414
+ if (options.rejectOnError === true && exitCode !== 0) {
415
+ throw new Error(`${options.command} ${args.join(' ')} failed with exit code ${exitCode}: ${stripFinalNewline(asString(result.stderr))}`);
416
+ }
417
+ return {
418
+ command: options.command,
419
+ args,
420
+ exitCode,
421
+ stdout: stripFinalNewline(asString(result.stdout)),
422
+ stderr: stripFinalNewline(asString(result.stderr)),
423
+ durationMs: Date.now() - startedAt,
424
+ };
425
+ }
426
+ }
394
427
  /**
395
428
  * @deprecated Use {@link ProcessExecutor.runStreaming} instead.
396
429
  * This class is kept for backward compatibility.
@@ -435,137 +468,6 @@ function buildExecaOptions(opts) {
435
468
  };
436
469
  }
437
470
  // ── 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);
540
- };
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
471
  /** Classify an execa rejection on paths where the executor did not own the termination. */
570
472
  function classifyFailedCompletion(error) {
571
473
  if (error.timedOut === true)
@@ -580,17 +482,25 @@ function classifyFailedCompletion(error) {
580
482
  function observeOutput(stream, name, observer) {
581
483
  if (!stream || !observer)
582
484
  return;
583
- stream.on('data', (chunk) => {
485
+ // Task 0086 R13: one decoder per stream with `stream: true` keeps multi-byte
486
+ // UTF-8 sequences intact across chunk boundaries; a fresh decoder per chunk
487
+ // would split them into U+FFFD replacement characters.
488
+ const decoder = new TextDecoder();
489
+ const emit = (chunk) => {
584
490
  try {
585
- observer({
586
- stream: name,
587
- chunk: asString(chunk),
588
- timestamp: new Date().toISOString(),
589
- });
491
+ observer({ stream: name, chunk, timestamp: new Date().toISOString() });
590
492
  }
591
493
  catch {
592
494
  // Observability is best-effort and must never interrupt child I/O.
593
495
  }
496
+ };
497
+ stream.on('data', (chunk) => {
498
+ emit(typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true }));
499
+ });
500
+ stream.on('end', () => {
501
+ const tail = decoder.decode();
502
+ if (tail.length > 0)
503
+ emit(tail);
594
504
  });
595
505
  }
596
506
  function asString(value) {
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Executor-owned process-group containment (task 0087 R8): deadline resolution,
3
+ * group liveness probes, SIGTERM→SIGKILL escalation, and reap lifecycle for
4
+ * process groups spawned by NodeProcessExecutor. Extracted from
5
+ * process-executor.ts as a pure move — no behavior change.
6
+ */
7
+ import type { ProcessOutcome } from './process-executor';
8
+ /**
9
+ * Resolve the effective deadline: omitted inherits the executor default (which may itself
10
+ * be unlimited), explicit `null` is unlimited and overrides any finite default, and a
11
+ * finite value must be a positive integer within the native timer range. Invalid values
12
+ * are rejected before spawn — never clamped and never overflowed onto a timer.
13
+ */
14
+ export declare function resolveDeadline(optionTimeout: number | null | undefined, defaultTimeout: number | null | undefined): number | null | undefined;
15
+ /**
16
+ * Resolve the SIGTERM→SIGKILL escalation grace: omitted uses {@link DEFAULT_KILL_GRACE_MS},
17
+ * `0` escalates immediately, and anything outside the native timer range is rejected.
18
+ */
19
+ export declare function resolveKillGraceMs(optionGraceMs: number | undefined): number;
20
+ /** Handle for the executor-owned termination of one spawned process group. */
21
+ export interface ProcessGroupOwnership {
22
+ /**
23
+ * Settle the run's termination: resolve the outcome delivered by the executor's own
24
+ * path (deadline `timeout` or external `cancelled`) after the escalation completed, or
25
+ * reap surviving owned descendants after a natural leader exit (outcome stays `exit`).
26
+ * Call exactly once, after the execa promise has settled; bounded, never hangs.
27
+ */
28
+ finish(): Promise<ProcessOutcome | undefined>;
29
+ /** Release the deadline timer and abort listener once the execa promise has settled. */
30
+ stop(): void;
31
+ }
32
+ /**
33
+ * Own one detached child's process group on Unix. A deadline timer and the caller's abort
34
+ * signal both feed a single termination path — group SIGTERM, grace, SIGKILL — and after a
35
+ * natural leader exit surviving owned descendants are reaped with the same sequence, so
36
+ * completion cannot leak group members holding inherited pipes or database write locks.
37
+ */
38
+ export declare function ownProcessGroupLifecycle(ownership: {
39
+ pid: number;
40
+ deadlineMs: number | undefined;
41
+ signal: AbortSignal | undefined;
42
+ graceMs: number;
43
+ }): ProcessGroupOwnership;
44
+ //# sourceMappingURL=process-group.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"process-group.d.ts","sourceRoot":"","sources":["../src/process-group.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAiBzD;;;;;GAKG;AACH,wBAAgB,eAAe,CAC3B,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACxC,cAAc,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC1C,MAAM,GAAG,IAAI,GAAG,SAAS,CAU3B;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAQ5E;AA+CD,8EAA8E;AAC9E,MAAM,WAAW,qBAAqB;IAClC;;;;;OAKG;IACH,MAAM,IAAI,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;IAC9C,wFAAwF;IACxF,IAAI,IAAI,IAAI,CAAC;CAChB;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,SAAS,EAAE;IAChD,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,MAAM,EAAE,WAAW,GAAG,SAAS,CAAC;IAChC,OAAO,EAAE,MAAM,CAAC;CACnB,GAAG,qBAAqB,CAqCxB"}
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Executor-owned process-group containment (task 0087 R8): deadline resolution,
3
+ * group liveness probes, SIGTERM→SIGKILL escalation, and reap lifecycle for
4
+ * process groups spawned by NodeProcessExecutor. Extracted from
5
+ * process-executor.ts as a pure move — no behavior change.
6
+ */
7
+ /**
8
+ * Largest delay accepted by native timers; anything larger would overflow onto an
9
+ * immediate timer, so deadline/grace values beyond it are rejected before spawn.
10
+ */
11
+ const MAX_TIMEOUT_MS = 2_147_483_647;
12
+ /** Default grace between the owned group SIGTERM and the SIGKILL escalation. */
13
+ const DEFAULT_KILL_GRACE_MS = 5000;
14
+ /** Poll cadence while waiting for an owned process group to empty. */
15
+ const GROUP_POLL_INTERVAL_MS = 25;
16
+ /** Bounded patience after group SIGKILL before giving up on unkillable (D-state) members. */
17
+ const GROUP_KILL_PATIENCE_MS = 2000;
18
+ /**
19
+ * Resolve the effective deadline: omitted inherits the executor default (which may itself
20
+ * be unlimited), explicit `null` is unlimited and overrides any finite default, and a
21
+ * finite value must be a positive integer within the native timer range. Invalid values
22
+ * are rejected before spawn — never clamped and never overflowed onto a timer.
23
+ */
24
+ export function resolveDeadline(optionTimeout, defaultTimeout) {
25
+ if (optionTimeout === null)
26
+ return null;
27
+ const candidate = optionTimeout === undefined ? defaultTimeout : optionTimeout;
28
+ if (candidate === undefined || candidate === null)
29
+ return candidate;
30
+ if (!Number.isInteger(candidate) || candidate <= 0 || candidate > MAX_TIMEOUT_MS) {
31
+ throw new TypeError(`Process timeout must be a positive integer within the native timer range (1-${MAX_TIMEOUT_MS} ms), got ${String(candidate)}`);
32
+ }
33
+ return candidate;
34
+ }
35
+ /**
36
+ * Resolve the SIGTERM→SIGKILL escalation grace: omitted uses {@link DEFAULT_KILL_GRACE_MS},
37
+ * `0` escalates immediately, and anything outside the native timer range is rejected.
38
+ */
39
+ export function resolveKillGraceMs(optionGraceMs) {
40
+ if (optionGraceMs === undefined)
41
+ return DEFAULT_KILL_GRACE_MS;
42
+ if (!Number.isInteger(optionGraceMs) || optionGraceMs < 0 || optionGraceMs > MAX_TIMEOUT_MS) {
43
+ throw new TypeError(`Process killGraceMs must be a non-negative integer within the native timer range (0-${MAX_TIMEOUT_MS} ms), got ${String(optionGraceMs)}`);
44
+ }
45
+ return optionGraceMs;
46
+ }
47
+ /** Whether any member of the owned process group is still alive (signal 0 probe on `-pid`). */
48
+ function groupExists(pid) {
49
+ try {
50
+ process.kill(-pid, 0);
51
+ return true;
52
+ }
53
+ catch (error) {
54
+ // EPERM means the group exists but is not signallable from here.
55
+ return error.code === 'EPERM';
56
+ }
57
+ }
58
+ /** Signal the owned process group; `false` when the group is already gone (ESRCH). */
59
+ function signalGroup(pid, signal) {
60
+ try {
61
+ process.kill(-pid, signal);
62
+ return true;
63
+ }
64
+ catch {
65
+ return false;
66
+ }
67
+ }
68
+ async function waitForGroupExit(pid, budgetMs) {
69
+ const giveUpAt = Date.now() + budgetMs;
70
+ while (groupExists(pid)) {
71
+ if (Date.now() >= giveUpAt)
72
+ return false;
73
+ await new Promise((resolve) => setTimeout(resolve, GROUP_POLL_INTERVAL_MS));
74
+ }
75
+ return true;
76
+ }
77
+ /**
78
+ * The one termination sequence for an owned Unix process group: group SIGTERM, wait the
79
+ * grace for the whole group to empty, then group SIGKILL and a bounded settle wait.
80
+ * Group liveness — not direct-child exit — decides completion, so escalation is never
81
+ * cancelled because the leader exited first while descendants retain pipes or locks.
82
+ * Resolves `true` only when a group signal was actually delivered.
83
+ */
84
+ async function reapProcessGroup(pid, graceMs) {
85
+ if (!signalGroup(pid, 'SIGTERM'))
86
+ return false;
87
+ if (await waitForGroupExit(pid, graceMs))
88
+ return true;
89
+ signalGroup(pid, 'SIGKILL');
90
+ await waitForGroupExit(pid, GROUP_KILL_PATIENCE_MS);
91
+ return true;
92
+ }
93
+ /**
94
+ * Own one detached child's process group on Unix. A deadline timer and the caller's abort
95
+ * signal both feed a single termination path — group SIGTERM, grace, SIGKILL — and after a
96
+ * natural leader exit surviving owned descendants are reaped with the same sequence, so
97
+ * completion cannot leak group members holding inherited pipes or database write locks.
98
+ */
99
+ export function ownProcessGroupLifecycle(ownership) {
100
+ const { pid, graceMs } = ownership;
101
+ let trigger;
102
+ let termination;
103
+ let timer;
104
+ const initiate = (reason) => {
105
+ if (trigger !== undefined)
106
+ return;
107
+ trigger = reason;
108
+ termination = reapProcessGroup(pid, graceMs);
109
+ };
110
+ if (ownership.deadlineMs !== undefined) {
111
+ timer = setTimeout(() => initiate('timeout'), ownership.deadlineMs);
112
+ }
113
+ const onAbort = () => initiate('cancelled');
114
+ if (ownership.signal !== undefined) {
115
+ if (ownership.signal.aborted)
116
+ onAbort();
117
+ else
118
+ ownership.signal.addEventListener('abort', onAbort, { once: true });
119
+ }
120
+ return {
121
+ finish: async () => {
122
+ if (trigger === undefined) {
123
+ // Natural completion: reap stragglers (no-op when the group is already empty).
124
+ await reapProcessGroup(pid, graceMs);
125
+ return undefined;
126
+ }
127
+ // An undeliverable trigger (group already gone — the child finished first)
128
+ // yields undefined so the outcome falls back to the natural classification.
129
+ return (await termination) ? trigger : undefined;
130
+ },
131
+ stop: () => {
132
+ if (timer !== undefined)
133
+ clearTimeout(timer);
134
+ ownership.signal?.removeEventListener('abort', onAbort);
135
+ },
136
+ };
137
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/ts-runtime",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
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.5.4",
61
+ "@gobing-ai/ts-utils": "^0.5.5",
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.5.4",
67
+ "@gobing-ai/ts-db": "^0.5.5",
68
68
  "@types/bun": "1.3.14"
69
69
  },
70
70
  "peerDependencies": {
71
- "@gobing-ai/ts-db": "^0.5.4"
71
+ "@gobing-ai/ts-db": "^0.5.5"
72
72
  },
73
73
  "peerDependenciesMeta": {
74
74
  "@gobing-ai/ts-db": {
package/src/index.ts CHANGED
@@ -55,13 +55,9 @@ export * from './types';
55
55
 
56
56
  // ── Deprecated re-exports (backward compatibility) ──────────────────────
57
57
 
58
- export { BunPipeProcessSpawner, BunSyncProcessExecutor } from './process-executor';
59
-
60
- /**
61
- * @deprecated Use {@link ProcessExecutor} directly for async execution.
62
- * Use `Bun.spawnSync` or `child_process.spawnSync` for sync.
63
- */
64
- export type SyncProcessExecutor = InstanceType<typeof import('./process-executor').BunSyncProcessExecutor>;
58
+ /** Sync executor shape (task 0087 R7) — satisfied by NodeSyncProcessExecutor (preferred) and the deprecated BunSyncProcessExecutor. */
59
+ export type { SyncProcessExecutor } from './process-executor';
60
+ export { BunPipeProcessSpawner, BunSyncProcessExecutor, NodeSyncProcessExecutor } from './process-executor';
65
61
 
66
62
  /**
67
63
  * @deprecated Use {@link ProcessExecutor.runStreaming} instead.
@@ -1,6 +1,13 @@
1
+ import { spawnSync } from 'node:child_process';
1
2
  import { isatty } from 'node:tty';
2
3
  import { type Options as ExecaOptions, execa } from 'execa';
3
4
  import { getProcessEnv } from './config';
5
+ import {
6
+ ownProcessGroupLifecycle,
7
+ type ProcessGroupOwnership,
8
+ resolveDeadline,
9
+ resolveKillGraceMs,
10
+ } from './process-group';
4
11
  import type { ProcessExecutionSource, ProcessRegistry } from './process-registry';
5
12
  import type { RuntimePaths } from './runtime-paths';
6
13
 
@@ -428,7 +435,8 @@ export class NodeProcessExecutor implements ProcessExecutor {
428
435
  */
429
436
  runStreaming(options: PipeProcessOptions): PipeProcess {
430
437
  const args = options.args ?? [];
431
- void this.config.tracer?.traceAsync('process.runStreaming', async () => undefined).catch(() => undefined);
438
+ // Task 0086 R14: no lifetime span here — the empty fire-and-forget span
439
+ // carried no data and confused span consumers. Registry + events cover it.
432
440
  const startedAt = Date.now();
433
441
  const startedIso = new Date(startedAt).toISOString();
434
442
  // Begin registry before spawn so failed spawns still appear (then complete as error).
@@ -680,7 +688,7 @@ export const ProcessExecutor = NodeProcessExecutor;
680
688
  * Synchronous process execution is no longer recommended from ts-runtime.
681
689
  * This class is kept for backward compatibility.
682
690
  */
683
- export class BunSyncProcessExecutor {
691
+ export class BunSyncProcessExecutor implements SyncProcessExecutor {
684
692
  runSync(options: Omit<ProcessOptions, 'timeout'>): ProcessResult {
685
693
  const args = options.args ?? [];
686
694
  const startedAt = Date.now();
@@ -710,6 +718,49 @@ export class BunSyncProcessExecutor {
710
718
  }
711
719
  }
712
720
 
721
+ /**
722
+ * Sync process execution surface shared by the sync executors (task 0087 R7).
723
+ * Not deprecated — the deprecated part is the Bun-specific default, not the shape.
724
+ */
725
+ export interface SyncProcessExecutor {
726
+ runSync(options: Omit<ProcessOptions, 'timeout'>): ProcessResult;
727
+ }
728
+
729
+ /**
730
+ * Sync executor on `node:child_process.spawnSync` — the non-deprecated default
731
+ * for sync callers (task 0087 R7). Contract-compatible with
732
+ * {@link BunSyncProcessExecutor} for success and non-zero exits. One deliberate
733
+ * divergence: a spawn failure (e.g. missing binary) maps to exitCode 1 instead of
734
+ * throwing, so `getGitContextSync`-style probes see a plain non-zero exit.
735
+ */
736
+ export class NodeSyncProcessExecutor implements SyncProcessExecutor {
737
+ runSync(options: Omit<ProcessOptions, 'timeout'>): ProcessResult {
738
+ const args = options.args ?? [];
739
+ const startedAt = Date.now();
740
+ const result = spawnSync(options.command, args, {
741
+ stdio: ['ignore', 'pipe', 'pipe'],
742
+ ...(options.cwd !== undefined ? { cwd: options.cwd } : {}),
743
+ ...(options.env !== undefined ? { env: options.env } : {}),
744
+ });
745
+ const exitCode = result.status ?? 1;
746
+ if (options.rejectOnError === true && exitCode !== 0) {
747
+ throw new Error(
748
+ `${options.command} ${args.join(' ')} failed with exit code ${exitCode}: ${stripFinalNewline(
749
+ asString(result.stderr),
750
+ )}`,
751
+ );
752
+ }
753
+ return {
754
+ command: options.command,
755
+ args,
756
+ exitCode,
757
+ stdout: stripFinalNewline(asString(result.stdout)),
758
+ stderr: stripFinalNewline(asString(result.stderr)),
759
+ durationMs: Date.now() - startedAt,
760
+ };
761
+ }
762
+ }
763
+
713
764
  /**
714
765
  * @deprecated Use {@link ProcessExecutor.runStreaming} instead.
715
766
  * This class is kept for backward compatibility.
@@ -769,165 +820,6 @@ function buildExecaOptions(opts: {
769
820
  }
770
821
 
771
822
  // ── 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
- },
928
- };
929
- }
930
-
931
823
  /** Classify an execa rejection on paths where the executor did not own the termination. */
932
824
  function classifyFailedCompletion(error: {
933
825
  timedOut?: boolean;
@@ -950,16 +842,23 @@ function observeOutput(
950
842
  observer: ProcessOptions['onOutput'],
951
843
  ): void {
952
844
  if (!stream || !observer) return;
953
- stream.on('data', (chunk: string | Uint8Array) => {
845
+ // Task 0086 R13: one decoder per stream with `stream: true` keeps multi-byte
846
+ // UTF-8 sequences intact across chunk boundaries; a fresh decoder per chunk
847
+ // would split them into U+FFFD replacement characters.
848
+ const decoder = new TextDecoder();
849
+ const emit = (chunk: string) => {
954
850
  try {
955
- observer({
956
- stream: name,
957
- chunk: asString(chunk),
958
- timestamp: new Date().toISOString(),
959
- });
851
+ observer({ stream: name, chunk, timestamp: new Date().toISOString() });
960
852
  } catch {
961
853
  // Observability is best-effort and must never interrupt child I/O.
962
854
  }
855
+ };
856
+ stream.on('data', (chunk: string | Uint8Array) => {
857
+ emit(typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true }));
858
+ });
859
+ stream.on('end', () => {
860
+ const tail = decoder.decode();
861
+ if (tail.length > 0) emit(tail);
963
862
  });
964
863
  }
965
864
 
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Executor-owned process-group containment (task 0087 R8): deadline resolution,
3
+ * group liveness probes, SIGTERM→SIGKILL escalation, and reap lifecycle for
4
+ * process groups spawned by NodeProcessExecutor. Extracted from
5
+ * process-executor.ts as a pure move — no behavior change.
6
+ */
7
+
8
+ import type { ProcessOutcome } from './process-executor';
9
+
10
+ /**
11
+ * Largest delay accepted by native timers; anything larger would overflow onto an
12
+ * immediate timer, so deadline/grace values beyond it are rejected before spawn.
13
+ */
14
+ const MAX_TIMEOUT_MS = 2_147_483_647;
15
+
16
+ /** Default grace between the owned group SIGTERM and the SIGKILL escalation. */
17
+ const DEFAULT_KILL_GRACE_MS = 5000;
18
+
19
+ /** Poll cadence while waiting for an owned process group to empty. */
20
+ const GROUP_POLL_INTERVAL_MS = 25;
21
+
22
+ /** Bounded patience after group SIGKILL before giving up on unkillable (D-state) members. */
23
+ const GROUP_KILL_PATIENCE_MS = 2000;
24
+
25
+ /**
26
+ * Resolve the effective deadline: omitted inherits the executor default (which may itself
27
+ * be unlimited), explicit `null` is unlimited and overrides any finite default, and a
28
+ * finite value must be a positive integer within the native timer range. Invalid values
29
+ * are rejected before spawn — never clamped and never overflowed onto a timer.
30
+ */
31
+ export function resolveDeadline(
32
+ optionTimeout: number | null | undefined,
33
+ defaultTimeout: number | null | undefined,
34
+ ): number | null | undefined {
35
+ if (optionTimeout === null) return null;
36
+ const candidate = optionTimeout === undefined ? defaultTimeout : optionTimeout;
37
+ if (candidate === undefined || candidate === null) return candidate;
38
+ if (!Number.isInteger(candidate) || candidate <= 0 || candidate > MAX_TIMEOUT_MS) {
39
+ throw new TypeError(
40
+ `Process timeout must be a positive integer within the native timer range (1-${MAX_TIMEOUT_MS} ms), got ${String(candidate)}`,
41
+ );
42
+ }
43
+ return candidate;
44
+ }
45
+
46
+ /**
47
+ * Resolve the SIGTERM→SIGKILL escalation grace: omitted uses {@link DEFAULT_KILL_GRACE_MS},
48
+ * `0` escalates immediately, and anything outside the native timer range is rejected.
49
+ */
50
+ export function resolveKillGraceMs(optionGraceMs: number | undefined): number {
51
+ if (optionGraceMs === undefined) return DEFAULT_KILL_GRACE_MS;
52
+ if (!Number.isInteger(optionGraceMs) || optionGraceMs < 0 || optionGraceMs > MAX_TIMEOUT_MS) {
53
+ throw new TypeError(
54
+ `Process killGraceMs must be a non-negative integer within the native timer range (0-${MAX_TIMEOUT_MS} ms), got ${String(optionGraceMs)}`,
55
+ );
56
+ }
57
+ return optionGraceMs;
58
+ }
59
+
60
+ /** Whether any member of the owned process group is still alive (signal 0 probe on `-pid`). */
61
+ function groupExists(pid: number): boolean {
62
+ try {
63
+ process.kill(-pid, 0);
64
+ return true;
65
+ } catch (error) {
66
+ // EPERM means the group exists but is not signallable from here.
67
+ return (error as NodeJS.ErrnoException).code === 'EPERM';
68
+ }
69
+ }
70
+
71
+ /** Signal the owned process group; `false` when the group is already gone (ESRCH). */
72
+ function signalGroup(pid: number, signal: NodeJS.Signals | number): boolean {
73
+ try {
74
+ process.kill(-pid, signal);
75
+ return true;
76
+ } catch {
77
+ return false;
78
+ }
79
+ }
80
+
81
+ async function waitForGroupExit(pid: number, budgetMs: number): Promise<boolean> {
82
+ const giveUpAt = Date.now() + budgetMs;
83
+ while (groupExists(pid)) {
84
+ if (Date.now() >= giveUpAt) return false;
85
+ await new Promise<void>((resolve) => setTimeout(resolve, GROUP_POLL_INTERVAL_MS));
86
+ }
87
+ return true;
88
+ }
89
+
90
+ /**
91
+ * The one termination sequence for an owned Unix process group: group SIGTERM, wait the
92
+ * grace for the whole group to empty, then group SIGKILL and a bounded settle wait.
93
+ * Group liveness — not direct-child exit — decides completion, so escalation is never
94
+ * cancelled because the leader exited first while descendants retain pipes or locks.
95
+ * Resolves `true` only when a group signal was actually delivered.
96
+ */
97
+ async function reapProcessGroup(pid: number, graceMs: number): Promise<boolean> {
98
+ if (!signalGroup(pid, 'SIGTERM')) return false;
99
+ if (await waitForGroupExit(pid, graceMs)) return true;
100
+ signalGroup(pid, 'SIGKILL');
101
+ await waitForGroupExit(pid, GROUP_KILL_PATIENCE_MS);
102
+ return true;
103
+ }
104
+
105
+ /** Handle for the executor-owned termination of one spawned process group. */
106
+ export interface ProcessGroupOwnership {
107
+ /**
108
+ * Settle the run's termination: resolve the outcome delivered by the executor's own
109
+ * path (deadline `timeout` or external `cancelled`) after the escalation completed, or
110
+ * reap surviving owned descendants after a natural leader exit (outcome stays `exit`).
111
+ * Call exactly once, after the execa promise has settled; bounded, never hangs.
112
+ */
113
+ finish(): Promise<ProcessOutcome | undefined>;
114
+ /** Release the deadline timer and abort listener once the execa promise has settled. */
115
+ stop(): void;
116
+ }
117
+
118
+ /**
119
+ * Own one detached child's process group on Unix. A deadline timer and the caller's abort
120
+ * signal both feed a single termination path — group SIGTERM, grace, SIGKILL — and after a
121
+ * natural leader exit surviving owned descendants are reaped with the same sequence, so
122
+ * completion cannot leak group members holding inherited pipes or database write locks.
123
+ */
124
+ export function ownProcessGroupLifecycle(ownership: {
125
+ pid: number;
126
+ deadlineMs: number | undefined;
127
+ signal: AbortSignal | undefined;
128
+ graceMs: number;
129
+ }): ProcessGroupOwnership {
130
+ const { pid, graceMs } = ownership;
131
+ let trigger: 'timeout' | 'cancelled' | undefined;
132
+ let termination: Promise<boolean> | undefined;
133
+ let timer: ReturnType<typeof setTimeout> | undefined;
134
+
135
+ const initiate = (reason: 'timeout' | 'cancelled'): void => {
136
+ if (trigger !== undefined) return;
137
+ trigger = reason;
138
+ termination = reapProcessGroup(pid, graceMs);
139
+ };
140
+
141
+ if (ownership.deadlineMs !== undefined) {
142
+ timer = setTimeout(() => initiate('timeout'), ownership.deadlineMs);
143
+ }
144
+ const onAbort = (): void => initiate('cancelled');
145
+ if (ownership.signal !== undefined) {
146
+ if (ownership.signal.aborted) onAbort();
147
+ else ownership.signal.addEventListener('abort', onAbort, { once: true });
148
+ }
149
+
150
+ return {
151
+ finish: async () => {
152
+ if (trigger === undefined) {
153
+ // Natural completion: reap stragglers (no-op when the group is already empty).
154
+ await reapProcessGroup(pid, graceMs);
155
+ return undefined;
156
+ }
157
+ // An undeliverable trigger (group already gone — the child finished first)
158
+ // yields undefined so the outcome falls back to the natural classification.
159
+ return (await termination) ? trigger : undefined;
160
+ },
161
+ stop: () => {
162
+ if (timer !== undefined) clearTimeout(timer);
163
+ ownership.signal?.removeEventListener('abort', onAbort);
164
+ },
165
+ };
166
+ }