@actiondock/core 2.20.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.
- package/dist/errors.d.ts +11 -1
- package/dist/errors.js +23 -0
- package/dist/execution/service.js +36 -5
- package/dist/host/host.d.ts +11 -0
- package/dist/host/host.js +258 -39
- package/dist/host/types.d.ts +10 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +5 -1
- package/dist/invocation/types.d.ts +31 -0
- package/dist/invocation/types.js +10 -1
- package/dist/package/index.d.ts +1 -1
- package/dist/package/runtime.js +18 -4
- package/dist/platform/node.d.ts +6 -0
- package/dist/platform/node.js +59 -16
- package/dist/platform/types.d.ts +11 -0
- package/dist/process/command-executor.d.ts +35 -0
- package/dist/process/command-executor.js +326 -0
- package/dist/process/index.d.ts +1 -0
- package/dist/process/index.js +1 -0
- package/dist/process/process-driver.d.ts +19 -6
- package/dist/process/process-driver.js +223 -54
- package/dist/process/run-executor.js +101 -47
- package/dist/project/dependencies.d.ts +92 -0
- package/dist/project/dependencies.js +543 -0
- package/dist/project/index.d.ts +3 -0
- package/dist/project/index.js +3 -0
- package/dist/project/init.js +2 -2
- package/dist/project/loader.js +15 -30
- package/dist/project/package-manager.d.ts +40 -0
- package/dist/project/package-manager.js +224 -0
- package/dist/project/preparation.d.ts +8 -0
- package/dist/project/preparation.js +130 -0
- package/dist/project/transactions.d.ts +5 -1
- package/dist/project/transactions.js +35 -75
- package/dist/runtime/context.d.ts +21 -2
- package/dist/runtime/context.js +42 -17
- package/dist/runtime/run-persistence.d.ts +1 -1
- package/dist/runtime/run-persistence.js +4 -3
- package/dist/security/config-cipher.d.ts +54 -0
- package/dist/security/config-cipher.js +190 -0
- package/dist/security/config-key.d.ts +35 -0
- package/dist/security/config-key.js +469 -0
- package/dist/server/routes/config.js +20 -1
- package/dist/service/factory.js +3 -0
- package/dist/service/types.d.ts +8 -0
- package/dist/storage/config-value.d.ts +33 -0
- package/dist/storage/config-value.js +102 -0
- package/dist/storage/file-lock.js +35 -21
- package/dist/storage/index.d.ts +24 -2
- package/dist/storage/index.js +65 -2
- package/dist/storage/migrations/index.d.ts +40 -0
- package/dist/storage/migrations/index.js +96 -0
- package/dist/storage/migrations/v2-to-v3.d.ts +16 -0
- package/dist/storage/migrations/v2-to-v3.js +116 -0
- package/dist/storage/sqlite-config.d.ts +62 -3
- package/dist/storage/sqlite-config.js +163 -9
- package/dist/storage/sqlite-driver.d.ts +5 -2
- package/dist/storage/sqlite-driver.js +24 -3
- package/dist/storage/sqlite-schema.d.ts +20 -2
- package/dist/storage/sqlite-schema.js +129 -132
- package/dist/storage/sqlite.d.ts +7 -2
- package/dist/storage/sqlite.js +64 -13
- package/dist/storage/types.d.ts +72 -4
- package/dist/storage/types.js +7 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- 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
|
-
*
|
|
555
|
-
*
|
|
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
|
|
655
|
+
if (!pid) {
|
|
564
656
|
return;
|
|
565
657
|
}
|
|
566
|
-
|
|
567
|
-
if (
|
|
568
|
-
|
|
569
|
-
await killProcessGroup(pid, "SIGKILL", this.spawnFn);
|
|
570
|
-
}
|
|
571
|
-
return;
|
|
658
|
+
// 若当前实例已有正在进行的活跃 terminate 流程,直接复用合并,杜绝并发竞争
|
|
659
|
+
if (instance.terminationPromise) {
|
|
660
|
+
return instance.terminationPromise;
|
|
572
661
|
}
|
|
573
|
-
|
|
662
|
+
// 若父进程已退出且输出已关闭,且进程树已无存活成员,直接无害返回(防止对历史 PID 发信号导致 PID 重用风险)
|
|
663
|
+
if (instance.exited && instance.outputClosed && !isProcessGroupAlive(pid)) {
|
|
574
664
|
return;
|
|
575
665
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
-
|
|
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
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
35
|
-
let
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
94
|
+
exitResult = exit;
|
|
95
|
+
wakeUpWaiters();
|
|
77
96
|
},
|
|
78
97
|
outputClosed: () => { },
|
|
79
98
|
fault: (err) => {
|
|
80
|
-
|
|
99
|
+
faultError = err;
|
|
100
|
+
triggerTerminate(0);
|
|
101
|
+
wakeUpWaiters();
|
|
81
102
|
},
|
|
82
103
|
};
|
|
83
|
-
|
|
104
|
+
driverHandle = await this.driver.spawn(effectiveSpec, observer);
|
|
105
|
+
// 检查派生期间是否已被标记取消
|
|
84
106
|
if (call?.signal?.aborted) {
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
|
|
116
|
-
|
|
127
|
+
triggerTerminate(1000);
|
|
128
|
+
wakeUpWaiters();
|
|
117
129
|
};
|
|
118
130
|
call.signal.addEventListener("abort", onAbort, { once: true });
|
|
119
131
|
}
|
|
120
132
|
try {
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|