@actiondock/core 2.19.0 → 2.21.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.
Files changed (108) hide show
  1. package/dist/client/config.d.ts +17 -14
  2. package/dist/client/config.js +112 -14
  3. package/dist/client/index.d.ts +1 -1
  4. package/dist/client/index.js +1 -1
  5. package/dist/errors.d.ts +13 -1
  6. package/dist/errors.js +25 -0
  7. package/dist/execution/service.js +44 -10
  8. package/dist/execution/types.d.ts +3 -3
  9. package/dist/host/host.d.ts +20 -1
  10. package/dist/host/host.js +430 -66
  11. package/dist/host/types.d.ts +13 -1
  12. package/dist/index.d.ts +11 -3
  13. package/dist/index.js +5 -1
  14. package/dist/input/config-override.d.ts +5 -0
  15. package/dist/input/config-override.js +23 -0
  16. package/dist/input/input-resolver.d.ts +24 -7
  17. package/dist/input/input-resolver.js +73 -5
  18. package/dist/invocation/bindings.d.ts +53 -0
  19. package/dist/invocation/bindings.js +248 -0
  20. package/dist/invocation/policy.d.ts +5 -1
  21. package/dist/invocation/policy.js +16 -9
  22. package/dist/invocation/remote.d.ts +40 -0
  23. package/dist/invocation/remote.js +489 -0
  24. package/dist/invocation/types.d.ts +33 -0
  25. package/dist/invocation/types.js +11 -1
  26. package/dist/package/index.d.ts +2 -2
  27. package/dist/package/runtime.js +48 -15
  28. package/dist/platform/node.d.ts +6 -0
  29. package/dist/platform/node.js +59 -16
  30. package/dist/platform/types.d.ts +59 -1
  31. package/dist/process/command-executor.d.ts +35 -0
  32. package/dist/process/command-executor.js +326 -0
  33. package/dist/process/index.d.ts +1 -0
  34. package/dist/process/index.js +1 -0
  35. package/dist/process/process-driver.d.ts +19 -6
  36. package/dist/process/process-driver.js +223 -54
  37. package/dist/process/run-executor.js +101 -47
  38. package/dist/project/config-declarations.d.ts +61 -0
  39. package/dist/project/config-declarations.js +297 -0
  40. package/dist/project/dependencies.d.ts +92 -0
  41. package/dist/project/dependencies.js +543 -0
  42. package/dist/project/index.d.ts +8 -2
  43. package/dist/project/index.js +8 -2
  44. package/dist/project/init.js +2 -2
  45. package/dist/project/loader.js +19 -30
  46. package/dist/project/manifest.js +10 -0
  47. package/dist/project/package-manager.d.ts +40 -0
  48. package/dist/project/package-manager.js +224 -0
  49. package/dist/project/preparation.d.ts +8 -0
  50. package/dist/project/preparation.js +130 -0
  51. package/dist/project/transactions.d.ts +5 -1
  52. package/dist/project/transactions.js +35 -75
  53. package/dist/project/types.d.ts +30 -0
  54. package/dist/runtime/context.d.ts +27 -5
  55. package/dist/runtime/context.js +98 -26
  56. package/dist/runtime/env.js +15 -1
  57. package/dist/runtime/run-persistence.d.ts +1 -1
  58. package/dist/runtime/run-persistence.js +4 -3
  59. package/dist/runtime/standalone-commands.d.ts +2 -2
  60. package/dist/runtime/standalone-commands.js +113 -47
  61. package/dist/runtime/standalone-help.d.ts +1 -0
  62. package/dist/runtime/standalone-help.js +15 -9
  63. package/dist/runtime/standalone.js +2 -2
  64. package/dist/security/config-cipher.d.ts +54 -0
  65. package/dist/security/config-cipher.js +190 -0
  66. package/dist/security/config-key.d.ts +35 -0
  67. package/dist/security/config-key.js +469 -0
  68. package/dist/server/policy.d.ts +24 -107
  69. package/dist/server/policy.js +134 -291
  70. package/dist/server/routes/actions.js +10 -8
  71. package/dist/server/routes/common.d.ts +5 -20
  72. package/dist/server/routes/common.js +22 -58
  73. package/dist/server/routes/config.js +84 -7
  74. package/dist/server/routes/doctor.d.ts +1 -0
  75. package/dist/server/routes/doctor.js +4 -3
  76. package/dist/server/routes/info.js +100 -13
  77. package/dist/server/routes/playbooks.js +1 -3
  78. package/dist/server/routes/runs.js +96 -18
  79. package/dist/server/server.js +25 -53
  80. package/dist/server/types.d.ts +6 -6
  81. package/dist/service/factory.js +4 -0
  82. package/dist/service/remote.js +14 -29
  83. package/dist/service/types.d.ts +28 -5
  84. package/dist/service/types.js +8 -0
  85. package/dist/storage/config-value.d.ts +33 -0
  86. package/dist/storage/config-value.js +102 -0
  87. package/dist/storage/file-lock.js +35 -21
  88. package/dist/storage/index.d.ts +24 -2
  89. package/dist/storage/index.js +65 -2
  90. package/dist/storage/mask.d.ts +33 -6
  91. package/dist/storage/mask.js +298 -16
  92. package/dist/storage/migrations/index.d.ts +40 -0
  93. package/dist/storage/migrations/index.js +96 -0
  94. package/dist/storage/migrations/v2-to-v3.d.ts +16 -0
  95. package/dist/storage/migrations/v2-to-v3.js +116 -0
  96. package/dist/storage/sqlite-config.d.ts +62 -3
  97. package/dist/storage/sqlite-config.js +163 -9
  98. package/dist/storage/sqlite-driver.d.ts +5 -2
  99. package/dist/storage/sqlite-driver.js +24 -3
  100. package/dist/storage/sqlite-schema.d.ts +20 -2
  101. package/dist/storage/sqlite-schema.js +129 -132
  102. package/dist/storage/sqlite.d.ts +7 -2
  103. package/dist/storage/sqlite.js +64 -13
  104. package/dist/storage/types.d.ts +72 -4
  105. package/dist/storage/types.js +7 -2
  106. package/dist/version.d.ts +1 -1
  107. package/dist/version.js +1 -1
  108. package/package.json +2 -2
@@ -1,8 +1,33 @@
1
1
  import childProcess from "node:child_process";
2
2
  import { createRequire } from "node:module";
3
- import { ActionDockError, INPUT_CLOSED, PROCESS_NOT_FOUND, UNSUPPORTED_CAPABILITY, ProcessError, } from "../errors.js";
3
+ import { ActionDockError, INPUT_CLOSED, PROCESS_NOT_FOUND, PROCESS_TIMEOUT, UNSUPPORTED_CAPABILITY, ProcessError, } from "../errors.js";
4
4
  import { DEFAULT_ENV_ALLOWLIST, resolveProcessEnv, } from "./driver.js";
5
5
  export { DEFAULT_ENV_ALLOWLIST, resolveProcessEnv };
6
+ /**
7
+ * 跨平台探测进程组是否在操作系统中仍然存活。
8
+ *
9
+ * - 在 POSIX 环境下驱动创建了独立进程组,仅向负数进程标识 (-pid) 发送探测信号 0;
10
+ * 若返回 EPERM 视为仍存活;若返回 ESRCH 则表示进程组已彻底消亡;其他错误不静默当作消亡;
11
+ * 绝不对正 pid 探测,杜绝命中操作系统已重用 PID 的其他进程。
12
+ * - 在 Windows 环境下通过 process.kill(pid, 0) 探测父进程存活性。
13
+ *
14
+ * @param pid 目标进程标识
15
+ */
16
+ export function isProcessGroupAlive(pid) {
17
+ if (pid <= 0 || !Number.isInteger(pid))
18
+ return false;
19
+ try {
20
+ process.kill(process.platform === "win32" ? pid : -pid, 0);
21
+ return true;
22
+ }
23
+ catch (error) {
24
+ if (error?.code === "EPERM")
25
+ return true;
26
+ if (error?.code === "ESRCH")
27
+ return false;
28
+ throw error;
29
+ }
30
+ }
6
31
  /**
7
32
  * 跨平台终止进程组,确保不会遗留孤儿进程。
8
33
  *
@@ -149,10 +174,12 @@ function ptySignalToName(signal) {
149
174
  export class NodeProcessDriver {
150
175
  drainDeadlineMs;
151
176
  spawnFn;
177
+ waitForOutputClose;
152
178
  instances = new Map();
153
179
  constructor(options = {}) {
154
180
  this.drainDeadlineMs = options.drainDeadlineMs ?? 5000;
155
181
  this.spawnFn = options.spawnFn ?? childProcess.spawn;
182
+ this.waitForOutputClose = options.waitForOutputClose ?? false;
156
183
  }
157
184
  /**
158
185
  * 运行时能力集属性访问器。
@@ -235,6 +262,7 @@ export class NodeProcessDriver {
235
262
  throw faultError;
236
263
  }
237
264
  const exitListeners = [];
265
+ const closedListeners = [];
238
266
  const instance = {
239
267
  id: handleId,
240
268
  pid: ptyProcess.pid,
@@ -245,6 +273,7 @@ export class NodeProcessDriver {
245
273
  exited: false,
246
274
  outputClosed: false,
247
275
  exitListeners,
276
+ closedListeners,
248
277
  onExit(cb) {
249
278
  if (instance.exited) {
250
279
  cb();
@@ -253,19 +282,33 @@ export class NodeProcessDriver {
253
282
  exitListeners.push(cb);
254
283
  }
255
284
  },
285
+ onClosed(cb) {
286
+ if (instance.outputClosed) {
287
+ cb();
288
+ }
289
+ else {
290
+ closedListeners.push(cb);
291
+ }
292
+ },
256
293
  cleanup() {
257
294
  if (instance.drainTimer) {
258
295
  clearTimeout(instance.drainTimer);
259
296
  instance.drainTimer = undefined;
260
297
  }
261
298
  exitListeners.length = 0;
299
+ closedListeners.length = 0;
262
300
  },
263
301
  selfRelease: () => {
264
302
  // 终态收敛后自移除:按实例身份判断,dispose 先行移除或表内已换新实例时不重复删除
303
+ // 活跃 terminate 过程中不能因为父 close 提前自移除而绕过清理
304
+ if (instance.terminationPromise) {
305
+ return;
306
+ }
265
307
  if (this.instances.get(handleId) === instance) {
266
308
  this.instances.delete(handleId);
267
309
  }
268
310
  },
311
+ markOutputClosed: (reason) => markOutputClosed(reason),
269
312
  };
270
313
  const markExited = (code, signal) => {
271
314
  if (!instance.exited) {
@@ -284,12 +327,20 @@ export class NodeProcessDriver {
284
327
  }
285
328
  if (!instance.outputClosed) {
286
329
  instance.outputClosed = true;
330
+ instance.outputClosedReason = reason;
287
331
  observer.outputClosed(reason);
332
+ for (const listener of closedListeners) {
333
+ listener();
334
+ }
335
+ }
336
+ if (instance.outputClosedReason === "natural" && !instance.terminationPromise) {
337
+ instance.selfRelease();
288
338
  }
289
- instance.selfRelease();
290
339
  };
291
340
  // 绑定 PTY 数据输出
292
341
  ptyProcess.onData((data) => {
342
+ if (instance.outputClosed)
343
+ return;
293
344
  observer.output("pty", new TextEncoder().encode(data));
294
345
  });
295
346
  ptyProcess.onExit((e) => {
@@ -324,6 +375,7 @@ export class NodeProcessDriver {
324
375
  throw faultError;
325
376
  }
326
377
  const exitListeners = [];
378
+ const closedListeners = [];
327
379
  const instance = {
328
380
  id: handleId,
329
381
  pid: child.pid,
@@ -333,6 +385,7 @@ export class NodeProcessDriver {
333
385
  exited: false,
334
386
  outputClosed: false,
335
387
  exitListeners,
388
+ closedListeners,
336
389
  onExit(cb) {
337
390
  if (instance.exited) {
338
391
  cb();
@@ -341,19 +394,33 @@ export class NodeProcessDriver {
341
394
  exitListeners.push(cb);
342
395
  }
343
396
  },
397
+ onClosed(cb) {
398
+ if (instance.outputClosed) {
399
+ cb();
400
+ }
401
+ else {
402
+ closedListeners.push(cb);
403
+ }
404
+ },
344
405
  cleanup() {
345
406
  if (instance.drainTimer) {
346
407
  clearTimeout(instance.drainTimer);
347
408
  instance.drainTimer = undefined;
348
409
  }
349
410
  exitListeners.length = 0;
411
+ closedListeners.length = 0;
350
412
  },
351
413
  selfRelease: () => {
352
414
  // 终态收敛后自移除:按实例身份判断,dispose 先行移除或表内已换新实例时不重复删除
415
+ // 活跃 terminate 过程中不能因为父 close 提前自移除而绕过清理
416
+ if (instance.terminationPromise) {
417
+ return;
418
+ }
353
419
  if (this.instances.get(handleId) === instance) {
354
420
  this.instances.delete(handleId);
355
421
  }
356
422
  },
423
+ markOutputClosed: (reason) => markOutputClosed(reason),
357
424
  };
358
425
  const markExited = (code, signal) => {
359
426
  if (!instance.exited) {
@@ -372,18 +439,42 @@ export class NodeProcessDriver {
372
439
  }
373
440
  if (!instance.outputClosed) {
374
441
  instance.outputClosed = true;
442
+ instance.outputClosedReason = reason;
443
+ if (reason !== "natural") {
444
+ try {
445
+ child.stdout?.destroy();
446
+ }
447
+ catch { }
448
+ try {
449
+ child.stderr?.destroy();
450
+ }
451
+ catch { }
452
+ try {
453
+ child.stdin?.destroy();
454
+ }
455
+ catch { }
456
+ }
375
457
  observer.outputClosed(reason);
458
+ for (const listener of closedListeners) {
459
+ listener();
460
+ }
461
+ }
462
+ if (instance.outputClosedReason === "natural" && !instance.terminationPromise) {
463
+ instance.selfRelease();
376
464
  }
377
- instance.selfRelease();
378
465
  };
379
466
  // 同步完成 stdout/stderr 监听绑定,防止产生数据丢失竞态
380
467
  if (child.stdout) {
381
468
  child.stdout.on("data", (chunk) => {
469
+ if (instance.outputClosed)
470
+ return;
382
471
  observer.output("stdout", new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength));
383
472
  });
384
473
  }
385
474
  if (child.stderr) {
386
475
  child.stderr.on("data", (chunk) => {
476
+ if (instance.outputClosed)
477
+ return;
387
478
  observer.output("stderr", new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength));
388
479
  });
389
480
  }
@@ -395,7 +486,7 @@ export class NodeProcessDriver {
395
486
  }
396
487
  child.on("exit", (code, signal) => {
397
488
  markExited(code, signal);
398
- if (!instance.outputClosed && !instance.drainTimer) {
489
+ if (!this.waitForOutputClose && !instance.terminationPromise && !instance.outputClosed && !instance.drainTimer) {
399
490
  instance.drainTimer = setTimeout(() => {
400
491
  markOutputClosed("drain-timeout");
401
492
  }, this.drainDeadlineMs);
@@ -551,8 +642,9 @@ export class NodeProcessDriver {
551
642
  instance.ptyProcess.resize(cols, rows);
552
643
  }
553
644
  /**
554
- * 优雅终止进程,超时未退出则发送 SIGKILL 强杀兜底。
555
- * 终态自清理(selfRelease)或 dispose 先行移除实例时视为已终止,直接无害返回。
645
+ * 优雅终止进程整棵进程树,超时未退出则发送 SIGKILL 强杀兜底。
646
+ * 修复父进程先退出导致的短路,确保整组存活成员(包括忽略 SIGTERM 的孙进程及未闭合输出的活实例)被彻底终止,
647
+ * 并等待父进程与输出收敛。
556
648
  */
557
649
  async terminate(handle, graceMs) {
558
650
  const instance = this.instances.get(handle.id);
@@ -560,72 +652,149 @@ export class NodeProcessDriver {
560
652
  return;
561
653
  }
562
654
  const pid = instance.pid;
563
- if (!pid || instance.exited) {
655
+ if (!pid) {
564
656
  return;
565
657
  }
566
- await killProcessGroup(pid, "SIGTERM", this.spawnFn);
567
- if (graceMs <= 0) {
568
- if (!instance.exited) {
569
- await killProcessGroup(pid, "SIGKILL", this.spawnFn);
570
- }
571
- return;
658
+ // 若当前实例已有正在进行的活跃 terminate 流程,直接复用合并,杜绝并发竞争
659
+ if (instance.terminationPromise) {
660
+ return instance.terminationPromise;
572
661
  }
573
- if (instance.exited) {
662
+ // 若父进程已退出且输出已关闭,且进程树已无存活成员,直接无害返回(防止对历史 PID 发信号导致 PID 重用风险)
663
+ if (instance.exited && instance.outputClosed && !isProcessGroupAlive(pid)) {
574
664
  return;
575
665
  }
576
- await new Promise((resolve) => {
577
- let timer;
578
- let settled = false;
579
- let exitFinish;
580
- const finish = () => {
581
- if (settled) {
582
- return;
583
- }
584
- settled = true;
585
- // 完成后自注销本次注册的 exit 监听器,避免同一实例反复 terminate 时累积监听器
586
- if (exitFinish) {
587
- const idx = instance.exitListeners.indexOf(exitFinish);
588
- if (idx >= 0) {
589
- instance.exitListeners.splice(idx, 1);
666
+ const terminationPromise = Promise.resolve().then(async () => {
667
+ try {
668
+ const isSettled = () => instance.exited && instance.outputClosed;
669
+ await killProcessGroup(pid, "SIGTERM", this.spawnFn);
670
+ if (graceMs <= 0) {
671
+ if (!isSettled() || isProcessGroupAlive(pid)) {
672
+ await killProcessGroup(pid, "SIGKILL", this.spawnFn);
590
673
  }
591
674
  }
592
- if (timer)
593
- clearTimeout(timer);
594
- resolve();
595
- };
596
- exitFinish = finish;
597
- instance.onExit(finish);
598
- timer = setTimeout(async () => {
599
- if (!instance.exited) {
600
- try {
601
- await killProcessGroup(pid, "SIGKILL", this.spawnFn);
675
+ else {
676
+ if (!isSettled() || isProcessGroupAlive(pid)) {
677
+ await new Promise((resolve) => {
678
+ let timer;
679
+ let pollTimer;
680
+ let settled = false;
681
+ let exitListener;
682
+ let closeListener;
683
+ const cleanup = () => {
684
+ if (settled) {
685
+ return;
686
+ }
687
+ settled = true;
688
+ if (exitListener) {
689
+ const idx = instance.exitListeners.indexOf(exitListener);
690
+ if (idx >= 0) {
691
+ instance.exitListeners.splice(idx, 1);
692
+ }
693
+ }
694
+ if (closeListener) {
695
+ const idx = instance.closedListeners.indexOf(closeListener);
696
+ if (idx >= 0) {
697
+ instance.closedListeners.splice(idx, 1);
698
+ }
699
+ }
700
+ if (timer)
701
+ clearTimeout(timer);
702
+ if (pollTimer)
703
+ clearInterval(pollTimer);
704
+ resolve();
705
+ };
706
+ const check = () => {
707
+ if (isSettled() && !isProcessGroupAlive(pid)) {
708
+ cleanup();
709
+ }
710
+ };
711
+ exitListener = check;
712
+ closeListener = check;
713
+ pollTimer = setInterval(check, 20);
714
+ timer = setTimeout(cleanup, graceMs);
715
+ instance.onExit(exitListener);
716
+ instance.onClosed(closeListener);
717
+ });
718
+ if (!isSettled() || isProcessGroupAlive(pid)) {
719
+ await killProcessGroup(pid, "SIGKILL", this.spawnFn);
720
+ }
602
721
  }
603
- catch {
604
- // 忽略终止失败异常
722
+ }
723
+ // 第二阶段:强杀后等待父进程退出与输出收敛
724
+ if (!isSettled()) {
725
+ if (instance.drainTimer) {
726
+ clearTimeout(instance.drainTimer);
727
+ instance.drainTimer = undefined;
605
728
  }
729
+ await new Promise((resolve, reject) => {
730
+ let timer;
731
+ let settled = false;
732
+ let exitListener;
733
+ let closeListener;
734
+ const cleanup = () => {
735
+ if (settled)
736
+ return;
737
+ settled = true;
738
+ if (exitListener) {
739
+ const idx = instance.exitListeners.indexOf(exitListener);
740
+ if (idx >= 0) {
741
+ instance.exitListeners.splice(idx, 1);
742
+ }
743
+ }
744
+ if (closeListener) {
745
+ const idx = instance.closedListeners.indexOf(closeListener);
746
+ if (idx >= 0) {
747
+ instance.closedListeners.splice(idx, 1);
748
+ }
749
+ }
750
+ if (timer)
751
+ clearTimeout(timer);
752
+ };
753
+ const check = () => {
754
+ if (isSettled()) {
755
+ cleanup();
756
+ resolve();
757
+ }
758
+ };
759
+ exitListener = check;
760
+ closeListener = check;
761
+ instance.onExit(exitListener);
762
+ instance.onClosed(closeListener);
763
+ timer = setTimeout(() => {
764
+ cleanup();
765
+ if (!isSettled()) {
766
+ if (!instance.outputClosed) {
767
+ instance.markOutputClosed("drain-timeout");
768
+ }
769
+ reject(new ProcessError(PROCESS_TIMEOUT, `Process failed to exit and close output within ${this.drainDeadlineMs}ms after termination: handle id ${instance.id}`));
770
+ }
771
+ else {
772
+ resolve();
773
+ }
774
+ }, this.drainDeadlineMs);
775
+ });
606
776
  }
607
- finish();
608
- }, graceMs);
609
- timer.unref?.();
777
+ }
778
+ finally {
779
+ instance.terminationPromise = undefined;
780
+ if (instance.outputClosed) {
781
+ instance.selfRelease();
782
+ }
783
+ }
610
784
  });
785
+ instance.terminationPromise = terminationPromise;
786
+ return terminationPromise;
611
787
  }
612
788
  /**
613
789
  * 销毁进程句柄并清理相关资源。
614
- * 对仍在运行的实例先执行 terminate(0) 兜底(SIGTERM 后立即 SIGKILL),
615
- * 绝不从受管表移除仍存活的子进程而遗留孤儿;已退出实例直接清理。
616
- * 终态自清理(selfRelease)与 dispose 可能并发到达,二者均按实例身份判断,
617
- * 后到方对已删除条目为无害空操作。
790
+ * 对仍在运行或进程树存活或输出未闭合的实例先执行 terminate(0) 兜底(SIGTERM 后立即 SIGKILL),
791
+ * 绝不从受管表移除仍存活的子进程而遗留孤儿;已退出且输出闭合实例直接清理。
618
792
  */
619
793
  async dispose(handle) {
620
794
  const instance = this.instances.get(handle.id);
621
795
  if (instance) {
622
- if (!instance.exited) {
623
- try {
624
- await this.terminate(handle, 0);
625
- }
626
- catch {
627
- // 兜底终止失败不阻断句柄清理,退出事件仍会经监听链路上报
628
- }
796
+ if (!instance.exited || !instance.outputClosed || isProcessGroupAlive(instance.pid ?? 0)) {
797
+ await this.terminate(handle, 0);
629
798
  }
630
799
  instance.cleanup();
631
800
  if (this.instances.get(handle.id) === instance) {
@@ -1,5 +1,5 @@
1
1
  import { encodeBytes, } from "@actiondock/sdk";
2
- import { PROCESS_CANCELLED, PROCESS_TIMEOUT, UNSUPPORTED_CAPABILITY, ProcessError, } from "../errors.js";
2
+ import { PROCESS_CANCELLED, PROCESS_TIMEOUT, PROCESS_SPAWN_ERROR, UNSUPPORTED_CAPABILITY, ProcessError, } from "../errors.js";
3
3
  /**
4
4
  * 一次性 run 执行器。
5
5
  *
@@ -19,6 +19,12 @@ export class RunExecutor {
19
19
  if (ioMode !== "pipe") {
20
20
  throw new ProcessError(UNSUPPORTED_CAPABILITY, "Run requires IO mode to be 'pipe'");
21
21
  }
22
+ // 对齐 start 防御顺序:派生前检查取消信号,已中止直接抛出取消错误不再派生进程
23
+ if (call?.signal?.aborted) {
24
+ throw call.signal.reason instanceof ProcessError
25
+ ? call.signal.reason
26
+ : new ProcessError(PROCESS_CANCELLED, "Process run was cancelled");
27
+ }
22
28
  const effectiveSpec = {
23
29
  ...input.spec,
24
30
  io: {
@@ -31,13 +37,27 @@ export class RunExecutor {
31
37
  let truncated = false;
32
38
  let timedOut = false;
33
39
  let cancelled = false;
34
- let resolveExit;
35
- let rejectError;
36
- const exitPromise = new Promise((resolve, reject) => {
37
- resolveExit = resolve;
38
- rejectError = reject;
39
- });
40
+ let faultError;
41
+ let exitResult;
40
42
  let driverHandle;
43
+ let terminationOutcomePromise;
44
+ let requestedGraceMs;
45
+ // 唤醒统一等待链路的信号
46
+ let wakeUpWaiters;
47
+ const wakeUpPromise = new Promise((resolve) => {
48
+ wakeUpWaiters = resolve;
49
+ });
50
+ const triggerTerminate = (graceMs) => {
51
+ requestedGraceMs = Math.min(requestedGraceMs ?? graceMs, graceMs);
52
+ // 同步驱动可在 spawn 返回句柄前上报超限或故障,不能缓存一次空的清理结果。
53
+ if (!driverHandle)
54
+ return Promise.resolve({ ok: true });
55
+ if (!terminationOutcomePromise) {
56
+ const rawPromise = Promise.resolve().then(() => this.driver.terminate(driverHandle, requestedGraceMs));
57
+ terminationOutcomePromise = rawPromise.then(() => ({ ok: true }), (error) => ({ ok: false, error }));
58
+ }
59
+ return terminationOutcomePromise;
60
+ };
41
61
  const observer = {
42
62
  output: (stream, data) => {
43
63
  if (truncated)
@@ -47,9 +67,8 @@ export class RunExecutor {
47
67
  const remaining = input.maxOutputBytes - totalBytes;
48
68
  if (remaining <= 0) {
49
69
  truncated = true;
50
- if (driverHandle) {
51
- void this.driver.terminate(driverHandle, 1000);
52
- }
70
+ triggerTerminate(1000);
71
+ wakeUpWaiters();
53
72
  return;
54
73
  }
55
74
  if (data.byteLength > remaining) {
@@ -60,9 +79,8 @@ export class RunExecutor {
60
79
  });
61
80
  totalBytes += slice.byteLength;
62
81
  truncated = true;
63
- if (driverHandle) {
64
- void this.driver.terminate(driverHandle, 1000);
65
- }
82
+ triggerTerminate(1000);
83
+ wakeUpWaiters();
66
84
  }
67
85
  else {
68
86
  chunks.push({
@@ -73,82 +91,118 @@ export class RunExecutor {
73
91
  }
74
92
  },
75
93
  exited: (exit) => {
76
- resolveExit(exit);
94
+ exitResult = exit;
95
+ wakeUpWaiters();
77
96
  },
78
97
  outputClosed: () => { },
79
98
  fault: (err) => {
80
- rejectError(err);
99
+ faultError = err;
100
+ triggerTerminate(0);
101
+ wakeUpWaiters();
81
102
  },
82
103
  };
83
- // 对齐 start 防御顺序:派生前检查取消信号,已中止直接抛出取消错误不再派生进程
104
+ driverHandle = await this.driver.spawn(effectiveSpec, observer);
105
+ // 检查派生期间是否已被标记取消
84
106
  if (call?.signal?.aborted) {
85
- throw call.signal.reason instanceof ProcessError
86
- ? call.signal.reason
87
- : new ProcessError(PROCESS_CANCELLED, "Process run was cancelled");
107
+ cancelled = true;
108
+ triggerTerminate(1000);
88
109
  }
89
- driverHandle = await this.driver.spawn(effectiveSpec, observer);
90
110
  let timeoutTimer;
91
- if (input.timeoutMs > 0) {
111
+ if (input.timeoutMs > 0 && !cancelled) {
92
112
  timeoutTimer = setTimeout(() => {
93
113
  timedOut = true;
94
- if (driverHandle) {
95
- void this.driver.terminate(driverHandle, 1000);
96
- }
114
+ triggerTerminate(1000);
115
+ wakeUpWaiters();
97
116
  }, input.timeoutMs);
98
117
  if (typeof timeoutTimer?.unref === "function") {
99
118
  timeoutTimer.unref();
100
119
  }
101
120
  }
102
121
  let onAbort;
103
- if (call?.signal) {
104
- if (call.signal.aborted) {
105
- if (timeoutTimer)
106
- clearTimeout(timeoutTimer);
107
- if (driverHandle)
108
- void this.driver.terminate(driverHandle, 1000);
109
- throw call.signal.reason ?? new ProcessError(PROCESS_CANCELLED, "Process run was cancelled");
110
- }
122
+ if (call?.signal && !cancelled) {
111
123
  onAbort = () => {
112
124
  cancelled = true;
113
125
  if (timeoutTimer)
114
126
  clearTimeout(timeoutTimer);
115
- if (driverHandle)
116
- void this.driver.terminate(driverHandle, 1000);
127
+ triggerTerminate(1000);
128
+ wakeUpWaiters();
117
129
  };
118
130
  call.signal.addEventListener("abort", onAbort, { once: true });
119
131
  }
120
132
  try {
121
- const exit = await exitPromise;
133
+ // 统一等待唤醒:exit、fault、timeout、cancel、output-limit 均触发 wakeUpPromise
134
+ while (!exitResult && !faultError && !timedOut && !cancelled && !truncated) {
135
+ await wakeUpPromise;
136
+ }
137
+ // 等待终结后清理定时器与事件监听
122
138
  if (timeoutTimer)
123
139
  clearTimeout(timeoutTimer);
124
140
  if (call?.signal && onAbort) {
125
141
  call.signal.removeEventListener("abort", onAbort);
126
142
  }
143
+ // 若启动了终止(超时、取消、输出超限、故障),统一等待单次终止排空结果
144
+ let outcome = { ok: true };
145
+ if (terminationOutcomePromise || timedOut || cancelled || truncated || faultError) {
146
+ outcome = await triggerTerminate(1000);
147
+ }
148
+ // 统一分类主错误及清理 cause
149
+ if (faultError) {
150
+ if (!outcome.ok && !faultError.cause) {
151
+ faultError.cause = outcome.error;
152
+ }
153
+ throw faultError;
154
+ }
127
155
  if (timedOut) {
128
- throw new ProcessError(PROCESS_TIMEOUT, `Process exceeded timeout of ${input.timeoutMs}ms`);
156
+ const err = new ProcessError(PROCESS_TIMEOUT, `Process exceeded timeout of ${input.timeoutMs}ms`, !outcome.ok ? { cause: outcome.error } : undefined);
157
+ if (!outcome.ok) {
158
+ err.cause = outcome.error;
159
+ }
160
+ throw err;
129
161
  }
130
162
  if (cancelled || call?.signal?.aborted) {
131
- throw call?.signal?.reason ?? new ProcessError(PROCESS_CANCELLED, "Process run was cancelled");
163
+ const baseReason = call?.signal?.reason;
164
+ if (baseReason !== undefined && outcome.ok)
165
+ throw baseReason;
166
+ if (baseReason instanceof Error) {
167
+ if (!outcome.ok && !baseReason.cause) {
168
+ baseReason.cause = outcome.error;
169
+ }
170
+ throw baseReason;
171
+ }
172
+ const err = new ProcessError(PROCESS_CANCELLED, "Process run was cancelled", !outcome.ok ? { cause: outcome.error, reason: baseReason } : undefined);
173
+ if (!outcome.ok) {
174
+ err.cause = outcome.error;
175
+ }
176
+ throw err;
177
+ }
178
+ if (truncated) {
179
+ if (!outcome.ok) {
180
+ // 输出限额触发的强杀清理若发生异常,必须抛出清理错误避免静默吞错
181
+ throw outcome.error instanceof Error
182
+ ? outcome.error
183
+ : new ProcessError(PROCESS_TIMEOUT, String(outcome.error));
184
+ }
185
+ if (!exitResult) {
186
+ throw new ProcessError(PROCESS_SPAWN_ERROR, "Terminated process did not report an exit result");
187
+ }
188
+ return {
189
+ exit: exitResult,
190
+ chunks,
191
+ truncated: true,
192
+ };
132
193
  }
133
194
  return {
134
- exit,
195
+ exit: exitResult,
135
196
  chunks,
136
- truncated,
197
+ truncated: false,
137
198
  };
138
199
  }
139
- catch (err) {
200
+ finally {
140
201
  if (timeoutTimer)
141
202
  clearTimeout(timeoutTimer);
142
203
  if (call?.signal && onAbort) {
143
204
  call.signal.removeEventListener("abort", onAbort);
144
205
  }
145
- if (timedOut) {
146
- throw new ProcessError(PROCESS_TIMEOUT, `Process exceeded timeout of ${input.timeoutMs}ms`);
147
- }
148
- if (cancelled || call?.signal?.aborted) {
149
- throw call?.signal?.reason ?? new ProcessError(PROCESS_CANCELLED, "Process run was cancelled");
150
- }
151
- throw err;
152
206
  }
153
207
  }
154
208
  }