@xioflow/kernel 0.1.5 → 0.2.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/README.md +14 -3
- package/dist/domain.d.ts +40 -6
- package/dist/domain.d.ts.map +1 -1
- package/dist/domain.js +388 -24
- package/dist/domain.js.map +1 -1
- package/dist/driver/node-driver.d.ts +10 -24
- package/dist/driver/node-driver.d.ts.map +1 -1
- package/dist/driver/node-driver.js +201 -280
- package/dist/driver/node-driver.js.map +1 -1
- package/dist/driver/process-facts.d.ts +25 -0
- package/dist/driver/process-facts.d.ts.map +1 -0
- package/dist/driver/process-facts.js +164 -0
- package/dist/driver/process-facts.js.map +1 -0
- package/dist/driver/sampler.d.ts +51 -0
- package/dist/driver/sampler.d.ts.map +1 -0
- package/dist/driver/sampler.js +224 -0
- package/dist/driver/sampler.js.map +1 -0
- package/dist/driver/types.d.ts +21 -1
- package/dist/driver/types.d.ts.map +1 -1
- package/dist/recovery/engine.d.ts.map +1 -1
- package/dist/recovery/engine.js +103 -19
- package/dist/recovery/engine.js.map +1 -1
- package/dist/store/sqlite.d.ts +12 -0
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/sqlite.js +162 -12
- package/dist/store/sqlite.js.map +1 -1
- package/dist/supervisor/supervisor.d.ts +15 -0
- package/dist/supervisor/supervisor.d.ts.map +1 -1
- package/dist/supervisor/supervisor.js +619 -214
- package/dist/supervisor/supervisor.js.map +1 -1
- package/dist/testing/contract-suite.d.ts.map +1 -1
- package/dist/testing/contract-suite.js +301 -11
- package/dist/testing/contract-suite.js.map +1 -1
- package/dist/types.d.ts +34 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +29 -0
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,44 +1,106 @@
|
|
|
1
|
-
import { spawn
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { readBootId, readStartTime, getGroupMembers } from './process-facts.js';
|
|
5
|
+
import { ProcessSampler } from './sampler.js';
|
|
6
|
+
function resolveExecutable(bin, envPath) {
|
|
7
|
+
if (bin.includes(path.sep)) {
|
|
8
|
+
try {
|
|
9
|
+
fs.accessSync(bin, fs.constants.X_OK);
|
|
10
|
+
return bin;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const dirs = (envPath || process.env.PATH || '').split(path.delimiter);
|
|
17
|
+
for (const dir of dirs) {
|
|
18
|
+
if (!dir)
|
|
19
|
+
continue;
|
|
20
|
+
const full = path.join(dir, bin);
|
|
21
|
+
try {
|
|
22
|
+
fs.accessSync(full, fs.constants.X_OK);
|
|
23
|
+
return full;
|
|
24
|
+
}
|
|
25
|
+
catch { }
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
2
29
|
export class NodePlatformDriver {
|
|
3
30
|
name = 'node-default';
|
|
31
|
+
cachedBootId = readBootId();
|
|
32
|
+
sampler = ProcessSampler.getInstance();
|
|
33
|
+
async readBootId() {
|
|
34
|
+
return this.cachedBootId;
|
|
35
|
+
}
|
|
36
|
+
async getGroupEvidence(pgid) {
|
|
37
|
+
return getGroupMembers(pgid);
|
|
38
|
+
}
|
|
4
39
|
capabilities = {
|
|
5
40
|
// Windows 无进程组语义:process.kill(-pid) 会失败并退化为只杀直接子进程,
|
|
6
41
|
// 所以必须如实声明不支持,让准入期拒绝 hard 请求而不是静默降级。
|
|
7
42
|
processGroupKill: process.platform !== 'win32',
|
|
8
43
|
accurateStartTime: true,
|
|
44
|
+
startTimeSource: process.platform === 'linux' ? 'procfs' : process.platform === 'darwin' ? 'ps_lstart' : 'none',
|
|
9
45
|
memoryHardLimit: false, // macOS/Node.js 默认不支持进程树 cgroup 内存硬限
|
|
10
46
|
pidsLimit: false,
|
|
11
47
|
cpuLimit: false,
|
|
12
48
|
// 后代枚举依赖 ps(1):Windows 上无法枚举,不能谎报 full。
|
|
13
49
|
descendantEnumeration: process.platform === 'win32' ? 'none' : 'full',
|
|
50
|
+
// 门管道受控启动:POSIX 平台下通过 /bin/sh 与 stdio[3] 实现
|
|
51
|
+
gatedSpawn: process.platform !== 'win32',
|
|
14
52
|
};
|
|
15
53
|
// 内存中追踪当前驱动启动的所有活跃进程
|
|
16
54
|
activeHandles = new Map();
|
|
17
55
|
// 持续累积后代 PID 集合:leaderPid -> Set<descendantPid>
|
|
18
56
|
cumulativeDescendantsMap = new Map();
|
|
19
|
-
|
|
20
|
-
descendantPollers = new Map();
|
|
57
|
+
pollerTimer = null;
|
|
21
58
|
async spawn(command) {
|
|
22
59
|
const stdinPayload = command.stdin === undefined
|
|
23
60
|
? undefined
|
|
24
61
|
: typeof command.stdin === 'string'
|
|
25
62
|
? Buffer.from(command.stdin, 'utf8')
|
|
26
63
|
: Buffer.from(command.stdin);
|
|
64
|
+
const childEnv = command.envWhiteList
|
|
65
|
+
? { ...command.envWhiteList }
|
|
66
|
+
: command.inheritEnv === false
|
|
67
|
+
? {}
|
|
68
|
+
: process.env;
|
|
69
|
+
const useGatedSpawn = Boolean(this.capabilities.gatedSpawn);
|
|
70
|
+
// 门管道模式下,先验证二进制是否存在与可执行(P0-3 / 契约 #1 保证)
|
|
71
|
+
if (useGatedSpawn) {
|
|
72
|
+
const resolved = resolveExecutable(command.execPath, childEnv.PATH);
|
|
73
|
+
if (!resolved) {
|
|
74
|
+
const err = new Error(`spawn ${command.execPath} ENOENT`);
|
|
75
|
+
err.code = 'ENOENT';
|
|
76
|
+
err.syscall = `spawn ${command.execPath}`;
|
|
77
|
+
err.path = command.execPath;
|
|
78
|
+
return Promise.reject(err);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
27
81
|
return new Promise((resolve, reject) => {
|
|
28
82
|
let child;
|
|
83
|
+
let gatePipe = null;
|
|
84
|
+
let gateReleased = false;
|
|
29
85
|
try {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
86
|
+
if (useGatedSpawn) {
|
|
87
|
+
const springboard = 'IFS= read -r _ <&3 || exit 125; exec 3<&-; exec "$0" "$@";';
|
|
88
|
+
child = spawn('/bin/sh', ['-c', springboard, command.execPath, ...command.args], {
|
|
89
|
+
cwd: command.cwd,
|
|
90
|
+
env: childEnv,
|
|
91
|
+
detached: true,
|
|
92
|
+
stdio: [stdinPayload === undefined ? 'ignore' : 'pipe', 'pipe', 'pipe', 'pipe'],
|
|
93
|
+
});
|
|
94
|
+
gatePipe = child.stdio[3];
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
child = spawn(command.execPath, command.args, {
|
|
98
|
+
cwd: command.cwd,
|
|
99
|
+
env: childEnv,
|
|
100
|
+
detached: true,
|
|
101
|
+
stdio: [stdinPayload === undefined ? 'ignore' : 'pipe', 'pipe', 'pipe'],
|
|
102
|
+
});
|
|
103
|
+
}
|
|
42
104
|
}
|
|
43
105
|
catch (err) {
|
|
44
106
|
return reject(err);
|
|
@@ -49,41 +111,28 @@ export class NodePlatformDriver {
|
|
|
49
111
|
reject(err);
|
|
50
112
|
}
|
|
51
113
|
});
|
|
52
|
-
child.on('spawn', () => {
|
|
114
|
+
child.on('spawn', async () => {
|
|
53
115
|
hasSpawned = true;
|
|
54
116
|
const pid = child.pid;
|
|
55
117
|
const startTimeMonotonic = Number(process.hrtime.bigint() / 1000n);
|
|
118
|
+
const bootId = await this.cachedBootId;
|
|
56
119
|
const identity = {
|
|
57
120
|
pid,
|
|
58
121
|
pgid: pid,
|
|
59
122
|
startTimeMonotonic,
|
|
60
123
|
spawnTime: new Date().toISOString(),
|
|
61
124
|
commandFingerprint: `${command.execPath}:${command.args.join(' ')}`,
|
|
125
|
+
bootId: bootId ?? undefined,
|
|
62
126
|
};
|
|
63
127
|
const cumSet = new Set();
|
|
64
128
|
this.cumulativeDescendantsMap.set(pid, cumSet);
|
|
65
|
-
|
|
66
|
-
const currentCum = this.cumulativeDescendantsMap.get(pid);
|
|
67
|
-
if (!currentCum)
|
|
68
|
-
return;
|
|
69
|
-
const tree = this.getProcessTree(pid);
|
|
70
|
-
for (const d of tree.allDescendants) {
|
|
71
|
-
currentCum.add(d);
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
scanDescendants();
|
|
75
|
-
const poller = setInterval(scanDescendants, 30);
|
|
76
|
-
if (poller.unref)
|
|
77
|
-
poller.unref();
|
|
78
|
-
this.descendantPollers.set(pid, poller);
|
|
129
|
+
this.ensureSharedPoller();
|
|
79
130
|
const exitPromise = new Promise((exitResolve) => {
|
|
80
131
|
const cleanup = () => {
|
|
81
|
-
const p = this.descendantPollers.get(pid);
|
|
82
|
-
if (p) {
|
|
83
|
-
clearInterval(p);
|
|
84
|
-
this.descendantPollers.delete(pid);
|
|
85
|
-
}
|
|
86
132
|
this.activeHandles.delete(pid);
|
|
133
|
+
if (this.activeHandles.size === 0) {
|
|
134
|
+
this.stopSharedPoller();
|
|
135
|
+
}
|
|
87
136
|
};
|
|
88
137
|
child.on('close', (exitCode, signal) => {
|
|
89
138
|
cleanup();
|
|
@@ -107,6 +156,25 @@ export class NodePlatformDriver {
|
|
|
107
156
|
child.on('error', () => settle(1, null));
|
|
108
157
|
child.on('close', (exitCode, signal) => settle(exitCode, signal));
|
|
109
158
|
});
|
|
159
|
+
const releaseGate = () => {
|
|
160
|
+
if (!gateReleased && gatePipe && !gatePipe.destroyed) {
|
|
161
|
+
gateReleased = true;
|
|
162
|
+
try {
|
|
163
|
+
gatePipe.write('GO\n');
|
|
164
|
+
gatePipe.end();
|
|
165
|
+
}
|
|
166
|
+
catch { }
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
const destroyGate = () => {
|
|
170
|
+
if (!gateReleased && gatePipe && !gatePipe.destroyed) {
|
|
171
|
+
gateReleased = true;
|
|
172
|
+
try {
|
|
173
|
+
gatePipe.destroy();
|
|
174
|
+
}
|
|
175
|
+
catch { }
|
|
176
|
+
}
|
|
177
|
+
};
|
|
110
178
|
const handle = {
|
|
111
179
|
identity,
|
|
112
180
|
stdout: child.stdout,
|
|
@@ -114,6 +182,8 @@ export class NodePlatformDriver {
|
|
|
114
182
|
onRootExit: rootExitPromise,
|
|
115
183
|
onExit: exitPromise,
|
|
116
184
|
rawProcess: child,
|
|
185
|
+
releaseGate: useGatedSpawn ? releaseGate : undefined,
|
|
186
|
+
destroyGate: useGatedSpawn ? destroyGate : undefined,
|
|
117
187
|
};
|
|
118
188
|
this.activeHandles.set(pid, { handle, child, startTime: startTimeMonotonic });
|
|
119
189
|
// 一次性 stdin:写入后关闭。子进程不读就退出的 EPIPE 不是启动失败,由退出码体现。
|
|
@@ -125,14 +195,45 @@ export class NodePlatformDriver {
|
|
|
125
195
|
});
|
|
126
196
|
});
|
|
127
197
|
}
|
|
198
|
+
ensureSharedPoller() {
|
|
199
|
+
if (this.pollerTimer)
|
|
200
|
+
return;
|
|
201
|
+
this.pollerTimer = setInterval(async () => {
|
|
202
|
+
if (this.cumulativeDescendantsMap.size === 0) {
|
|
203
|
+
this.stopSharedPoller();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
const snapshot = await this.sampler.getSnapshot(30);
|
|
208
|
+
for (const [leaderPid, cumSet] of this.cumulativeDescendantsMap.entries()) {
|
|
209
|
+
const tree = await this.sampler.getProcessTree(leaderPid, snapshot, Array.from(cumSet));
|
|
210
|
+
for (const d of tree.allDescendants) {
|
|
211
|
+
cumSet.add(d);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch { }
|
|
216
|
+
}, 35);
|
|
217
|
+
if (this.pollerTimer.unref)
|
|
218
|
+
this.pollerTimer.unref();
|
|
219
|
+
}
|
|
220
|
+
stopSharedPoller() {
|
|
221
|
+
if (this.pollerTimer) {
|
|
222
|
+
clearInterval(this.pollerTimer);
|
|
223
|
+
this.pollerTimer = null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
128
226
|
async verifyIdentity(identity) {
|
|
129
|
-
// 1. 检查操作系统中该 PID
|
|
130
|
-
|
|
131
|
-
// 走到下面的指纹校验并得出 cannot_determine,把一个已经确定的崩溃错判成不可知。
|
|
132
|
-
if (!this.isPidAlive(identity.pid) || this.isZombie(identity.pid)) {
|
|
227
|
+
// 1. 检查操作系统中该 PID 是否存活或为僵尸
|
|
228
|
+
if (!this.isPidAlive(identity.pid) || (await this.sampler.isZombie(identity.pid))) {
|
|
133
229
|
return 'not_original_process';
|
|
134
230
|
}
|
|
135
|
-
// 2.
|
|
231
|
+
// 2. bootId 核对:宿主若已重启,绝不盲目信任 PID (P0-1)
|
|
232
|
+
const currentBootId = await this.readBootId();
|
|
233
|
+
if (identity.bootId && currentBootId && identity.bootId !== currentBootId) {
|
|
234
|
+
return 'cannot_determine';
|
|
235
|
+
}
|
|
236
|
+
// 3. 核对内存中本驱动登记的启动记录
|
|
136
237
|
const tracked = this.activeHandles.get(identity.pid);
|
|
137
238
|
if (tracked) {
|
|
138
239
|
if (identity.startTimeMonotonic &&
|
|
@@ -142,21 +243,33 @@ export class NodePlatformDriver {
|
|
|
142
243
|
}
|
|
143
244
|
return 'cannot_determine';
|
|
144
245
|
}
|
|
145
|
-
//
|
|
246
|
+
// 4. 跨崩溃/重启进程恢复:查 OS 进程创建时间与命令行指纹 (P0-1)
|
|
247
|
+
const snapshot = await this.sampler.getSnapshot(100);
|
|
248
|
+
const proc = snapshot.get(identity.pid);
|
|
249
|
+
// 进程在进程表中已不存在,或处于僵尸态(<defunct> / Z),直接判定为死亡
|
|
250
|
+
if (!proc || proc.state === 'Z' || proc.command.includes('<defunct>')) {
|
|
251
|
+
return 'not_original_process';
|
|
252
|
+
}
|
|
253
|
+
// 若命令行指纹不匹配,直接否定为 not_original_process
|
|
146
254
|
if (identity.commandFingerprint) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
255
|
+
const baseExec = identity.commandFingerprint.split(':')[0];
|
|
256
|
+
if (!proc.command.includes(baseExec)) {
|
|
257
|
+
return 'not_original_process';
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// 若登记了 spawnTime,核对 OS 实际启动时间
|
|
261
|
+
const actualStartTimeMs = await readStartTime(identity.pid);
|
|
262
|
+
if (identity.spawnTime && actualStartTimeMs !== null) {
|
|
263
|
+
const expectedTimeMs = new Date(identity.spawnTime).getTime();
|
|
264
|
+
if (!isNaN(expectedTimeMs)) {
|
|
265
|
+
// macOS ps lstart 为 1 秒精度,允许 3 秒宽限
|
|
266
|
+
if (Math.abs(actualStartTimeMs - expectedTimeMs) > 3000) {
|
|
267
|
+
return 'not_original_process';
|
|
268
|
+
}
|
|
269
|
+
if (proc && identity.commandFingerprint && proc.command.includes(identity.commandFingerprint.split(':')[0])) {
|
|
154
270
|
return 'is_original_process';
|
|
155
271
|
}
|
|
156
272
|
}
|
|
157
|
-
catch {
|
|
158
|
-
return 'cannot_determine';
|
|
159
|
-
}
|
|
160
273
|
}
|
|
161
274
|
return 'cannot_determine';
|
|
162
275
|
}
|
|
@@ -164,18 +277,25 @@ export class NodePlatformDriver {
|
|
|
164
277
|
const pid = identity.pid;
|
|
165
278
|
const cumSet = this.cumulativeDescendantsMap.get(pid);
|
|
166
279
|
// 1. 终止前先枚举后代进程树,捕获所有组内进程与逃逸进程
|
|
167
|
-
|
|
280
|
+
this.sampler.clearCache();
|
|
281
|
+
const initialTree = await this.sampler.getProcessTree(pid, undefined, cumSet ? Array.from(cumSet) : undefined);
|
|
282
|
+
if (cumSet) {
|
|
283
|
+
for (const d of initialTree.allDescendants) {
|
|
284
|
+
cumSet.add(d);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
168
287
|
const allTrackedPids = new Set([pid, ...initialTree.allDescendants]);
|
|
169
288
|
if (cumSet) {
|
|
170
289
|
for (const cp of cumSet) {
|
|
171
290
|
allTrackedPids.add(cp);
|
|
172
291
|
}
|
|
173
292
|
}
|
|
174
|
-
//
|
|
293
|
+
// 若原主进程已死且无任何进程组或后代存活,直接收尾
|
|
294
|
+
const isGroupAlive = await this.sampler.isGroupAlive(pid);
|
|
175
295
|
const aliveBeforeSignals = Array.from(allTrackedPids).filter((p) => p !== pid && this.isPidAlive(p));
|
|
176
|
-
if (!this.isPidAlive(pid) && aliveBeforeSignals.length === 0) {
|
|
296
|
+
if (!this.isPidAlive(pid) && !isGroupAlive && aliveBeforeSignals.length === 0) {
|
|
177
297
|
this.cumulativeDescendantsMap.delete(pid);
|
|
178
|
-
return { stopped:
|
|
298
|
+
return { stopped: 'confirmed_stopped', scope: 'direct_child' };
|
|
179
299
|
}
|
|
180
300
|
// 第一阶段:SIGINT 中断信号至整组
|
|
181
301
|
this.sendSignalToGroup(pid, 'SIGINT');
|
|
@@ -191,7 +311,8 @@ export class NodePlatformDriver {
|
|
|
191
311
|
exited = await this.waitForGroupAndPids(pid, initialTree.inGroupDescendants, 1000);
|
|
192
312
|
}
|
|
193
313
|
// 组空后再次扫描后代,核查是否有任何残留进程或 setsid 逃逸孤儿
|
|
194
|
-
|
|
314
|
+
this.sampler.clearCache();
|
|
315
|
+
const postTree = await this.sampler.getProcessTree(pid, undefined, cumSet ? Array.from(cumSet) : undefined);
|
|
195
316
|
for (const p of postTree.allDescendants) {
|
|
196
317
|
allTrackedPids.add(p);
|
|
197
318
|
}
|
|
@@ -203,191 +324,65 @@ export class NodePlatformDriver {
|
|
|
203
324
|
// 探活所有曾属于该进程树的后代
|
|
204
325
|
const aliveResiduals = Array.from(allTrackedPids).filter((p) => this.isPidAlive(p));
|
|
205
326
|
if (aliveResiduals.length > 0) {
|
|
206
|
-
// 存在逃逸或残留孤儿,如实标记
|
|
327
|
+
// 存在逃逸或残留孤儿,如实标记 cannot_determine 与 residualPids
|
|
207
328
|
return {
|
|
208
|
-
stopped:
|
|
329
|
+
stopped: 'cannot_determine',
|
|
209
330
|
scope: 'unknown',
|
|
210
331
|
residualPids: aliveResiduals,
|
|
211
332
|
errorDetails: `Residual or escaped processes still alive after termination: ${aliveResiduals.join(', ')}`,
|
|
212
333
|
};
|
|
213
334
|
}
|
|
214
335
|
this.cumulativeDescendantsMap.delete(pid);
|
|
215
|
-
return { stopped:
|
|
336
|
+
return { stopped: 'confirmed_stopped', scope: 'process_group' };
|
|
216
337
|
}
|
|
217
|
-
/**
|
|
218
|
-
* 崩溃恢复专用:leader 已经死了(通常是 SIGKILL 后的僵尸),但组里可能还有后代
|
|
219
|
-
* 进程活着。这些进程没有任何 owner,唯一诚实的处置是按 pgid 定向清场,并在返回前
|
|
220
|
-
* 确认组已经空了——否则残留进程会被继续留在这个域里。
|
|
221
|
-
*/
|
|
222
338
|
async terminateGroup(pgid, graceMs = 2000) {
|
|
223
339
|
if (!this.capabilities.processGroupKill) {
|
|
224
340
|
return {
|
|
225
|
-
stopped:
|
|
341
|
+
stopped: 'not_stopped',
|
|
226
342
|
scope: 'unknown',
|
|
227
343
|
errorDetails: 'process groups are not supported on this platform',
|
|
228
344
|
};
|
|
229
345
|
}
|
|
230
|
-
if (!this.isGroupAlive(pgid)) {
|
|
231
|
-
return { stopped:
|
|
346
|
+
if (!(await this.sampler.isGroupAlive(pgid))) {
|
|
347
|
+
return { stopped: 'confirmed_stopped', scope: 'process_group' };
|
|
232
348
|
}
|
|
233
349
|
this.sendSignalToGroup(pgid, 'SIGKILL');
|
|
234
350
|
const start = Date.now();
|
|
235
351
|
while (Date.now() - start < graceMs) {
|
|
236
|
-
|
|
352
|
+
this.sampler.clearCache();
|
|
353
|
+
if (!(await this.sampler.isGroupAlive(pgid))) {
|
|
237
354
|
this.cumulativeDescendantsMap.delete(pgid);
|
|
238
|
-
return { stopped:
|
|
355
|
+
return { stopped: 'confirmed_stopped', scope: 'process_group' };
|
|
239
356
|
}
|
|
240
357
|
await new Promise((r) => setTimeout(r, 25));
|
|
241
358
|
}
|
|
359
|
+
const snapshot = await this.sampler.getSnapshot(10);
|
|
242
360
|
const survivors = [];
|
|
243
|
-
for (const entry of
|
|
361
|
+
for (const entry of snapshot.values()) {
|
|
244
362
|
if (entry.state === 'Z')
|
|
245
363
|
continue;
|
|
246
364
|
if (entry.pgid === pgid)
|
|
247
365
|
survivors.push(entry.pid);
|
|
248
366
|
}
|
|
249
367
|
return {
|
|
250
|
-
stopped: survivors.length === 0,
|
|
368
|
+
stopped: survivors.length === 0 ? 'confirmed_stopped' : 'cannot_determine',
|
|
251
369
|
scope: 'process_group',
|
|
252
370
|
residualPids: survivors.length > 0 ? survivors : undefined,
|
|
253
371
|
};
|
|
254
372
|
}
|
|
255
373
|
async sampleMetrics(identity) {
|
|
256
374
|
try {
|
|
257
|
-
const tree = this.getProcessTree(identity.pid);
|
|
375
|
+
const tree = await this.sampler.getProcessTree(identity.pid);
|
|
258
376
|
const targetPids = [identity.pid, ...tree.allDescendants].filter((p) => this.isPidAlive(p));
|
|
259
377
|
if (targetPids.length === 0) {
|
|
260
378
|
return { rssBytes: 0, pidsCount: 0, cpuTimeMs: 0 };
|
|
261
379
|
}
|
|
262
|
-
|
|
263
|
-
encoding: 'utf8',
|
|
264
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
265
|
-
});
|
|
266
|
-
let totalRssKb = 0;
|
|
267
|
-
let totalCpuMs = 0;
|
|
268
|
-
let count = 0;
|
|
269
|
-
const lines = out.split('\n');
|
|
270
|
-
for (const line of lines) {
|
|
271
|
-
const trimmed = line.trim();
|
|
272
|
-
if (!trimmed)
|
|
273
|
-
continue;
|
|
274
|
-
const parts = trimmed.split(/\s+/);
|
|
275
|
-
if (parts.length >= 3) {
|
|
276
|
-
const rssKb = parseInt(parts[1], 10) || 0;
|
|
277
|
-
const timeStr = parts[2];
|
|
278
|
-
totalRssKb += rssKb;
|
|
279
|
-
totalCpuMs += this.parseCpuTimeToMs(timeStr);
|
|
280
|
-
count++;
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
return {
|
|
284
|
-
rssBytes: totalRssKb * 1024,
|
|
285
|
-
pidsCount: count || targetPids.length,
|
|
286
|
-
cpuTimeMs: totalCpuMs,
|
|
287
|
-
};
|
|
380
|
+
return this.sampler.sampleMetrics(targetPids);
|
|
288
381
|
}
|
|
289
382
|
catch {
|
|
290
383
|
return { rssBytes: 0, pidsCount: 0, cpuTimeMs: 0 };
|
|
291
384
|
}
|
|
292
385
|
}
|
|
293
|
-
getProcessTree(leaderPid) {
|
|
294
|
-
const allProcs = this.getAllProcesses();
|
|
295
|
-
const procsByPid = new Map();
|
|
296
|
-
const childrenMap = new Map();
|
|
297
|
-
for (const p of allProcs) {
|
|
298
|
-
procsByPid.set(p.pid, p);
|
|
299
|
-
const list = childrenMap.get(p.ppid);
|
|
300
|
-
if (list) {
|
|
301
|
-
list.push(p);
|
|
302
|
-
}
|
|
303
|
-
else {
|
|
304
|
-
childrenMap.set(p.ppid, [p]);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
const allDescendants = [];
|
|
308
|
-
const inGroupDescendants = [];
|
|
309
|
-
const escapedDescendants = [];
|
|
310
|
-
const cumSet = this.cumulativeDescendantsMap.get(leaderPid);
|
|
311
|
-
const historicalDescendants = cumSet ? Array.from(cumSet) : [];
|
|
312
|
-
// 初始队列包括 leaderPid 以及所有已知历史后代(防止中间父进程提前退出导致孙进程 PPID 变为 1 发生断链漏报)
|
|
313
|
-
const queue = [leaderPid];
|
|
314
|
-
const visited = new Set([leaderPid]);
|
|
315
|
-
for (const hPid of historicalDescendants) {
|
|
316
|
-
if (!visited.has(hPid)) {
|
|
317
|
-
visited.add(hPid);
|
|
318
|
-
queue.push(hPid);
|
|
319
|
-
const pInfo = procsByPid.get(hPid);
|
|
320
|
-
if (pInfo) {
|
|
321
|
-
allDescendants.push(hPid);
|
|
322
|
-
if (pInfo.pgid === leaderPid) {
|
|
323
|
-
inGroupDescendants.push(hPid);
|
|
324
|
-
}
|
|
325
|
-
else {
|
|
326
|
-
escapedDescendants.push(hPid);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
while (queue.length > 0) {
|
|
332
|
-
const currentPid = queue.shift();
|
|
333
|
-
const children = childrenMap.get(currentPid) || [];
|
|
334
|
-
for (const child of children) {
|
|
335
|
-
if (!visited.has(child.pid)) {
|
|
336
|
-
visited.add(child.pid);
|
|
337
|
-
allDescendants.push(child.pid);
|
|
338
|
-
if (child.pgid === leaderPid) {
|
|
339
|
-
inGroupDescendants.push(child.pid);
|
|
340
|
-
}
|
|
341
|
-
else {
|
|
342
|
-
escapedDescendants.push(child.pid);
|
|
343
|
-
}
|
|
344
|
-
queue.push(child.pid);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
// 扫描可能已由于中间父进程退出导致 PPID 变为 1、但仍同属于 leader 进程组的残留进程
|
|
349
|
-
for (const p of allProcs) {
|
|
350
|
-
if (p.pgid === leaderPid && !visited.has(p.pid)) {
|
|
351
|
-
visited.add(p.pid);
|
|
352
|
-
allDescendants.push(p.pid);
|
|
353
|
-
inGroupDescendants.push(p.pid);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
if (cumSet) {
|
|
357
|
-
for (const d of allDescendants) {
|
|
358
|
-
cumSet.add(d);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
return { allDescendants, inGroupDescendants, escapedDescendants };
|
|
362
|
-
}
|
|
363
|
-
getAllProcesses() {
|
|
364
|
-
try {
|
|
365
|
-
const output = execFileSync('ps', ['-A', '-o', 'pid=,ppid=,pgid=,command='], {
|
|
366
|
-
encoding: 'utf8',
|
|
367
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
368
|
-
});
|
|
369
|
-
const lines = output.split('\n');
|
|
370
|
-
const procs = [];
|
|
371
|
-
for (const line of lines) {
|
|
372
|
-
const trimmed = line.trim();
|
|
373
|
-
if (!trimmed)
|
|
374
|
-
continue;
|
|
375
|
-
const match = trimmed.match(/^(\d+)\s+(\d+)\s+(\d+)\s+(.*)$/);
|
|
376
|
-
if (match) {
|
|
377
|
-
procs.push({
|
|
378
|
-
pid: parseInt(match[1], 10),
|
|
379
|
-
ppid: parseInt(match[2], 10),
|
|
380
|
-
pgid: parseInt(match[3], 10),
|
|
381
|
-
command: match[4],
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
return procs;
|
|
386
|
-
}
|
|
387
|
-
catch {
|
|
388
|
-
return [];
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
386
|
isPidAlive(pid) {
|
|
392
387
|
try {
|
|
393
388
|
process.kill(pid, 0);
|
|
@@ -397,68 +392,24 @@ export class NodePlatformDriver {
|
|
|
397
392
|
return err.code === 'EPERM';
|
|
398
393
|
}
|
|
399
394
|
}
|
|
400
|
-
/**
|
|
401
|
-
* 崩溃恢复的判定依据:进程组里是否还有**真正能工作**的进程。
|
|
402
|
-
*
|
|
403
|
-
* SIGKILL 掉的 leader 会以僵尸形态留在组里,`kill(-pgid, 0)` 因此永远为真。
|
|
404
|
-
* 只按 PID 存在与否判断会让恢复永远无法确认"组已清空",孤儿进程也就永远得不到
|
|
405
|
-
* 处置。这里按 ps 的进程状态把僵尸排除掉。
|
|
406
|
-
*/
|
|
407
|
-
isGroupAlive(pgid) {
|
|
408
|
-
try {
|
|
409
|
-
process.kill(-pgid, 0);
|
|
410
|
-
}
|
|
411
|
-
catch (err) {
|
|
412
|
-
return err.code === 'EPERM';
|
|
413
|
-
}
|
|
414
|
-
// The group exists; zombies do not count as work in progress.
|
|
415
|
-
for (const entry of this.processTable().values()) {
|
|
416
|
-
if (entry.state === 'Z')
|
|
417
|
-
continue;
|
|
418
|
-
if (entry.pgid === pgid)
|
|
419
|
-
return true;
|
|
420
|
-
}
|
|
421
|
-
return false;
|
|
422
|
-
}
|
|
423
|
-
/** pid → { pid, pgid, state } snapshot from `ps -A`; state is one letter (R/S/T/Z/…). */
|
|
424
|
-
processTable() {
|
|
425
|
-
const table = new Map();
|
|
426
|
-
try {
|
|
427
|
-
const output = execFileSync('ps', ['-A', '-o', 'pid=,pgid=,state='], {
|
|
428
|
-
encoding: 'utf8',
|
|
429
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
430
|
-
});
|
|
431
|
-
for (const line of output.split('\n')) {
|
|
432
|
-
const match = line.trim().match(/^(\d+)\s+(\d+)\s+(\S+)/);
|
|
433
|
-
if (!match)
|
|
434
|
-
continue;
|
|
435
|
-
const pid = parseInt(match[1], 10);
|
|
436
|
-
table.set(pid, { pid, pgid: parseInt(match[2], 10), state: match[3][0] });
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
catch {
|
|
440
|
-
// no snapshot available; callers treat the process as alive
|
|
441
|
-
}
|
|
442
|
-
return table;
|
|
443
|
-
}
|
|
444
|
-
isZombie(pid) {
|
|
445
|
-
return this.processTable().get(pid)?.state === 'Z';
|
|
446
|
-
}
|
|
447
395
|
sendSignalToGroup(pid, signal) {
|
|
448
396
|
try {
|
|
449
397
|
process.kill(-pid, signal);
|
|
450
398
|
}
|
|
451
|
-
catch {
|
|
452
|
-
|
|
453
|
-
|
|
399
|
+
catch (err) {
|
|
400
|
+
if (err && err.code === 'ESRCH') {
|
|
401
|
+
// ESRCH 表示该进程组已不存在或已完全清空,如实返回
|
|
402
|
+
return;
|
|
454
403
|
}
|
|
455
|
-
|
|
404
|
+
// 其他错误(如 EPERM)如实上抛,禁止退回向单 PID 发信号(N5 / 契约 #42)
|
|
405
|
+
throw err;
|
|
456
406
|
}
|
|
457
407
|
}
|
|
458
408
|
async waitForGroupAndPids(pid, inGroupPids, timeoutMs) {
|
|
459
409
|
const start = Date.now();
|
|
460
410
|
while (Date.now() - start < timeoutMs) {
|
|
461
|
-
|
|
411
|
+
this.sampler.clearCache();
|
|
412
|
+
const grpAlive = await this.sampler.isGroupAlive(pid);
|
|
462
413
|
const procAlive = this.isPidAlive(pid);
|
|
463
414
|
const inGrpAlive = inGroupPids.some((p) => this.isPidAlive(p));
|
|
464
415
|
if (!grpAlive && !procAlive && !inGrpAlive) {
|
|
@@ -466,41 +417,11 @@ export class NodePlatformDriver {
|
|
|
466
417
|
}
|
|
467
418
|
await new Promise((r) => setTimeout(r, 50));
|
|
468
419
|
}
|
|
469
|
-
|
|
420
|
+
this.sampler.clearCache();
|
|
421
|
+
const stillGrp = await this.sampler.isGroupAlive(pid);
|
|
470
422
|
const stillProc = this.isPidAlive(pid);
|
|
471
423
|
const stillInGrp = inGroupPids.some((p) => this.isPidAlive(p));
|
|
472
424
|
return !stillGrp && !stillProc && !stillInGrp;
|
|
473
425
|
}
|
|
474
|
-
parseCpuTimeToMs(timeStr) {
|
|
475
|
-
try {
|
|
476
|
-
let days = 0;
|
|
477
|
-
let rest = timeStr;
|
|
478
|
-
if (rest.includes('-')) {
|
|
479
|
-
const daySplit = rest.split('-');
|
|
480
|
-
days = parseInt(daySplit[0], 10) || 0;
|
|
481
|
-
rest = daySplit[1];
|
|
482
|
-
}
|
|
483
|
-
const parts = rest.split(':');
|
|
484
|
-
let hours = 0;
|
|
485
|
-
let minutes = 0;
|
|
486
|
-
let seconds = 0;
|
|
487
|
-
if (parts.length === 3) {
|
|
488
|
-
hours = parseInt(parts[0], 10) || 0;
|
|
489
|
-
minutes = parseInt(parts[1], 10) || 0;
|
|
490
|
-
seconds = parseFloat(parts[2]) || 0;
|
|
491
|
-
}
|
|
492
|
-
else if (parts.length === 2) {
|
|
493
|
-
minutes = parseInt(parts[0], 10) || 0;
|
|
494
|
-
seconds = parseFloat(parts[1]) || 0;
|
|
495
|
-
}
|
|
496
|
-
else if (parts.length === 1) {
|
|
497
|
-
seconds = parseFloat(parts[0]) || 0;
|
|
498
|
-
}
|
|
499
|
-
return Math.round(days * 86400000 + hours * 3600000 + minutes * 60000 + seconds * 1000);
|
|
500
|
-
}
|
|
501
|
-
catch {
|
|
502
|
-
return 0;
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
426
|
}
|
|
506
427
|
//# sourceMappingURL=node-driver.js.map
|