@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.
Files changed (39) hide show
  1. package/README.md +27 -8
  2. package/dist/domain.d.ts +40 -6
  3. package/dist/domain.d.ts.map +1 -1
  4. package/dist/domain.js +388 -24
  5. package/dist/domain.js.map +1 -1
  6. package/dist/driver/node-driver.d.ts +11 -9
  7. package/dist/driver/node-driver.d.ts.map +1 -1
  8. package/dist/driver/node-driver.js +224 -225
  9. package/dist/driver/node-driver.js.map +1 -1
  10. package/dist/driver/process-facts.d.ts +25 -0
  11. package/dist/driver/process-facts.d.ts.map +1 -0
  12. package/dist/driver/process-facts.js +164 -0
  13. package/dist/driver/process-facts.js.map +1 -0
  14. package/dist/driver/sampler.d.ts +51 -0
  15. package/dist/driver/sampler.d.ts.map +1 -0
  16. package/dist/driver/sampler.js +224 -0
  17. package/dist/driver/sampler.js.map +1 -0
  18. package/dist/driver/types.d.ts +29 -1
  19. package/dist/driver/types.d.ts.map +1 -1
  20. package/dist/recovery/engine.d.ts +2 -0
  21. package/dist/recovery/engine.d.ts.map +1 -1
  22. package/dist/recovery/engine.js +137 -14
  23. package/dist/recovery/engine.js.map +1 -1
  24. package/dist/store/sqlite.d.ts +12 -0
  25. package/dist/store/sqlite.d.ts.map +1 -1
  26. package/dist/store/sqlite.js +162 -12
  27. package/dist/store/sqlite.js.map +1 -1
  28. package/dist/supervisor/supervisor.d.ts +15 -0
  29. package/dist/supervisor/supervisor.d.ts.map +1 -1
  30. package/dist/supervisor/supervisor.js +619 -214
  31. package/dist/supervisor/supervisor.js.map +1 -1
  32. package/dist/testing/contract-suite.d.ts.map +1 -1
  33. package/dist/testing/contract-suite.js +301 -11
  34. package/dist/testing/contract-suite.js.map +1 -1
  35. package/dist/types.d.ts +37 -0
  36. package/dist/types.d.ts.map +1 -1
  37. package/dist/types.js +29 -0
  38. package/dist/types.js.map +1 -1
  39. package/package.json +2 -2
@@ -1,44 +1,106 @@
1
- import { spawn, execFileSync } from 'node:child_process';
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
- // 活跃进程树监控定时器:leaderPid -> Timeout
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
- // envWhiteList 是精确语义:给了什么就是什么,不静默补 PATH。
31
- const childEnv = command.envWhiteList
32
- ? { ...command.envWhiteList }
33
- : command.inheritEnv === false
34
- ? {}
35
- : process.env;
36
- child = spawn(command.execPath, command.args, {
37
- cwd: command.cwd,
38
- env: childEnv,
39
- detached: true,
40
- stdio: [stdinPayload === undefined ? 'ignore' : 'pipe', 'pipe', 'pipe'],
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
- const scanDescendants = () => {
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
- // 3. 跨崩溃/重启进程恢复:查 OS 进程命令行指纹验证真实身份
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
- try {
146
- const cmdLine = execFileSync('ps', ['-p', String(identity.pid), '-o', 'command='], {
147
- encoding: 'utf8',
148
- stdio: ['ignore', 'pipe', 'ignore'],
149
- }).trim();
150
- const baseExec = identity.commandFingerprint.split(':')[0];
151
- if (cmdLine && cmdLine.includes(baseExec)) {
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
- const initialTree = this.getProcessTree(pid);
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: true, scope: 'direct_child' };
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
- const postTree = this.getProcessTree(pid);
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
- // 存在逃逸或残留孤儿,如实标记 stopped: false 与 residualPids
327
+ // 存在逃逸或残留孤儿,如实标记 cannot_determine 与 residualPids
205
328
  return {
206
- stopped: false,
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: true, scope: 'process_group' };
336
+ return { stopped: 'confirmed_stopped', scope: 'process_group' };
214
337
  }
215
- async sampleMetrics(identity) {
216
- try {
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
- rssBytes: totalRssKb * 1024,
245
- pidsCount: count || targetPids.length,
246
- cpuTimeMs: totalCpuMs,
341
+ stopped: 'not_stopped',
342
+ scope: 'unknown',
343
+ errorDetails: 'process groups are not supported on this platform',
247
344
  };
248
345
  }
249
- catch {
250
- return { rssBytes: 0, pidsCount: 0, cpuTimeMs: 0 };
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
- // 扫描可能已由于中间父进程退出导致 PPID 变为 1、但仍同属于 leader 进程组的残留进程
309
- for (const p of allProcs) {
310
- if (p.pgid === leaderPid && !visited.has(p.pid)) {
311
- visited.add(p.pid);
312
- allDescendants.push(p.pid);
313
- inGroupDescendants.push(p.pid);
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
- if (cumSet) {
317
- for (const d of allDescendants) {
318
- cumSet.add(d);
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 { allDescendants, inGroupDescendants, escapedDescendants };
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
- getAllProcesses() {
373
+ async sampleMetrics(identity) {
324
374
  try {
325
- const output = execFileSync('ps', ['-A', '-o', 'pid=,ppid=,pgid=,command='], {
326
- encoding: 'utf8',
327
- stdio: ['ignore', 'pipe', 'ignore'],
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 procs;
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
- try {
375
- process.kill(pid, signal);
399
+ catch (err) {
400
+ if (err && err.code === 'ESRCH') {
401
+ // ESRCH 表示该进程组已不存在或已完全清空,如实返回
402
+ return;
376
403
  }
377
- catch { }
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
- const grpAlive = this.isGroupAlive(pid);
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
- const stillGrp = this.isGroupAlive(pid);
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