@xioflow/kernel 0.1.4 → 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 +27 -8
- 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 +11 -9
- package/dist/driver/node-driver.d.ts.map +1 -1
- package/dist/driver/node-driver.js +224 -225
- 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 +29 -1
- package/dist/driver/types.d.ts.map +1 -1
- package/dist/recovery/engine.d.ts +2 -0
- package/dist/recovery/engine.d.ts.map +1 -1
- package/dist/recovery/engine.js +137 -14
- 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 +37 -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,12 +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
|
-
if (!this.isPidAlive(identity.pid)) {
|
|
227
|
+
// 1. 检查操作系统中该 PID 是否存活或为僵尸
|
|
228
|
+
if (!this.isPidAlive(identity.pid) || (await this.sampler.isZombie(identity.pid))) {
|
|
131
229
|
return 'not_original_process';
|
|
132
230
|
}
|
|
133
|
-
// 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. 核对内存中本驱动登记的启动记录
|
|
134
237
|
const tracked = this.activeHandles.get(identity.pid);
|
|
135
238
|
if (tracked) {
|
|
136
239
|
if (identity.startTimeMonotonic &&
|
|
@@ -140,21 +243,33 @@ export class NodePlatformDriver {
|
|
|
140
243
|
}
|
|
141
244
|
return 'cannot_determine';
|
|
142
245
|
}
|
|
143
|
-
//
|
|
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
|
|
144
254
|
if (identity.commandFingerprint) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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])) {
|
|
152
270
|
return 'is_original_process';
|
|
153
271
|
}
|
|
154
272
|
}
|
|
155
|
-
catch {
|
|
156
|
-
return 'cannot_determine';
|
|
157
|
-
}
|
|
158
273
|
}
|
|
159
274
|
return 'cannot_determine';
|
|
160
275
|
}
|
|
@@ -162,18 +277,25 @@ export class NodePlatformDriver {
|
|
|
162
277
|
const pid = identity.pid;
|
|
163
278
|
const cumSet = this.cumulativeDescendantsMap.get(pid);
|
|
164
279
|
// 1. 终止前先枚举后代进程树,捕获所有组内进程与逃逸进程
|
|
165
|
-
|
|
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
|
+
}
|
|
166
287
|
const allTrackedPids = new Set([pid, ...initialTree.allDescendants]);
|
|
167
288
|
if (cumSet) {
|
|
168
289
|
for (const cp of cumSet) {
|
|
169
290
|
allTrackedPids.add(cp);
|
|
170
291
|
}
|
|
171
292
|
}
|
|
172
|
-
//
|
|
293
|
+
// 若原主进程已死且无任何进程组或后代存活,直接收尾
|
|
294
|
+
const isGroupAlive = await this.sampler.isGroupAlive(pid);
|
|
173
295
|
const aliveBeforeSignals = Array.from(allTrackedPids).filter((p) => p !== pid && this.isPidAlive(p));
|
|
174
|
-
if (!this.isPidAlive(pid) && aliveBeforeSignals.length === 0) {
|
|
296
|
+
if (!this.isPidAlive(pid) && !isGroupAlive && aliveBeforeSignals.length === 0) {
|
|
175
297
|
this.cumulativeDescendantsMap.delete(pid);
|
|
176
|
-
return { stopped:
|
|
298
|
+
return { stopped: 'confirmed_stopped', scope: 'direct_child' };
|
|
177
299
|
}
|
|
178
300
|
// 第一阶段:SIGINT 中断信号至整组
|
|
179
301
|
this.sendSignalToGroup(pid, 'SIGINT');
|
|
@@ -189,7 +311,8 @@ export class NodePlatformDriver {
|
|
|
189
311
|
exited = await this.waitForGroupAndPids(pid, initialTree.inGroupDescendants, 1000);
|
|
190
312
|
}
|
|
191
313
|
// 组空后再次扫描后代,核查是否有任何残留进程或 setsid 逃逸孤儿
|
|
192
|
-
|
|
314
|
+
this.sampler.clearCache();
|
|
315
|
+
const postTree = await this.sampler.getProcessTree(pid, undefined, cumSet ? Array.from(cumSet) : undefined);
|
|
193
316
|
for (const p of postTree.allDescendants) {
|
|
194
317
|
allTrackedPids.add(p);
|
|
195
318
|
}
|
|
@@ -201,151 +324,63 @@ export class NodePlatformDriver {
|
|
|
201
324
|
// 探活所有曾属于该进程树的后代
|
|
202
325
|
const aliveResiduals = Array.from(allTrackedPids).filter((p) => this.isPidAlive(p));
|
|
203
326
|
if (aliveResiduals.length > 0) {
|
|
204
|
-
// 存在逃逸或残留孤儿,如实标记
|
|
327
|
+
// 存在逃逸或残留孤儿,如实标记 cannot_determine 与 residualPids
|
|
205
328
|
return {
|
|
206
|
-
stopped:
|
|
329
|
+
stopped: 'cannot_determine',
|
|
207
330
|
scope: 'unknown',
|
|
208
331
|
residualPids: aliveResiduals,
|
|
209
332
|
errorDetails: `Residual or escaped processes still alive after termination: ${aliveResiduals.join(', ')}`,
|
|
210
333
|
};
|
|
211
334
|
}
|
|
212
335
|
this.cumulativeDescendantsMap.delete(pid);
|
|
213
|
-
return { stopped:
|
|
336
|
+
return { stopped: 'confirmed_stopped', scope: 'process_group' };
|
|
214
337
|
}
|
|
215
|
-
async
|
|
216
|
-
|
|
217
|
-
const tree = this.getProcessTree(identity.pid);
|
|
218
|
-
const targetPids = [identity.pid, ...tree.allDescendants].filter((p) => this.isPidAlive(p));
|
|
219
|
-
if (targetPids.length === 0) {
|
|
220
|
-
return { rssBytes: 0, pidsCount: 0, cpuTimeMs: 0 };
|
|
221
|
-
}
|
|
222
|
-
const out = execFileSync('ps', ['-p', targetPids.join(','), '-o', 'pid=,rss=,time='], {
|
|
223
|
-
encoding: 'utf8',
|
|
224
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
225
|
-
});
|
|
226
|
-
let totalRssKb = 0;
|
|
227
|
-
let totalCpuMs = 0;
|
|
228
|
-
let count = 0;
|
|
229
|
-
const lines = out.split('\n');
|
|
230
|
-
for (const line of lines) {
|
|
231
|
-
const trimmed = line.trim();
|
|
232
|
-
if (!trimmed)
|
|
233
|
-
continue;
|
|
234
|
-
const parts = trimmed.split(/\s+/);
|
|
235
|
-
if (parts.length >= 3) {
|
|
236
|
-
const rssKb = parseInt(parts[1], 10) || 0;
|
|
237
|
-
const timeStr = parts[2];
|
|
238
|
-
totalRssKb += rssKb;
|
|
239
|
-
totalCpuMs += this.parseCpuTimeToMs(timeStr);
|
|
240
|
-
count++;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
338
|
+
async terminateGroup(pgid, graceMs = 2000) {
|
|
339
|
+
if (!this.capabilities.processGroupKill) {
|
|
243
340
|
return {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
341
|
+
stopped: 'not_stopped',
|
|
342
|
+
scope: 'unknown',
|
|
343
|
+
errorDetails: 'process groups are not supported on this platform',
|
|
247
344
|
};
|
|
248
345
|
}
|
|
249
|
-
|
|
250
|
-
return {
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
getProcessTree(leaderPid) {
|
|
254
|
-
const allProcs = this.getAllProcesses();
|
|
255
|
-
const procsByPid = new Map();
|
|
256
|
-
const childrenMap = new Map();
|
|
257
|
-
for (const p of allProcs) {
|
|
258
|
-
procsByPid.set(p.pid, p);
|
|
259
|
-
const list = childrenMap.get(p.ppid);
|
|
260
|
-
if (list) {
|
|
261
|
-
list.push(p);
|
|
262
|
-
}
|
|
263
|
-
else {
|
|
264
|
-
childrenMap.set(p.ppid, [p]);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
const allDescendants = [];
|
|
268
|
-
const inGroupDescendants = [];
|
|
269
|
-
const escapedDescendants = [];
|
|
270
|
-
const cumSet = this.cumulativeDescendantsMap.get(leaderPid);
|
|
271
|
-
const historicalDescendants = cumSet ? Array.from(cumSet) : [];
|
|
272
|
-
// 初始队列包括 leaderPid 以及所有已知历史后代(防止中间父进程提前退出导致孙进程 PPID 变为 1 发生断链漏报)
|
|
273
|
-
const queue = [leaderPid];
|
|
274
|
-
const visited = new Set([leaderPid]);
|
|
275
|
-
for (const hPid of historicalDescendants) {
|
|
276
|
-
if (!visited.has(hPid)) {
|
|
277
|
-
visited.add(hPid);
|
|
278
|
-
queue.push(hPid);
|
|
279
|
-
const pInfo = procsByPid.get(hPid);
|
|
280
|
-
if (pInfo) {
|
|
281
|
-
allDescendants.push(hPid);
|
|
282
|
-
if (pInfo.pgid === leaderPid) {
|
|
283
|
-
inGroupDescendants.push(hPid);
|
|
284
|
-
}
|
|
285
|
-
else {
|
|
286
|
-
escapedDescendants.push(hPid);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
while (queue.length > 0) {
|
|
292
|
-
const currentPid = queue.shift();
|
|
293
|
-
const children = childrenMap.get(currentPid) || [];
|
|
294
|
-
for (const child of children) {
|
|
295
|
-
if (!visited.has(child.pid)) {
|
|
296
|
-
visited.add(child.pid);
|
|
297
|
-
allDescendants.push(child.pid);
|
|
298
|
-
if (child.pgid === leaderPid) {
|
|
299
|
-
inGroupDescendants.push(child.pid);
|
|
300
|
-
}
|
|
301
|
-
else {
|
|
302
|
-
escapedDescendants.push(child.pid);
|
|
303
|
-
}
|
|
304
|
-
queue.push(child.pid);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
346
|
+
if (!(await this.sampler.isGroupAlive(pgid))) {
|
|
347
|
+
return { stopped: 'confirmed_stopped', scope: 'process_group' };
|
|
307
348
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
349
|
+
this.sendSignalToGroup(pgid, 'SIGKILL');
|
|
350
|
+
const start = Date.now();
|
|
351
|
+
while (Date.now() - start < graceMs) {
|
|
352
|
+
this.sampler.clearCache();
|
|
353
|
+
if (!(await this.sampler.isGroupAlive(pgid))) {
|
|
354
|
+
this.cumulativeDescendantsMap.delete(pgid);
|
|
355
|
+
return { stopped: 'confirmed_stopped', scope: 'process_group' };
|
|
314
356
|
}
|
|
357
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
315
358
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
359
|
+
const snapshot = await this.sampler.getSnapshot(10);
|
|
360
|
+
const survivors = [];
|
|
361
|
+
for (const entry of snapshot.values()) {
|
|
362
|
+
if (entry.state === 'Z')
|
|
363
|
+
continue;
|
|
364
|
+
if (entry.pgid === pgid)
|
|
365
|
+
survivors.push(entry.pid);
|
|
320
366
|
}
|
|
321
|
-
return {
|
|
367
|
+
return {
|
|
368
|
+
stopped: survivors.length === 0 ? 'confirmed_stopped' : 'cannot_determine',
|
|
369
|
+
scope: 'process_group',
|
|
370
|
+
residualPids: survivors.length > 0 ? survivors : undefined,
|
|
371
|
+
};
|
|
322
372
|
}
|
|
323
|
-
|
|
373
|
+
async sampleMetrics(identity) {
|
|
324
374
|
try {
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const lines = output.split('\n');
|
|
330
|
-
const procs = [];
|
|
331
|
-
for (const line of lines) {
|
|
332
|
-
const trimmed = line.trim();
|
|
333
|
-
if (!trimmed)
|
|
334
|
-
continue;
|
|
335
|
-
const match = trimmed.match(/^(\d+)\s+(\d+)\s+(\d+)\s+(.*)$/);
|
|
336
|
-
if (match) {
|
|
337
|
-
procs.push({
|
|
338
|
-
pid: parseInt(match[1], 10),
|
|
339
|
-
ppid: parseInt(match[2], 10),
|
|
340
|
-
pgid: parseInt(match[3], 10),
|
|
341
|
-
command: match[4],
|
|
342
|
-
});
|
|
343
|
-
}
|
|
375
|
+
const tree = await this.sampler.getProcessTree(identity.pid);
|
|
376
|
+
const targetPids = [identity.pid, ...tree.allDescendants].filter((p) => this.isPidAlive(p));
|
|
377
|
+
if (targetPids.length === 0) {
|
|
378
|
+
return { rssBytes: 0, pidsCount: 0, cpuTimeMs: 0 };
|
|
344
379
|
}
|
|
345
|
-
return
|
|
380
|
+
return this.sampler.sampleMetrics(targetPids);
|
|
346
381
|
}
|
|
347
382
|
catch {
|
|
348
|
-
return
|
|
383
|
+
return { rssBytes: 0, pidsCount: 0, cpuTimeMs: 0 };
|
|
349
384
|
}
|
|
350
385
|
}
|
|
351
386
|
isPidAlive(pid) {
|
|
@@ -357,30 +392,24 @@ export class NodePlatformDriver {
|
|
|
357
392
|
return err.code === 'EPERM';
|
|
358
393
|
}
|
|
359
394
|
}
|
|
360
|
-
isGroupAlive(pgid) {
|
|
361
|
-
try {
|
|
362
|
-
process.kill(-pgid, 0);
|
|
363
|
-
return true;
|
|
364
|
-
}
|
|
365
|
-
catch (err) {
|
|
366
|
-
return err.code === 'EPERM';
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
395
|
sendSignalToGroup(pid, signal) {
|
|
370
396
|
try {
|
|
371
397
|
process.kill(-pid, signal);
|
|
372
398
|
}
|
|
373
|
-
catch {
|
|
374
|
-
|
|
375
|
-
|
|
399
|
+
catch (err) {
|
|
400
|
+
if (err && err.code === 'ESRCH') {
|
|
401
|
+
// ESRCH 表示该进程组已不存在或已完全清空,如实返回
|
|
402
|
+
return;
|
|
376
403
|
}
|
|
377
|
-
|
|
404
|
+
// 其他错误(如 EPERM)如实上抛,禁止退回向单 PID 发信号(N5 / 契约 #42)
|
|
405
|
+
throw err;
|
|
378
406
|
}
|
|
379
407
|
}
|
|
380
408
|
async waitForGroupAndPids(pid, inGroupPids, timeoutMs) {
|
|
381
409
|
const start = Date.now();
|
|
382
410
|
while (Date.now() - start < timeoutMs) {
|
|
383
|
-
|
|
411
|
+
this.sampler.clearCache();
|
|
412
|
+
const grpAlive = await this.sampler.isGroupAlive(pid);
|
|
384
413
|
const procAlive = this.isPidAlive(pid);
|
|
385
414
|
const inGrpAlive = inGroupPids.some((p) => this.isPidAlive(p));
|
|
386
415
|
if (!grpAlive && !procAlive && !inGrpAlive) {
|
|
@@ -388,41 +417,11 @@ export class NodePlatformDriver {
|
|
|
388
417
|
}
|
|
389
418
|
await new Promise((r) => setTimeout(r, 50));
|
|
390
419
|
}
|
|
391
|
-
|
|
420
|
+
this.sampler.clearCache();
|
|
421
|
+
const stillGrp = await this.sampler.isGroupAlive(pid);
|
|
392
422
|
const stillProc = this.isPidAlive(pid);
|
|
393
423
|
const stillInGrp = inGroupPids.some((p) => this.isPidAlive(p));
|
|
394
424
|
return !stillGrp && !stillProc && !stillInGrp;
|
|
395
425
|
}
|
|
396
|
-
parseCpuTimeToMs(timeStr) {
|
|
397
|
-
try {
|
|
398
|
-
let days = 0;
|
|
399
|
-
let rest = timeStr;
|
|
400
|
-
if (rest.includes('-')) {
|
|
401
|
-
const daySplit = rest.split('-');
|
|
402
|
-
days = parseInt(daySplit[0], 10) || 0;
|
|
403
|
-
rest = daySplit[1];
|
|
404
|
-
}
|
|
405
|
-
const parts = rest.split(':');
|
|
406
|
-
let hours = 0;
|
|
407
|
-
let minutes = 0;
|
|
408
|
-
let seconds = 0;
|
|
409
|
-
if (parts.length === 3) {
|
|
410
|
-
hours = parseInt(parts[0], 10) || 0;
|
|
411
|
-
minutes = parseInt(parts[1], 10) || 0;
|
|
412
|
-
seconds = parseFloat(parts[2]) || 0;
|
|
413
|
-
}
|
|
414
|
-
else if (parts.length === 2) {
|
|
415
|
-
minutes = parseInt(parts[0], 10) || 0;
|
|
416
|
-
seconds = parseFloat(parts[1]) || 0;
|
|
417
|
-
}
|
|
418
|
-
else if (parts.length === 1) {
|
|
419
|
-
seconds = parseFloat(parts[0]) || 0;
|
|
420
|
-
}
|
|
421
|
-
return Math.round(days * 86400000 + hours * 3600000 + minutes * 60000 + seconds * 1000);
|
|
422
|
-
}
|
|
423
|
-
catch {
|
|
424
|
-
return 0;
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
426
|
}
|
|
428
427
|
//# sourceMappingURL=node-driver.js.map
|