workflow-ai 1.0.52 → 1.0.53
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 +97 -8
package/package.json
CHANGED
package/src/runner.mjs
CHANGED
|
@@ -8,15 +8,23 @@ import yaml from './lib/js-yaml.mjs';
|
|
|
8
8
|
import { findProjectRoot } from './lib/find-root.mjs';
|
|
9
9
|
|
|
10
10
|
// ============================================================================
|
|
11
|
-
// ProcessTracker —
|
|
11
|
+
// ProcessTracker — файловый трекинг дочерних процессов.
|
|
12
12
|
//
|
|
13
13
|
// Проблема: runner spawn → claude CLI → npx → node (MCP-сервер).
|
|
14
|
-
// При завершении
|
|
15
|
-
//
|
|
14
|
+
// При жёстком завершении runner'а (kill, закрытие терминала) process.on('exit')
|
|
15
|
+
// не срабатывает, MCP-серверы остаются сиротами навсегда.
|
|
16
16
|
//
|
|
17
|
-
// Решение:
|
|
18
|
-
//
|
|
17
|
+
// Решение: PID-файл (.workflow/logs/.runner-pids).
|
|
18
|
+
// - Перед spawn: записываем снапшот существующих PID'ов в файл
|
|
19
|
+
// - При старте runner'а: читаем файл, вычисляем сирот (текущие − снапшот), убиваем
|
|
20
|
+
// - Файл переживает крэш/kill — cleanup гарантирован при следующем запуске
|
|
19
21
|
// ============================================================================
|
|
22
|
+
const PIDFILE_NAME = '.runner-pids';
|
|
23
|
+
|
|
24
|
+
function getPidFilePath(projectRoot) {
|
|
25
|
+
return path.resolve(projectRoot, '.workflow/logs', PIDFILE_NAME);
|
|
26
|
+
}
|
|
27
|
+
|
|
20
28
|
function getProcessSnapshot() {
|
|
21
29
|
if (process.platform !== 'win32') return new Set();
|
|
22
30
|
try {
|
|
@@ -35,6 +43,80 @@ function getProcessSnapshot() {
|
|
|
35
43
|
}
|
|
36
44
|
}
|
|
37
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Записывает снапшот + runner PID в файл.
|
|
48
|
+
* Формат: JSON { runnerPid, snapshot: [...], timestamp }
|
|
49
|
+
*/
|
|
50
|
+
function writePidFile(pidFilePath, snapshot) {
|
|
51
|
+
try {
|
|
52
|
+
const dir = path.dirname(pidFilePath);
|
|
53
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
54
|
+
fs.writeFileSync(pidFilePath, JSON.stringify({
|
|
55
|
+
runnerPid: process.pid,
|
|
56
|
+
snapshot: [...snapshot],
|
|
57
|
+
timestamp: new Date().toISOString()
|
|
58
|
+
}));
|
|
59
|
+
} catch { /* non-critical */ }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Читает PID-файл. Возвращает { runnerPid, snapshot } или null.
|
|
64
|
+
*/
|
|
65
|
+
function readPidFile(pidFilePath) {
|
|
66
|
+
try {
|
|
67
|
+
if (!fs.existsSync(pidFilePath)) return null;
|
|
68
|
+
const data = JSON.parse(fs.readFileSync(pidFilePath, 'utf-8'));
|
|
69
|
+
return {
|
|
70
|
+
runnerPid: data.runnerPid,
|
|
71
|
+
snapshot: new Set(data.snapshot || [])
|
|
72
|
+
};
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function removePidFile(pidFilePath) {
|
|
79
|
+
try { fs.unlinkSync(pidFilePath); } catch { /* ok */ }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Cleanup сирот от предыдущего запуска.
|
|
84
|
+
* Читает PID-файл, берёт текущие процессы, убивает разницу (текущие − снапшот).
|
|
85
|
+
* Пропускает собственный PID runner'а.
|
|
86
|
+
*/
|
|
87
|
+
function cleanupOrphansFromFile(pidFilePath, logger = null) {
|
|
88
|
+
const saved = readPidFile(pidFilePath);
|
|
89
|
+
if (!saved) return;
|
|
90
|
+
|
|
91
|
+
// Проверяем что предыдущий runner мёртв (иначе не трогаем — он ещё работает)
|
|
92
|
+
if (saved.runnerPid && isProcessAlive(saved.runnerPid)) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const currentPids = getProcessSnapshot();
|
|
97
|
+
const orphans = [];
|
|
98
|
+
for (const pid of currentPids) {
|
|
99
|
+
if (!saved.snapshot.has(pid) && pid !== process.pid) {
|
|
100
|
+
orphans.push(pid);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (orphans.length > 0) {
|
|
105
|
+
killPids(orphans, logger);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
removePidFile(pidFilePath);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isProcessAlive(pid) {
|
|
112
|
+
try {
|
|
113
|
+
process.kill(pid, 0);
|
|
114
|
+
return true;
|
|
115
|
+
} catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
38
120
|
function killPids(pids, logger = null) {
|
|
39
121
|
if (pids.length === 0) return;
|
|
40
122
|
if (process.platform === 'win32') {
|
|
@@ -48,7 +130,7 @@ function killPids(pids, logger = null) {
|
|
|
48
130
|
try { process.kill(pid, 'SIGKILL'); } catch { /* already dead */ }
|
|
49
131
|
}
|
|
50
132
|
}
|
|
51
|
-
if (logger
|
|
133
|
+
if (logger) {
|
|
52
134
|
logger.info(`Cleaned up ${pids.length} orphan process(es): ${pids.join(', ')}`, 'ProcessCleanup');
|
|
53
135
|
}
|
|
54
136
|
}
|
|
@@ -56,7 +138,6 @@ function killPids(pids, logger = null) {
|
|
|
56
138
|
function killChildProcess(child) {
|
|
57
139
|
if (!child.pid) return;
|
|
58
140
|
if (process.platform === 'win32') {
|
|
59
|
-
// taskkill /T /F — лучший effort для прямого дерева
|
|
60
141
|
try { execSync(`taskkill /pid ${child.pid} /T /F`, { stdio: 'pipe' }); } catch {}
|
|
61
142
|
} else {
|
|
62
143
|
try { child.kill('SIGTERM'); } catch {}
|
|
@@ -1255,6 +1336,8 @@ class PipelineRunner {
|
|
|
1255
1336
|
|
|
1256
1337
|
// Текущий executor для доступа к activeChildren при shutdown
|
|
1257
1338
|
this.currentExecutor = null;
|
|
1339
|
+
// Путь к PID-файлу для cleanup сирот между запусками
|
|
1340
|
+
this.pidFilePath = getPidFilePath(projectRoot);
|
|
1258
1341
|
// Снапшот PID'ов перед стартом пайплайна — для snapshot-based cleanup
|
|
1259
1342
|
this.preRunSnapshot = new Set();
|
|
1260
1343
|
|
|
@@ -1323,8 +1406,12 @@ class PipelineRunner {
|
|
|
1323
1406
|
const maxSteps = this.pipeline.execution?.max_steps || 100;
|
|
1324
1407
|
const delayBetweenStages = this.pipeline.execution?.delay_between_stages || 5;
|
|
1325
1408
|
|
|
1326
|
-
//
|
|
1409
|
+
// Cleanup сирот от предыдущего запуска (если runner был убит жёстко)
|
|
1410
|
+
cleanupOrphansFromFile(this.pidFilePath, this.logger);
|
|
1411
|
+
|
|
1412
|
+
// Снапшот процессов до старта + запись PID-файла
|
|
1327
1413
|
this.preRunSnapshot = getProcessSnapshot();
|
|
1414
|
+
writePidFile(this.pidFilePath, this.preRunSnapshot);
|
|
1328
1415
|
|
|
1329
1416
|
this.logger.info('=== Pipeline Runner Started ===', 'PipelineRunner');
|
|
1330
1417
|
this.logger.info(`Entry stage: ${this.pipeline.entry}`, 'PipelineRunner');
|
|
@@ -1558,6 +1645,8 @@ class PipelineRunner {
|
|
|
1558
1645
|
if (orphans.length > 0) {
|
|
1559
1646
|
killPids(orphans, this.logger);
|
|
1560
1647
|
}
|
|
1648
|
+
// PID-файл больше не нужен — cleanup выполнен
|
|
1649
|
+
removePidFile(this.pidFilePath);
|
|
1561
1650
|
}
|
|
1562
1651
|
|
|
1563
1652
|
setupGracefulShutdown() {
|