workflow-ai 1.0.50 → 1.0.51
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/package.json +1 -1
- package/src/runner.mjs +132 -13
package/package.json
CHANGED
package/src/runner.mjs
CHANGED
|
@@ -8,43 +8,89 @@ import yaml from './lib/js-yaml.mjs';
|
|
|
8
8
|
import { findProjectRoot } from './lib/find-root.mjs';
|
|
9
9
|
|
|
10
10
|
// ============================================================================
|
|
11
|
-
// killProcessTree — убивает процесс и всех его потомков
|
|
12
|
-
// На Windows child.kill() не убивает дочерние процессы (npx → node → python)
|
|
13
|
-
//
|
|
11
|
+
// killProcessTree — убивает процесс и всех его потомков рекурсивно.
|
|
12
|
+
// На Windows child.kill() не убивает дочерние процессы (npx → node → python).
|
|
13
|
+
// taskkill /T /F не находит сирот (чей parent уже завершился).
|
|
14
|
+
// Поэтому: сначала собираем всё дерево PID через wmic, потом убиваем снизу вверх.
|
|
14
15
|
// ============================================================================
|
|
16
|
+
function getDescendantPids(pid) {
|
|
17
|
+
if (process.platform !== 'win32') return [];
|
|
18
|
+
try {
|
|
19
|
+
const output = execSync(
|
|
20
|
+
`wmic process where (ParentProcessId=${pid}) get ProcessId /FORMAT:LIST`,
|
|
21
|
+
{ stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf-8' }
|
|
22
|
+
);
|
|
23
|
+
const pids = [];
|
|
24
|
+
for (const line of output.split('\n')) {
|
|
25
|
+
const match = line.match(/ProcessId=(\d+)/);
|
|
26
|
+
if (match) {
|
|
27
|
+
const childPid = parseInt(match[1], 10);
|
|
28
|
+
pids.push(childPid);
|
|
29
|
+
// Рекурсивно собираем внуков
|
|
30
|
+
pids.push(...getDescendantPids(childPid));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return pids;
|
|
34
|
+
} catch {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
15
39
|
function killProcessTree(child, logger = null) {
|
|
16
40
|
const pid = child.pid;
|
|
17
41
|
if (!pid) return;
|
|
18
42
|
|
|
19
43
|
if (process.platform === 'win32') {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
44
|
+
// Собираем всех потомков ДО убийства (иначе дерево разорвётся)
|
|
45
|
+
const allPids = [...getDescendantPids(pid), pid];
|
|
46
|
+
|
|
47
|
+
// Убиваем снизу вверх (сначала внуки, потом дети, потом root)
|
|
48
|
+
for (const p of allPids) {
|
|
49
|
+
try {
|
|
50
|
+
execSync(`taskkill /pid ${p} /F`, { stdio: 'pipe' });
|
|
51
|
+
} catch {
|
|
52
|
+
// Процесс уже завершился
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (logger && allPids.length > 1) {
|
|
57
|
+
logger.info(`Killed process tree: ${allPids.join(' → ')}`, 'ProcessCleanup');
|
|
24
58
|
}
|
|
25
59
|
} else {
|
|
26
60
|
// Unix: отправляем SIGTERM, через 5 сек — SIGKILL если ещё жив
|
|
27
61
|
try {
|
|
28
62
|
child.kill('SIGTERM');
|
|
29
63
|
} catch {
|
|
30
|
-
return;
|
|
64
|
+
return;
|
|
31
65
|
}
|
|
32
66
|
|
|
33
67
|
setTimeout(() => {
|
|
34
68
|
try {
|
|
35
|
-
// Проверяем что процесс ещё жив (kill(0) не убивает, только проверяет)
|
|
36
69
|
process.kill(pid, 0);
|
|
37
70
|
child.kill('SIGKILL');
|
|
38
71
|
if (logger) {
|
|
39
72
|
logger.warn(`Process ${pid} did not exit after SIGTERM, sent SIGKILL`, 'ProcessCleanup');
|
|
40
73
|
}
|
|
41
74
|
} catch {
|
|
42
|
-
// Процесс уже завершился после SIGTERM
|
|
75
|
+
// Процесс уже завершился после SIGTERM
|
|
43
76
|
}
|
|
44
77
|
}, 5000);
|
|
45
78
|
}
|
|
46
79
|
}
|
|
47
80
|
|
|
81
|
+
// killPid — убивает один PID (для cleanup сирот)
|
|
82
|
+
function killPid(pid) {
|
|
83
|
+
try {
|
|
84
|
+
if (process.platform === 'win32') {
|
|
85
|
+
execSync(`taskkill /pid ${pid} /F`, { stdio: 'pipe' });
|
|
86
|
+
} else {
|
|
87
|
+
process.kill(pid, 'SIGKILL');
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
// уже завершился
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
48
94
|
// ============================================================================
|
|
49
95
|
// Logger — система логирования с уровнями DEBUG/INFO/WARN/ERROR
|
|
50
96
|
// ============================================================================
|
|
@@ -802,16 +848,59 @@ class StageExecutor {
|
|
|
802
848
|
|
|
803
849
|
// Трекинг активных дочерних процессов для cleanup
|
|
804
850
|
this.activeChildren = new Set();
|
|
851
|
+
// Все PID'ы потомков (включая внуков), собранные при жизни процесса
|
|
852
|
+
this.trackedPids = new Set();
|
|
853
|
+
// Интервал для периодического сбора PID-дерева
|
|
854
|
+
this._pidScanInterval = null;
|
|
805
855
|
}
|
|
806
856
|
|
|
807
857
|
/**
|
|
808
|
-
*
|
|
858
|
+
* Начинает периодический сбор PID'ов потомков для всех активных child processes.
|
|
859
|
+
* Нужно потому что при завершении промежуточного процесса (npx, cmd.exe)
|
|
860
|
+
* дерево разрывается и потомки становятся сиротами — их уже не найти через parent PID.
|
|
861
|
+
*/
|
|
862
|
+
_startPidTracking() {
|
|
863
|
+
if (this._pidScanInterval || process.platform !== 'win32') return;
|
|
864
|
+
this._pidScanInterval = setInterval(() => {
|
|
865
|
+
for (const child of this.activeChildren) {
|
|
866
|
+
if (child.pid) {
|
|
867
|
+
this.trackedPids.add(child.pid);
|
|
868
|
+
for (const pid of getDescendantPids(child.pid)) {
|
|
869
|
+
this.trackedPids.add(pid);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}, 3000);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Останавливает периодический сбор PID'ов
|
|
878
|
+
*/
|
|
879
|
+
_stopPidTracking() {
|
|
880
|
+
if (this._pidScanInterval) {
|
|
881
|
+
clearInterval(this._pidScanInterval);
|
|
882
|
+
this._pidScanInterval = null;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Убивает все активные дочерние процессы и все ранее найденные PID'ы потомков
|
|
809
888
|
*/
|
|
810
889
|
killAllChildren() {
|
|
890
|
+
this._stopPidTracking();
|
|
891
|
+
|
|
892
|
+
// Сначала убиваем известные child objects через killProcessTree
|
|
811
893
|
for (const child of this.activeChildren) {
|
|
812
894
|
killProcessTree(child, this.logger);
|
|
895
|
+
if (child.pid) this.trackedPids.delete(child.pid);
|
|
813
896
|
}
|
|
814
897
|
this.activeChildren.clear();
|
|
898
|
+
|
|
899
|
+
// Затем убиваем все ранее собранные PID'ы-сироты которые могли остаться
|
|
900
|
+
for (const pid of this.trackedPids) {
|
|
901
|
+
killPid(pid);
|
|
902
|
+
}
|
|
903
|
+
this.trackedPids.clear();
|
|
815
904
|
}
|
|
816
905
|
|
|
817
906
|
/**
|
|
@@ -957,6 +1046,8 @@ class StageExecutor {
|
|
|
957
1046
|
|
|
958
1047
|
// Регистрируем дочерний процесс для cleanup
|
|
959
1048
|
this.activeChildren.add(child);
|
|
1049
|
+
if (child.pid) this.trackedPids.add(child.pid);
|
|
1050
|
+
this._startPidTracking();
|
|
960
1051
|
|
|
961
1052
|
// Передаём промпт через stdin или закрываем если не нужно
|
|
962
1053
|
if (useStdin) {
|
|
@@ -1025,6 +1116,7 @@ class StageExecutor {
|
|
|
1025
1116
|
child.on('close', (code) => {
|
|
1026
1117
|
clearTimeout(timeoutId);
|
|
1027
1118
|
this.activeChildren.delete(child);
|
|
1119
|
+
if (this.activeChildren.size === 0) this._stopPidTracking();
|
|
1028
1120
|
// Обрабатываем остаток буфера стриминга
|
|
1029
1121
|
if (stdoutBuffer.trim()) {
|
|
1030
1122
|
try {
|
|
@@ -1215,6 +1307,8 @@ class PipelineRunner {
|
|
|
1215
1307
|
|
|
1216
1308
|
// Текущий executor для доступа к activeChildren при shutdown
|
|
1217
1309
|
this.currentExecutor = null;
|
|
1310
|
+
// Все PID'ы потомков всех executor'ов — для cleanup сирот при выходе
|
|
1311
|
+
this.allTrackedPids = new Set();
|
|
1218
1312
|
|
|
1219
1313
|
// Настройка graceful shutdown
|
|
1220
1314
|
this.setupGracefulShutdown();
|
|
@@ -1312,6 +1406,10 @@ class PipelineRunner {
|
|
|
1312
1406
|
} else {
|
|
1313
1407
|
this.currentExecutor = new StageExecutor(this.config, this.context, this.counters, {}, this.fileGuard, this.logger, this.projectRoot);
|
|
1314
1408
|
result = await this.currentExecutor.execute(this.currentStage);
|
|
1409
|
+
// Сохраняем tracked PIDs для cleanup сирот при выходе
|
|
1410
|
+
for (const pid of this.currentExecutor.trackedPids) {
|
|
1411
|
+
this.allTrackedPids.add(pid);
|
|
1412
|
+
}
|
|
1315
1413
|
this.currentExecutor = null;
|
|
1316
1414
|
}
|
|
1317
1415
|
|
|
@@ -1503,14 +1601,20 @@ class PipelineRunner {
|
|
|
1503
1601
|
if (this.currentExecutor) {
|
|
1504
1602
|
this.currentExecutor.killAllChildren();
|
|
1505
1603
|
}
|
|
1604
|
+
// Убиваем сирот от предыдущих stage'ей
|
|
1605
|
+
for (const pid of this.allTrackedPids) {
|
|
1606
|
+
killPid(pid);
|
|
1607
|
+
}
|
|
1608
|
+
this.allTrackedPids.clear();
|
|
1506
1609
|
};
|
|
1507
1610
|
|
|
1508
1611
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
1509
1612
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
1510
1613
|
|
|
1511
|
-
// Финальный cleanup при выходе процесса — синхронный, последний шанс убить детей
|
|
1614
|
+
// Финальный cleanup при выходе процесса — синхронный, последний шанс убить детей и сирот
|
|
1512
1615
|
process.on('exit', () => {
|
|
1513
|
-
|
|
1616
|
+
// Убиваем активные child objects (если shutdown во время работы stage)
|
|
1617
|
+
if (this.currentExecutor) {
|
|
1514
1618
|
for (const child of this.currentExecutor.activeChildren) {
|
|
1515
1619
|
if (child.pid) {
|
|
1516
1620
|
if (process.platform === 'win32') {
|
|
@@ -1520,6 +1624,21 @@ class PipelineRunner {
|
|
|
1520
1624
|
}
|
|
1521
1625
|
}
|
|
1522
1626
|
}
|
|
1627
|
+
// Добавляем PID'ы текущего executor'а
|
|
1628
|
+
for (const pid of this.currentExecutor.trackedPids) {
|
|
1629
|
+
this.allTrackedPids.add(pid);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
// Убиваем все ранее собранные PID'ы потомков (сироты от всех stage'ей)
|
|
1634
|
+
for (const pid of this.allTrackedPids) {
|
|
1635
|
+
try {
|
|
1636
|
+
if (process.platform === 'win32') {
|
|
1637
|
+
execSync(`taskkill /pid ${pid} /F`, { stdio: 'pipe' });
|
|
1638
|
+
} else {
|
|
1639
|
+
process.kill(pid, 'SIGKILL');
|
|
1640
|
+
}
|
|
1641
|
+
} catch {}
|
|
1523
1642
|
}
|
|
1524
1643
|
});
|
|
1525
1644
|
}
|