@lesliechan721/agent-skill-eval 0.1.0-beta.2 → 0.1.0-beta.21

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 (104) hide show
  1. package/dist/bin.js +167 -39
  2. package/dist/bin.js.map +1 -1
  3. package/dist/codex-eval-app-server.d.ts +199 -0
  4. package/dist/codex-eval-app-server.js +1372 -0
  5. package/dist/codex-eval-app-server.js.map +1 -0
  6. package/dist/codex-eval-cases.d.ts +43 -0
  7. package/dist/codex-eval-cases.js +327 -0
  8. package/dist/codex-eval-cases.js.map +1 -0
  9. package/dist/codex-eval-isolation.d.ts +79 -0
  10. package/dist/codex-eval-isolation.js +420 -0
  11. package/dist/codex-eval-isolation.js.map +1 -0
  12. package/dist/codex-eval-process-tree.d.ts +41 -0
  13. package/dist/codex-eval-process-tree.js +858 -0
  14. package/dist/codex-eval-process-tree.js.map +1 -0
  15. package/dist/codex-eval-runtime-config.d.ts +43 -0
  16. package/dist/codex-eval-runtime-config.js +607 -0
  17. package/dist/codex-eval-runtime-config.js.map +1 -0
  18. package/dist/codex-trigger-eval.d.ts +82 -0
  19. package/dist/codex-trigger-eval.js +790 -0
  20. package/dist/codex-trigger-eval.js.map +1 -0
  21. package/dist/codex-workflow.d.ts +191 -0
  22. package/dist/codex-workflow.js +1832 -0
  23. package/dist/codex-workflow.js.map +1 -0
  24. package/dist/credentials.d.ts +5 -0
  25. package/dist/credentials.js +30 -0
  26. package/dist/credentials.js.map +1 -0
  27. package/dist/custom-process.d.ts +60 -0
  28. package/dist/custom-process.js +424 -0
  29. package/dist/custom-process.js.map +1 -0
  30. package/dist/eval-audit.d.ts +38 -0
  31. package/dist/eval-audit.js +71 -0
  32. package/dist/eval-audit.js.map +1 -0
  33. package/dist/eval-file.d.ts +9 -0
  34. package/dist/eval-file.js +15 -0
  35. package/dist/eval-file.js.map +1 -0
  36. package/dist/eval-process-management.d.ts +22 -0
  37. package/dist/eval-process-management.js +90 -0
  38. package/dist/eval-process-management.js.map +1 -0
  39. package/dist/eval-result.d.ts +79 -0
  40. package/dist/eval-result.js +144 -0
  41. package/dist/eval-result.js.map +1 -0
  42. package/dist/eval-run.d.ts +27 -0
  43. package/dist/eval-run.js +564 -0
  44. package/dist/eval-run.js.map +1 -0
  45. package/dist/eval-spec.d.ts +57 -0
  46. package/dist/eval-spec.js +120 -0
  47. package/dist/eval-spec.js.map +1 -0
  48. package/dist/eval-suite-binding.d.ts +4 -0
  49. package/dist/eval-suite-binding.js +19 -0
  50. package/dist/eval-suite-binding.js.map +1 -0
  51. package/dist/index.d.ts +38 -9
  52. package/dist/index.js +20 -6
  53. package/dist/index.js.map +1 -1
  54. package/dist/json-schema.js +75 -5
  55. package/dist/json-schema.js.map +1 -1
  56. package/dist/output-path.js +2 -2
  57. package/dist/process-tree.d.ts +21 -1
  58. package/dist/process-tree.js +797 -158
  59. package/dist/process-tree.js.map +1 -1
  60. package/dist/process.d.ts +2 -2
  61. package/dist/process.js +99 -34
  62. package/dist/process.js.map +1 -1
  63. package/dist/provider-doctor.d.ts +46 -0
  64. package/dist/provider-doctor.js +120 -0
  65. package/dist/provider-doctor.js.map +1 -0
  66. package/dist/providers.d.ts +83 -0
  67. package/dist/providers.js +632 -0
  68. package/dist/providers.js.map +1 -0
  69. package/dist/run-paths.d.ts +8 -0
  70. package/dist/run-paths.js +71 -0
  71. package/dist/run-paths.js.map +1 -0
  72. package/dist/runtime-skill.d.ts +6 -0
  73. package/dist/runtime-skill.js +87 -3
  74. package/dist/runtime-skill.js.map +1 -1
  75. package/dist/schemas/digests.json +3 -3
  76. package/dist/schemas/eval-audit.schema.json +64 -0
  77. package/dist/schemas/eval-result.schema.json +232 -0
  78. package/dist/schemas/eval-spec.schema.json +109 -0
  79. package/dist/schemas.d.ts +1 -3
  80. package/dist/schemas.js +22 -20
  81. package/dist/schemas.js.map +1 -1
  82. package/dist/selected-cases.d.ts +30 -0
  83. package/dist/selected-cases.js +388 -0
  84. package/dist/selected-cases.js.map +1 -0
  85. package/dist/types.d.ts +2 -34
  86. package/dist/windows-job.d.ts +26 -0
  87. package/dist/windows-job.js +97 -0
  88. package/dist/windows-job.js.map +1 -0
  89. package/package.json +17 -3
  90. package/dist/bundle.d.ts +0 -93
  91. package/dist/bundle.js +0 -216
  92. package/dist/bundle.js.map +0 -1
  93. package/dist/capabilities.d.ts +0 -2
  94. package/dist/capabilities.js +0 -38
  95. package/dist/capabilities.js.map +0 -1
  96. package/dist/plan.d.ts +0 -79
  97. package/dist/plan.js +0 -285
  98. package/dist/plan.js.map +0 -1
  99. package/dist/run.d.ts +0 -2
  100. package/dist/run.js +0 -592
  101. package/dist/run.js.map +0 -1
  102. package/dist/schemas/aw-skill-eval-execution-plan.schema.json +0 -220
  103. package/dist/schemas/eval-run-bundle.schema.json +0 -408
  104. package/dist/schemas/eval-runtime-capabilities.schema.json +0 -85
@@ -1,12 +1,38 @@
1
- import { execFile } from 'node:child_process';
1
+ import { execFile, spawn } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { once } from 'node:events';
2
4
  const DEFAULT_PROCESS_MARKER_ENV = 'AW_NATIVE_CLI_PROCESS_MARKER';
3
5
  const PROCESS_TREE_COMMAND_TIMEOUT_MS = 1_000;
4
- const PROCESS_TREE_LOOKUP_TIMEOUT_MS = 250;
5
- const PROCESS_TREE_OBSERVATION_INTERVAL_MS = 50;
6
+ const POSIX_PROCESS_TREE_COMMAND_TIMEOUT_MS = 2_000;
7
+ const PROCESS_TREE_IDENTITY_INTERVAL_MS = 250;
8
+ const PROCESS_TREE_MARKER_INTERVAL_MS = 1_000;
9
+ const PROCESS_TREE_TERMINATION_GRACE_MS = 250;
10
+ const PROCESS_TREE_TERMINATION_SETTLE_MS = 50;
6
11
  const PROCESS_TREE_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
7
12
  const observationStates = new WeakMap();
13
+ const posixSamplers = new WeakMap();
8
14
  const defaultExecFile = (command, args, options, callback) => execFile(command, args, options, (error, stdout) => callback(error, stdout));
9
- function runCommand(command, args, execFileImpl, timeoutMs) {
15
+ function safeCommandFailureValue(value) {
16
+ if (typeof value === 'number' && Number.isFinite(value))
17
+ return value;
18
+ if (typeof value !== 'string' || !/^[A-Za-z0-9_.-]{1,64}$/u.test(value))
19
+ return undefined;
20
+ return value;
21
+ }
22
+ function commandFailure(reason, error) {
23
+ const details = error && typeof error === 'object'
24
+ ? error
25
+ : {};
26
+ const code = safeCommandFailureValue(details.code);
27
+ const signal = safeCommandFailureValue(details.signal);
28
+ return {
29
+ reason,
30
+ ...(code !== undefined ? { code } : {}),
31
+ ...(typeof signal === 'string' ? { signal } : {}),
32
+ ...(typeof details.killed === 'boolean' ? { killed: details.killed } : {}),
33
+ };
34
+ }
35
+ function runCommandWithDiagnostics(command, args, execFileImpl, timeoutMs) {
10
36
  return new Promise((resolve) => {
11
37
  let settled = false;
12
38
  const finish = (result) => {
@@ -16,70 +42,239 @@ function runCommand(command, args, execFileImpl, timeoutMs) {
16
42
  clearTimeout(timeout);
17
43
  resolve(result);
18
44
  };
19
- const timeout = setTimeout(() => finish(null), timeoutMs);
45
+ const timeout = setTimeout(() => finish({
46
+ output: null,
47
+ failure: commandFailure('deadline-exceeded'),
48
+ }), timeoutMs);
20
49
  timeout.unref();
21
50
  try {
22
51
  execFileImpl(command, args, {
23
52
  timeout: timeoutMs,
24
53
  windowsHide: true,
25
54
  maxBuffer: PROCESS_TREE_MAX_BUFFER_BYTES,
26
- }, (error, stdout) => finish(error ? null : stdout));
55
+ }, (error, stdout) => finish(error
56
+ ? { output: null, failure: commandFailure('callback-error', error) }
57
+ : { output: stdout }));
27
58
  }
28
- catch {
29
- finish(null);
59
+ catch (error) {
60
+ finish({ output: null, failure: commandFailure('synchronous-error', error) });
30
61
  }
31
62
  });
32
63
  }
64
+ function runCommand(command, args, execFileImpl, timeoutMs) {
65
+ return runCommandWithDiagnostics(command, args, execFileImpl, timeoutMs)
66
+ .then((result) => result.output);
67
+ }
33
68
  function parsePosixProcessTable(stdout) {
34
69
  const table = new Map();
35
70
  for (const line of stdout.split('\n')) {
36
- const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.+?)\s*$/u.exec(line);
37
- if (!match)
71
+ const groupMatch = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(.+?)\s*$/u.exec(line);
72
+ if (groupMatch) {
73
+ table.set(Number(groupMatch[1]), {
74
+ ppid: Number(groupMatch[2]),
75
+ pgid: Number(groupMatch[3]),
76
+ state: groupMatch[4],
77
+ startTime: groupMatch[5],
78
+ });
38
79
  continue;
39
- table.set(Number(match[1]), {
40
- ppid: Number(match[2]),
41
- state: match[3],
42
- startTime: match[4],
80
+ }
81
+ const legacyMatch = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.+?)\s*$/u.exec(line);
82
+ if (!legacyMatch)
83
+ continue;
84
+ table.set(Number(legacyMatch[1]), {
85
+ ppid: Number(legacyMatch[2]),
86
+ state: legacyMatch[3],
87
+ startTime: legacyMatch[4],
43
88
  });
44
89
  }
45
90
  return table;
46
91
  }
47
- function parseMarkerProcessIds(stdout, markerEnvironmentVariable, marker) {
92
+ function parsePosixMarkerScan(stdout) {
93
+ const table = new Map();
94
+ const commands = new Map();
95
+ for (const line of stdout.split('\n')) {
96
+ const actual = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S{3}\s+\S{3}\s+\d+\s+\S+\s+\d{4})\s+(.*)$/u.exec(line);
97
+ const synthetic = actual ??
98
+ /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(.*)$/u.exec(line);
99
+ if (!synthetic)
100
+ continue;
101
+ const pid = Number(synthetic[1]);
102
+ table.set(pid, {
103
+ ppid: Number(synthetic[2]),
104
+ pgid: Number(synthetic[3]),
105
+ state: synthetic[4],
106
+ startTime: synthetic[5].replace(/\s+/gu, ' '),
107
+ });
108
+ commands.set(pid, synthetic[6]);
109
+ }
110
+ return { table, commands };
111
+ }
112
+ function parseMarkerProcessIds(scan, markerEnvironmentVariable, marker) {
48
113
  const processIds = new Set();
49
114
  const assignment = `${markerEnvironmentVariable}=${marker}`;
50
- for (const line of stdout.split('\n')) {
51
- const match = /^\s*(\d+)\s+(.+)$/u.exec(line);
52
- if (match?.[2].includes(assignment))
53
- processIds.add(Number(match[1]));
115
+ for (const [pid, command] of scan.commands) {
116
+ if (command.split(/\s+/u).includes(assignment))
117
+ processIds.add(pid);
54
118
  }
55
119
  return processIds;
56
120
  }
57
- async function readPosixProcessTable(execFileImpl, markerEnvironmentVariable, marker, timeoutMs = PROCESS_TREE_LOOKUP_TIMEOUT_MS) {
58
- const [identityOutput, markerOutput] = await Promise.all([
59
- runCommand('ps', ['-A', '-o', 'pid=,ppid=,state=,lstart='], execFileImpl, timeoutMs),
121
+ async function readPosixProcessTable(execFileImpl, markerEnvironmentVariable, marker, timeoutMs = POSIX_PROCESS_TREE_COMMAND_TIMEOUT_MS, confirmUnmarkedPid) {
122
+ const identityArgs = ['-A', '-o', 'pid=,ppid=,pgid=,state=,lstart='];
123
+ const markerArgs = ['eww', '-A', '-o', 'pid=,ppid=,pgid=,state=,lstart=,command='];
124
+ const lookupStartedAt = Date.now();
125
+ const [identityResult, initialMarkerResult] = await Promise.all([
126
+ runCommandWithDiagnostics('ps', identityArgs, execFileImpl, timeoutMs),
60
127
  marker === undefined
61
- ? Promise.resolve('')
62
- : runCommand('ps', ['eww', '-A', '-o', 'pid=,command='], execFileImpl, timeoutMs),
128
+ ? Promise.resolve({ output: '' })
129
+ : runCommandWithDiagnostics('ps', markerArgs, execFileImpl, timeoutMs),
63
130
  ]);
64
- if (identityOutput === null)
131
+ if (identityResult.output === null)
65
132
  return null;
133
+ let markerResult = initialMarkerResult;
134
+ let markerLookupAttempts = marker === undefined ? 0 : 1;
135
+ const retryTimeoutMs = timeoutMs - (Date.now() - lookupStartedAt);
136
+ if (marker !== undefined && markerResult.output === null && retryTimeoutMs > 0) {
137
+ markerLookupAttempts += 1;
138
+ markerResult = await runCommandWithDiagnostics('ps', markerArgs, execFileImpl, retryTimeoutMs);
139
+ }
140
+ const markerOutput = markerResult.output;
141
+ const markerScan = markerOutput === null ? undefined : parsePosixMarkerScan(markerOutput);
142
+ const table = markerScan?.table ?? parsePosixProcessTable(identityResult.output);
143
+ const markerPids = marker === undefined || !markerScan
144
+ ? new Set()
145
+ : parseMarkerProcessIds(markerScan, markerEnvironmentVariable, marker);
146
+ const confirmedUnmarkedPids = new Set();
147
+ const unmarkedEntry = confirmUnmarkedPid === undefined || markerPids.has(confirmUnmarkedPid)
148
+ ? undefined
149
+ : table.get(confirmUnmarkedPid);
150
+ if (unmarkedEntry && !unmarkedEntry.state?.startsWith('Z')) {
151
+ const confirmationOutput = await runCommand('ps', markerArgs, execFileImpl, timeoutMs);
152
+ if (confirmationOutput === null)
153
+ return null;
154
+ const confirmationScan = parsePosixMarkerScan(confirmationOutput);
155
+ const confirmedEntry = confirmationScan.table.get(confirmUnmarkedPid);
156
+ if (confirmedEntry?.startTime === unmarkedEntry.startTime &&
157
+ !parseMarkerProcessIds(confirmationScan, markerEnvironmentVariable, marker).has(confirmUnmarkedPid) &&
158
+ !confirmedEntry.state?.startsWith('Z')) {
159
+ confirmedUnmarkedPids.add(confirmUnmarkedPid);
160
+ }
161
+ }
66
162
  return {
67
- table: parsePosixProcessTable(identityOutput),
68
- markerPids: marker === undefined || markerOutput === null
69
- ? new Set()
70
- : parseMarkerProcessIds(markerOutput, markerEnvironmentVariable, marker),
163
+ table,
164
+ markerPids,
71
165
  markerLookupSucceeded: marker === undefined || markerOutput !== null,
166
+ markerLookupAttempts,
167
+ ...(markerResult.failure ? { markerLookupFailure: markerResult.failure } : {}),
168
+ confirmedUnmarkedPids,
72
169
  };
73
170
  }
171
+ function readPosixIdentityTable(execFileImpl) {
172
+ return runCommandWithDiagnostics('ps', ['-A', '-o', 'pid=,ppid=,pgid=,state=,lstart='], execFileImpl, POSIX_PROCESS_TREE_COMMAND_TIMEOUT_MS);
173
+ }
174
+ function readPosixMarkerOutput(execFileImpl) {
175
+ return runCommandWithDiagnostics('ps', ['eww', '-A', '-o', 'pid=,ppid=,pgid=,state=,lstart=,command='], execFileImpl, POSIX_PROCESS_TREE_COMMAND_TIMEOUT_MS);
176
+ }
177
+ function posixMarkerLookupFailure(snapshot, stage) {
178
+ const fields = [
179
+ `stage=${stage}`,
180
+ `attempts=${snapshot.markerLookupAttempts}`,
181
+ `reason=${snapshot.markerLookupFailure?.reason ?? 'unknown'}`,
182
+ ];
183
+ if (snapshot.markerLookupFailure?.code !== undefined) {
184
+ fields.push(`code=${snapshot.markerLookupFailure.code}`);
185
+ }
186
+ if (snapshot.markerLookupFailure?.signal !== undefined) {
187
+ fields.push(`signal=${snapshot.markerLookupFailure.signal}`);
188
+ }
189
+ if (snapshot.markerLookupFailure?.killed !== undefined) {
190
+ fields.push(`killed=${snapshot.markerLookupFailure.killed}`);
191
+ }
192
+ return cleanupUnconfirmed(`POSIX process marker lookup failed (${fields.join(', ')}).`);
193
+ }
194
+ async function defaultCreateSentinel(markerEnvironmentVariable, marker) {
195
+ const child = spawn(process.execPath, ['-e', 'setInterval(() => {}, 60_000)'], {
196
+ env: { [markerEnvironmentVariable]: marker },
197
+ stdio: 'ignore',
198
+ windowsHide: true,
199
+ });
200
+ await once(child, 'spawn');
201
+ if (child.pid === undefined)
202
+ throw cleanupUnconfirmed('inspection sentinel did not start.');
203
+ return {
204
+ pid: child.pid,
205
+ stop: async () => {
206
+ if (child.exitCode !== null || child.signalCode !== null)
207
+ return;
208
+ const closed = once(child, 'close').then(() => undefined);
209
+ try {
210
+ child.kill('SIGKILL');
211
+ }
212
+ catch {
213
+ // The controlled sentinel can exit before release.
214
+ }
215
+ await closed;
216
+ },
217
+ };
218
+ }
219
+ function validateLeaseSnapshot(snapshot, lease, stage) {
220
+ if (!snapshot.markerLookupSucceeded) {
221
+ return posixMarkerLookupFailure(snapshot, stage);
222
+ }
223
+ const sentinelEntry = snapshot.table.get(lease.sentinel.pid);
224
+ if (sentinelEntry?.startTime !== lease.sentinel.startTime ||
225
+ !snapshot.markerPids.has(lease.sentinel.pid)) {
226
+ return cleanupUnconfirmed('POSIX inspection sentinel could not be verified.');
227
+ }
228
+ return undefined;
229
+ }
230
+ export async function acquireProcessTreeInspectionLease(dependencies = {}) {
231
+ const platform = dependencies.platform ?? process.platform;
232
+ if (platform !== 'darwin' && platform !== 'linux') {
233
+ throw cleanupUnconfirmed(`process inspection is unsupported on ${platform}.`);
234
+ }
235
+ const markerEnvironmentVariable = dependencies.markerEnvironmentVariable ?? DEFAULT_PROCESS_MARKER_ENV;
236
+ const marker = randomUUID();
237
+ const sentinel = await (dependencies.createSentinel ?? defaultCreateSentinel)(markerEnvironmentVariable, marker);
238
+ try {
239
+ const snapshot = await readPosixProcessTable(dependencies.execFile ?? defaultExecFile, markerEnvironmentVariable, marker);
240
+ if (!snapshot)
241
+ throw cleanupUnconfirmed('POSIX process identity lookup failed.');
242
+ if (!snapshot.markerLookupSucceeded) {
243
+ throw posixMarkerLookupFailure(snapshot, 'inspection-preflight');
244
+ }
245
+ const entry = snapshot.table.get(sentinel.pid);
246
+ if (!entry || !snapshot.markerPids.has(sentinel.pid)) {
247
+ throw cleanupUnconfirmed('POSIX inspection sentinel could not be verified.');
248
+ }
249
+ let released = false;
250
+ return {
251
+ marker,
252
+ sentinel: { pid: sentinel.pid, startTime: entry.startTime },
253
+ release: async () => {
254
+ if (released)
255
+ return;
256
+ released = true;
257
+ await sentinel.stop();
258
+ },
259
+ };
260
+ }
261
+ catch (error) {
262
+ await sentinel.stop();
263
+ throw error;
264
+ }
265
+ }
74
266
  function parseWindowsProcessTable(stdout) {
75
267
  const table = new Map();
76
268
  for (const line of stdout.split('\n')) {
77
269
  const match = /^\s*(\d+)\|(\d+)\|(.+?)\s*$/u.exec(line);
78
270
  if (!match)
79
271
  continue;
272
+ const startTime = match[3];
273
+ const creationTime = /^\d+$/u.test(startTime) ? BigInt(startTime) : undefined;
80
274
  table.set(Number(match[1]), {
81
275
  ppid: Number(match[2]),
82
- startTime: match[3],
276
+ startTime,
277
+ ...(creationTime !== undefined ? { creationTime } : {}),
83
278
  });
84
279
  }
85
280
  return table;
@@ -92,7 +287,62 @@ function readWindowsProcessTable(execFileImpl) {
92
287
  ].join(' ');
93
288
  return runCommand('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], execFileImpl, PROCESS_TREE_COMMAND_TIMEOUT_MS).then((stdout) => stdout === null ? null : parseWindowsProcessTable(stdout));
94
289
  }
95
- function refreshDescendants(root, knownDescendants, table) {
290
+ function collectDescendantProcessIds(rootPid, table) {
291
+ const childrenByParent = new Map();
292
+ for (const [processId, entry] of table) {
293
+ const children = childrenByParent.get(entry.ppid) ?? [];
294
+ children.push(processId);
295
+ childrenByParent.set(entry.ppid, children);
296
+ }
297
+ const descendants = new Set();
298
+ const visited = new Set([rootPid]);
299
+ const pending = [rootPid];
300
+ while (pending.length > 0) {
301
+ const parent = pending.shift();
302
+ for (const childPid of childrenByParent.get(parent) ?? []) {
303
+ if (visited.has(childPid))
304
+ continue;
305
+ visited.add(childPid);
306
+ descendants.add(childPid);
307
+ pending.push(childPid);
308
+ }
309
+ }
310
+ return descendants;
311
+ }
312
+ function collectComparableWindowsDescendantProcessIds(root, table) {
313
+ if (root?.creationTime === undefined)
314
+ return new Set();
315
+ const childrenByParent = new Map();
316
+ for (const [processId, entry] of table) {
317
+ const children = childrenByParent.get(entry.ppid) ?? [];
318
+ children.push(processId);
319
+ childrenByParent.set(entry.ppid, children);
320
+ }
321
+ const descendants = new Set();
322
+ const visited = new Set([root.pid]);
323
+ const pending = [root];
324
+ while (pending.length > 0) {
325
+ const parent = pending.shift();
326
+ for (const childPid of childrenByParent.get(parent.pid) ?? []) {
327
+ if (visited.has(childPid))
328
+ continue;
329
+ visited.add(childPid);
330
+ const entry = table.get(childPid);
331
+ if (entry.creationTime === undefined ||
332
+ parent.creationTime === undefined ||
333
+ entry.creationTime < parent.creationTime)
334
+ continue;
335
+ descendants.add(childPid);
336
+ pending.push({
337
+ pid: childPid,
338
+ startTime: entry.startTime,
339
+ creationTime: entry.creationTime,
340
+ });
341
+ }
342
+ }
343
+ return descendants;
344
+ }
345
+ function refreshDescendants(root, knownDescendants, table, requireComparableCreationOrder = false) {
96
346
  const childrenByParent = new Map();
97
347
  for (const [processId, entry] of table) {
98
348
  const children = childrenByParent.get(entry.ppid) ?? [];
@@ -100,20 +350,30 @@ function refreshDescendants(root, knownDescendants, table) {
100
350
  childrenByParent.set(entry.ppid, children);
101
351
  }
102
352
  const roots = [root, ...knownDescendants.values()].filter((identity) => identity !== undefined && table.get(identity.pid)?.startTime === identity.startTime);
103
- const pending = roots.map((identity) => identity.pid);
104
- const visited = new Set(pending);
353
+ const pending = [...roots];
354
+ const visited = new Set(pending.map((identity) => identity.pid));
105
355
  while (pending.length > 0) {
106
356
  const parent = pending.shift();
107
- for (const descendantPid of childrenByParent.get(parent) ?? []) {
357
+ for (const descendantPid of childrenByParent.get(parent.pid) ?? []) {
108
358
  if (visited.has(descendantPid))
109
359
  continue;
110
360
  visited.add(descendantPid);
111
361
  const entry = table.get(descendantPid);
362
+ if (requireComparableCreationOrder &&
363
+ (parent.creationTime === undefined ||
364
+ entry.creationTime === undefined ||
365
+ entry.creationTime < parent.creationTime))
366
+ continue;
112
367
  const knownIdentity = knownDescendants.get(descendantPid);
113
368
  if (knownIdentity && knownIdentity.startTime !== entry.startTime)
114
369
  continue;
115
- knownDescendants.set(descendantPid, { pid: descendantPid, startTime: entry.startTime });
116
- pending.push(descendantPid);
370
+ const identity = {
371
+ pid: descendantPid,
372
+ startTime: entry.startTime,
373
+ ...(entry.creationTime !== undefined ? { creationTime: entry.creationTime } : {}),
374
+ };
375
+ knownDescendants.set(descendantPid, identity);
376
+ pending.push(identity);
117
377
  }
118
378
  }
119
379
  }
@@ -123,32 +383,347 @@ function refreshObservedProcesses(state, table, markerPids = new Set(), allowRoo
123
383
  allowRootWithoutMarker ||
124
384
  (state.rootDiscoveryPending && (state.platform === 'win32' || state.marker === undefined)))) {
125
385
  const entry = table.get(state.pid);
126
- if (entry)
127
- state.root = { pid: state.pid, startTime: entry.startTime };
386
+ if (entry && (state.platform !== 'win32' || entry.creationTime !== undefined)) {
387
+ state.root = {
388
+ pid: state.pid,
389
+ startTime: entry.startTime,
390
+ ...(entry.creationTime !== undefined ? { creationTime: entry.creationTime } : {}),
391
+ };
392
+ }
128
393
  }
129
394
  state.rootDiscoveryPending = false;
130
395
  for (const processId of markerPids) {
396
+ if (processId === state.lease?.sentinel.pid)
397
+ continue;
131
398
  if (processId === state.root?.pid)
132
399
  continue;
133
400
  const entry = table.get(processId);
134
401
  if (!entry)
135
402
  continue;
403
+ if (state.platform === 'win32' && entry.creationTime === undefined)
404
+ continue;
136
405
  const known = state.descendants.get(processId);
137
406
  if (!known || known.startTime === entry.startTime) {
138
- state.descendants.set(processId, { pid: processId, startTime: entry.startTime });
407
+ state.descendants.set(processId, {
408
+ pid: processId,
409
+ startTime: entry.startTime,
410
+ ...(entry.creationTime !== undefined ? { creationTime: entry.creationTime } : {}),
411
+ });
412
+ }
413
+ }
414
+ if (state.platform === 'win32' || state.marker === undefined) {
415
+ refreshDescendants(state.root, state.descendants, table, state.platform === 'win32');
416
+ }
417
+ }
418
+ function latchObservationFailure(state, error) {
419
+ if (state.failure)
420
+ return;
421
+ state.failure = error;
422
+ for (const listener of state.failureListeners)
423
+ listener(error);
424
+ }
425
+ function acceptPosixSnapshot(state, snapshot, stage) {
426
+ if (!snapshot.markerLookupSucceeded && state.marker !== undefined) {
427
+ latchObservationFailure(state, posixMarkerLookupFailure(snapshot, stage));
428
+ return false;
429
+ }
430
+ if (state.lease) {
431
+ const leaseFailure = validateLeaseSnapshot(snapshot, state.lease, stage);
432
+ if (leaseFailure) {
433
+ latchObservationFailure(state, leaseFailure);
434
+ return false;
435
+ }
436
+ }
437
+ const rootEntry = snapshot.table.get(state.pid);
438
+ let confirmedUnmarkedRoot = snapshot.confirmedUnmarkedPids.has(state.pid);
439
+ if (state.marker !== undefined &&
440
+ rootEntry !== undefined &&
441
+ !rootEntry.state?.startsWith('Z') &&
442
+ (state.root === undefined || state.root.startTime === rootEntry.startTime) &&
443
+ !snapshot.markerPids.has(state.pid)) {
444
+ confirmedUnmarkedRoot ||=
445
+ state.unmarkedRootCandidate?.startTime === rootEntry.startTime;
446
+ state.unmarkedRootCandidate = { pid: state.pid, startTime: rootEntry.startTime };
447
+ }
448
+ else {
449
+ state.unmarkedRootCandidate = undefined;
450
+ }
451
+ if (state.marker !== undefined &&
452
+ state.dependencies.enforceCooperativeProcessGroup === true &&
453
+ rootEntry !== undefined &&
454
+ !rootEntry.state?.startsWith('Z') &&
455
+ confirmedUnmarkedRoot) {
456
+ latchObservationFailure(state, cleanupUnconfirmed('custom process contract violation: a live provider process lost its process marker.'));
457
+ }
458
+ refreshObservedProcesses(state, snapshot.table, snapshot.markerPids);
459
+ if (state.marker !== undefined) {
460
+ const providerMarkerPids = [...snapshot.markerPids].filter((pid) => pid !== state.lease?.sentinel.pid);
461
+ if (state.rootPgidMode === undefined) {
462
+ if (state.root?.pid === state.pid &&
463
+ rootEntry?.startTime === state.root.startTime &&
464
+ rootEntry.pgid !== undefined &&
465
+ snapshot.markerPids.has(state.pid)) {
466
+ if (rootEntry.pgid === state.pid) {
467
+ state.rootPgid = rootEntry.pgid;
468
+ state.rootPgidMode = 'owned';
469
+ }
470
+ else {
471
+ state.rootPgidMode = 'shared';
472
+ if (state.dependencies.enforceCooperativeProcessGroup === true) {
473
+ latchObservationFailure(state, cleanupUnconfirmed('custom process contract violation: the provider root left its initial process group.'));
474
+ }
475
+ }
476
+ }
477
+ else if (rootEntry === undefined &&
478
+ (providerMarkerPids.some((pid) => snapshot.table.get(pid)?.pgid !== undefined) ||
479
+ [...snapshot.table].some(([pid, entry]) => pid !== state.lease?.sentinel.pid &&
480
+ entry.pgid === state.pid &&
481
+ !entry.state?.startsWith('Z')))) {
482
+ state.rootPgid = state.pid;
483
+ state.rootPgidMode = 'missing';
484
+ }
485
+ }
486
+ const expectedPgid = state.rootPgidMode === 'owned'
487
+ ? state.rootPgid
488
+ : state.rootPgidMode === 'missing'
489
+ ? state.pid
490
+ : undefined;
491
+ if (state.dependencies.enforceCooperativeProcessGroup === true &&
492
+ expectedPgid !== undefined &&
493
+ providerMarkerPids.some((pid) => {
494
+ const entry = snapshot.table.get(pid);
495
+ return entry?.pgid !== undefined && entry.pgid !== expectedPgid;
496
+ })) {
497
+ latchObservationFailure(state, cleanupUnconfirmed('custom process contract violation: a marked process left its initial process group.'));
498
+ }
499
+ const liveKnownIdentities = [state.root, ...state.descendants.values()].filter((identity) => identityMatches(identity, snapshot.table) &&
500
+ !snapshot.table.get(identity.pid)?.state?.startsWith('Z'));
501
+ const markerlessKnownIdentity = liveKnownIdentities.some((identity) => !snapshot.markerPids.has(identity.pid));
502
+ let markerlessInitialGroupMember = false;
503
+ const groupLeader = state.rootPgid === undefined
504
+ ? undefined
505
+ : snapshot.table.get(state.rootPgid);
506
+ const groupLeaderWasReused = state.rootPgidMode === 'owned' &&
507
+ groupLeader !== undefined &&
508
+ groupLeader.startTime !== state.root?.startTime;
509
+ if (state.rootPgid !== undefined && !groupLeaderWasReused) {
510
+ for (const [pid, entry] of snapshot.table) {
511
+ if (entry.pgid !== state.rootPgid || entry.state?.startsWith('Z'))
512
+ continue;
513
+ if (pid !== state.pid && !snapshot.markerPids.has(pid)) {
514
+ markerlessInitialGroupMember = true;
515
+ }
516
+ }
517
+ }
518
+ if (state.dependencies.enforceCooperativeProcessGroup === true &&
519
+ (markerlessKnownIdentity || markerlessInitialGroupMember)) {
520
+ latchObservationFailure(state, cleanupUnconfirmed('custom process contract violation: a live provider process lost its process marker.'));
139
521
  }
140
522
  }
141
- refreshDescendants(state.root, state.descendants, table);
523
+ return true;
524
+ }
525
+ function getPosixProcessSampler(execFileImpl) {
526
+ const existing = posixSamplers.get(execFileImpl);
527
+ if (existing)
528
+ return existing;
529
+ const sampler = {
530
+ execFile: execFileImpl,
531
+ states: new Set(),
532
+ identityScannedAt: Number.NEGATIVE_INFINITY,
533
+ markerScannedAt: Number.NEGATIVE_INFINITY,
534
+ };
535
+ posixSamplers.set(execFileImpl, sampler);
536
+ return sampler;
142
537
  }
143
- function createProcessTreeObservation(pid, marker, dependencies, startLookup) {
538
+ function snapshotFromSampler(state, sampler) {
539
+ const markerLookupSucceeded = state.marker === undefined || sampler.markerFailure === undefined;
540
+ const table = state.marker === undefined || !markerLookupSucceeded
541
+ ? sampler.identityTable
542
+ : sampler.markerScan?.table;
543
+ if (!table)
544
+ return undefined;
545
+ const markerPids = state.marker === undefined || !markerLookupSucceeded || sampler.markerScan === undefined
546
+ ? new Set()
547
+ : parseMarkerProcessIds(sampler.markerScan, state.markerEnvironmentVariable, state.marker);
548
+ return {
549
+ table,
550
+ markerPids,
551
+ markerLookupSucceeded,
552
+ markerLookupAttempts: state.marker === undefined ? 0 : 1,
553
+ ...(sampler.markerFailure ? { markerLookupFailure: sampler.markerFailure } : {}),
554
+ confirmedUnmarkedPids: new Set(),
555
+ };
556
+ }
557
+ function deliverSamplerIdentity(sampler) {
558
+ for (const state of sampler.states) {
559
+ if (state.disposed)
560
+ continue;
561
+ if (sampler.identityFailure) {
562
+ latchObservationFailure(state, cleanupUnconfirmed('POSIX process identity lookup failed.'));
563
+ continue;
564
+ }
565
+ if (state.marker !== undefined) {
566
+ if (sampler.identityTable) {
567
+ refreshDescendants(state.root, state.descendants, sampler.identityTable);
568
+ }
569
+ const expectedPgid = state.rootPgidMode === 'owned'
570
+ ? state.rootPgid
571
+ : state.rootPgidMode === 'missing'
572
+ ? state.pid
573
+ : undefined;
574
+ if (state.dependencies.enforceCooperativeProcessGroup === true &&
575
+ expectedPgid !== undefined &&
576
+ [state.root, ...state.descendants.values()].some((identity) => {
577
+ if (!identity || !sampler.identityTable)
578
+ return false;
579
+ const entry = sampler.identityTable.get(identity.pid);
580
+ return entry?.startTime === identity.startTime &&
581
+ entry.pgid !== undefined && entry.pgid !== expectedPgid;
582
+ })) {
583
+ latchObservationFailure(state, cleanupUnconfirmed('custom process contract violation: a marked process left its initial process group.'));
584
+ }
585
+ continue;
586
+ }
587
+ const snapshot = snapshotFromSampler(state, sampler);
588
+ if (snapshot)
589
+ acceptPosixSnapshot(state, snapshot, 'observation');
590
+ }
591
+ }
592
+ function deliverSamplerMarkers(sampler, stage) {
593
+ for (const state of sampler.states) {
594
+ if (state.disposed || state.marker === undefined)
595
+ continue;
596
+ const snapshot = snapshotFromSampler(state, sampler);
597
+ if (snapshot)
598
+ acceptPosixSnapshot(state, snapshot, stage);
599
+ }
600
+ }
601
+ function scanSamplerIdentity(sampler, force) {
602
+ if (sampler.identityInFlight)
603
+ return sampler.identityInFlight;
604
+ const now = Date.now();
605
+ if (!force &&
606
+ Number.isFinite(sampler.identityScannedAt) &&
607
+ now - sampler.identityScannedAt < PROCESS_TREE_IDENTITY_INTERVAL_MS)
608
+ return Promise.resolve();
609
+ sampler.identityScannedAt = now;
610
+ sampler.identityInFlight = readPosixIdentityTable(sampler.execFile)
611
+ .then((result) => {
612
+ sampler.identityFailure = result.failure;
613
+ sampler.identityTable = result.output === null
614
+ ? undefined
615
+ : parsePosixProcessTable(result.output);
616
+ deliverSamplerIdentity(sampler);
617
+ })
618
+ .finally(() => {
619
+ sampler.identityInFlight = undefined;
620
+ });
621
+ return sampler.identityInFlight;
622
+ }
623
+ function runMarkerScan(sampler) {
624
+ sampler.markerScannedAt = Date.now();
625
+ sampler.markerInFlight = readPosixMarkerOutput(sampler.execFile)
626
+ .then((result) => {
627
+ sampler.markerFailure = result.failure;
628
+ sampler.markerScan = result.output === null
629
+ ? undefined
630
+ : parsePosixMarkerScan(result.output);
631
+ deliverSamplerMarkers(sampler, 'observation');
632
+ })
633
+ .finally(() => {
634
+ sampler.markerInFlight = undefined;
635
+ });
636
+ return sampler.markerInFlight;
637
+ }
638
+ function scanSamplerMarkers(sampler, force) {
639
+ if (sampler.markerInFlight)
640
+ return sampler.markerInFlight;
641
+ const now = Date.now();
642
+ if (!force &&
643
+ Number.isFinite(sampler.markerScannedAt) &&
644
+ now - sampler.markerScannedAt < PROCESS_TREE_MARKER_INTERVAL_MS)
645
+ return Promise.resolve();
646
+ return runMarkerScan(sampler);
647
+ }
648
+ function schedulePosixSampler(sampler) {
649
+ if (sampler.timer)
650
+ clearTimeout(sampler.timer);
651
+ sampler.timer = undefined;
652
+ if (sampler.states.size === 0)
653
+ return;
654
+ const now = Date.now();
655
+ const identityDelay = Math.max(0, PROCESS_TREE_IDENTITY_INTERVAL_MS - (now - sampler.identityScannedAt));
656
+ const markerDelay = [...sampler.states].some((state) => state.marker !== undefined)
657
+ ? Math.max(0, PROCESS_TREE_MARKER_INTERVAL_MS - (now - sampler.markerScannedAt))
658
+ : Number.POSITIVE_INFINITY;
659
+ const delayMs = Math.min(identityDelay, markerDelay);
660
+ sampler.timer = setTimeout(() => {
661
+ sampler.timer = undefined;
662
+ const current = Date.now();
663
+ const scans = [];
664
+ if (current - sampler.identityScannedAt >= PROCESS_TREE_IDENTITY_INTERVAL_MS) {
665
+ scans.push(scanSamplerIdentity(sampler, false));
666
+ }
667
+ if (markerDelay !== Number.POSITIVE_INFINITY &&
668
+ current - sampler.markerScannedAt >= PROCESS_TREE_MARKER_INTERVAL_MS) {
669
+ scans.push(scanSamplerMarkers(sampler, false));
670
+ }
671
+ void Promise.all(scans).finally(() => schedulePosixSampler(sampler));
672
+ }, delayMs);
673
+ sampler.timer.unref();
674
+ }
675
+ function stopObservingPosixState(state) {
676
+ const sampler = state.sampler;
677
+ if (!sampler)
678
+ return;
679
+ state.sampler = undefined;
680
+ sampler.states.delete(state);
681
+ schedulePosixSampler(sampler);
682
+ }
683
+ async function readImmediatePosixSnapshot(state, stage) {
684
+ const sampler = state.sampler ?? getPosixProcessSampler(state.dependencies.execFile ?? defaultExecFile);
685
+ if (state.marker === undefined)
686
+ await scanSamplerIdentity(sampler, true);
687
+ else
688
+ await scanSamplerMarkers(sampler, true);
689
+ if (state.marker === undefined && sampler.identityFailure) {
690
+ latchObservationFailure(state, cleanupUnconfirmed('POSIX process identity lookup failed.'));
691
+ return undefined;
692
+ }
693
+ if (state.marker !== undefined && sampler.markerFailure) {
694
+ latchObservationFailure(state, posixMarkerLookupFailure({
695
+ table: new Map(),
696
+ markerPids: new Set(),
697
+ markerLookupSucceeded: false,
698
+ markerLookupAttempts: 1,
699
+ markerLookupFailure: sampler.markerFailure,
700
+ confirmedUnmarkedPids: new Set(),
701
+ }, stage));
702
+ return undefined;
703
+ }
704
+ const snapshot = snapshotFromSampler(state, sampler);
705
+ if (!snapshot)
706
+ return undefined;
707
+ acceptPosixSnapshot(state, snapshot, stage);
708
+ return snapshot;
709
+ }
710
+ function createProcessTreeObservation(pid, marker, dependencies, startLookup, lease) {
144
711
  const observation = {
145
712
  pid,
713
+ onFailure: (listener) => {
714
+ const state = observationStates.get(observation);
715
+ if (!state)
716
+ return () => undefined;
717
+ state.failureListeners.add(listener);
718
+ if (state.failure)
719
+ listener(state.failure);
720
+ return () => state.failureListeners.delete(listener);
721
+ },
146
722
  dispose: () => {
147
723
  const state = observationStates.get(observation);
148
724
  if (state) {
149
725
  state.disposed = true;
150
- if (state.pollTimer)
151
- clearTimeout(state.pollTimer);
726
+ stopObservingPosixState(state);
152
727
  }
153
728
  },
154
729
  };
@@ -157,9 +732,12 @@ function createProcessTreeObservation(pid, marker, dependencies, startLookup) {
157
732
  platform: dependencies.platform ?? process.platform,
158
733
  marker,
159
734
  markerEnvironmentVariable: dependencies.markerEnvironmentVariable ?? DEFAULT_PROCESS_MARKER_ENV,
735
+ lease,
160
736
  dependencies,
161
737
  rootDiscoveryPending: true,
738
+ initialWindowsLookupSucceeded: false,
162
739
  descendants: new Map(),
740
+ failureListeners: new Set(),
163
741
  disposed: false,
164
742
  initialLookup: Promise.resolve(),
165
743
  };
@@ -167,48 +745,45 @@ function createProcessTreeObservation(pid, marker, dependencies, startLookup) {
167
745
  if (!startLookup)
168
746
  return observation;
169
747
  const execFileImpl = dependencies.execFile ?? defaultExecFile;
170
- const refresh = () => state.platform === 'win32'
171
- ? readWindowsProcessTable(execFileImpl).then((table) => {
172
- if (!state.disposed && table)
748
+ if (state.platform === 'win32') {
749
+ state.initialLookup = readWindowsProcessTable(execFileImpl).then((table) => {
750
+ if (!state.disposed && table) {
751
+ state.initialWindowsLookupSucceeded = true;
173
752
  refreshObservedProcesses(state, table);
174
- })
175
- : readPosixProcessTable(execFileImpl, state.markerEnvironmentVariable, marker).then((snapshot) => {
176
- if (!state.disposed && snapshot) {
177
- refreshObservedProcesses(state, snapshot.table, snapshot.markerPids, !snapshot.markerLookupSucceeded);
178
753
  }
179
754
  });
180
- const scheduleRefresh = () => {
755
+ return observation;
756
+ }
757
+ const sampler = getPosixProcessSampler(execFileImpl);
758
+ const periodic = dependencies.enforceCooperativeProcessGroup === true;
759
+ if (periodic) {
760
+ state.sampler = sampler;
761
+ sampler.states.add(state);
762
+ }
763
+ state.initialLookup = (marker === undefined
764
+ ? scanSamplerIdentity(sampler, true)
765
+ : Promise.all([
766
+ ...(periodic ? [scanSamplerIdentity(sampler, true)] : []),
767
+ scanSamplerMarkers(sampler, true),
768
+ ])).then(() => {
181
769
  if (state.disposed)
182
770
  return;
183
- state.pollTimer = setTimeout(() => {
184
- state.pollTimer = undefined;
185
- void refresh().finally(scheduleRefresh);
186
- }, PROCESS_TREE_OBSERVATION_INTERVAL_MS);
187
- state.pollTimer.unref();
188
- };
189
- state.initialLookup = refresh();
190
- if (state.platform !== 'win32')
191
- void state.initialLookup.finally(scheduleRefresh);
771
+ if (periodic)
772
+ return;
773
+ const snapshot = snapshotFromSampler(state, sampler);
774
+ if (snapshot)
775
+ acceptPosixSnapshot(state, snapshot, 'observation');
776
+ });
777
+ if (periodic)
778
+ void state.initialLookup.finally(() => schedulePosixSampler(sampler));
192
779
  return observation;
193
780
  }
194
- export function observeProcessTree(pid, marker, dependencies = {}) {
195
- return createProcessTreeObservation(pid, marker, dependencies, true);
781
+ export function observeProcessTree(pid, marker, dependencies = {}, lease) {
782
+ return createProcessTreeObservation(pid, marker, dependencies, true, lease);
196
783
  }
197
784
  export async function assertProcessTreeInspectionAvailable(dependencies = {}) {
198
- const platform = dependencies.platform ?? process.platform;
199
- const execFileImpl = dependencies.execFile ?? defaultExecFile;
200
- if (platform === 'win32') {
201
- if (!(await readWindowsProcessTable(execFileImpl))) {
202
- throw cleanupUnconfirmed('Windows process identity lookup failed.');
203
- }
204
- return;
205
- }
206
- const snapshot = await readPosixProcessTable(execFileImpl, dependencies.markerEnvironmentVariable ?? DEFAULT_PROCESS_MARKER_ENV, 'preflight-marker');
207
- if (!snapshot)
208
- throw cleanupUnconfirmed('POSIX process identity lookup failed.');
209
- if (!snapshot.markerLookupSucceeded) {
210
- throw cleanupUnconfirmed('POSIX process marker lookup failed.');
211
- }
785
+ const lease = await acquireProcessTreeInspectionLease(dependencies);
786
+ await lease.release();
212
787
  }
213
788
  function cleanupUnconfirmed(detail) {
214
789
  return new Error(`Process tree cleanup unconfirmed: ${detail}`);
@@ -229,10 +804,52 @@ function signalIdentity(identity, snapshot, signal, kill) {
229
804
  // The process can exit between identity verification and the signal.
230
805
  }
231
806
  }
807
+ function safelyOwnedProcessGroup(state, snapshot) {
808
+ if (state.marker === undefined ||
809
+ state.rootPgid === undefined ||
810
+ state.rootPgid !== state.pid)
811
+ return undefined;
812
+ const pgid = state.rootPgid;
813
+ const members = [...snapshot.table].filter(([, entry]) => entry.pgid === pgid && !entry.state?.startsWith('Z'));
814
+ if (members.length === 0)
815
+ return undefined;
816
+ if (state.rootPgidMode === 'owned') {
817
+ const currentLeader = snapshot.table.get(pgid);
818
+ if (currentLeader !== undefined &&
819
+ currentLeader.startTime !== state.root?.startTime)
820
+ return undefined;
821
+ return pgid;
822
+ }
823
+ const known = [state.root, ...state.descendants.values()];
824
+ if (members.some(([pid, entry]) => !known.some((identity) => identity?.pid === pid && identity.startTime === entry.startTime)))
825
+ return undefined;
826
+ return pgid;
827
+ }
828
+ function signalProcessGroup(pgid, signal, kill) {
829
+ if (pgid === undefined)
830
+ return;
831
+ try {
832
+ kill(-pgid, signal);
833
+ }
834
+ catch {
835
+ // The verified process group can exit before the signal.
836
+ }
837
+ }
838
+ function latchLiveDescendantCompletionFailure(state, snapshot) {
839
+ if (state.dependencies.enforceCooperativeProcessGroup !== true)
840
+ return;
841
+ const liveDescendant = [...state.descendants.values()].some((identity) => identity.pid !== state.pid &&
842
+ identity.pid !== state.lease?.sentinel.pid &&
843
+ posixIdentityMatches(identity, snapshot) &&
844
+ !snapshot.table.get(identity.pid)?.state?.startsWith('Z'));
845
+ if (!liveDescendant)
846
+ return;
847
+ latchObservationFailure(state, cleanupUnconfirmed('custom process contract violation: all provider descendants must exit before completion.'));
848
+ }
232
849
  async function runTaskkill(pid, execFileImpl) {
233
850
  return (await runCommand('taskkill', ['/PID', String(pid), '/T', '/F'], execFileImpl, PROCESS_TREE_COMMAND_TIMEOUT_MS)) !== null;
234
851
  }
235
- async function terminateWindowsProcessTree(state, requireConfirmedCleanup) {
852
+ async function terminateWindowsProcessTree(state, requireConfirmedCleanup, allowUnobservedProcessOnNormalCompletion) {
236
853
  const execFileImpl = state.dependencies.execFile ?? defaultExecFile;
237
854
  if (!requireConfirmedCleanup) {
238
855
  if (!(await runTaskkill(state.pid, execFileImpl))) {
@@ -240,122 +857,144 @@ async function terminateWindowsProcessTree(state, requireConfirmedCleanup) {
240
857
  }
241
858
  return;
242
859
  }
860
+ const identitiesBeforeNormalCompletion = allowUnobservedProcessOnNormalCompletion
861
+ ? {
862
+ root: state.root,
863
+ descendants: new Map(state.descendants),
864
+ }
865
+ : undefined;
243
866
  await state.initialLookup;
244
867
  const table = await readWindowsProcessTable(execFileImpl);
245
868
  if (!table) {
246
869
  throw cleanupUnconfirmed('Windows process identity lookup failed.');
247
870
  }
248
871
  refreshObservedProcesses(state, table);
249
- const rootTarget = identityMatches(state.root, table) ? state.root : undefined;
250
- const liveDescendants = [...state.descendants.values()].filter((identity) => identityMatches(identity, table));
872
+ const root = identitiesBeforeNormalCompletion
873
+ ? identitiesBeforeNormalCompletion.root
874
+ : state.root;
875
+ const descendants = identitiesBeforeNormalCompletion?.descendants ?? state.descendants;
876
+ const observedIdentities = [root, ...descendants.values()].filter((identity) => identity !== undefined);
877
+ const candidatePids = collectDescendantProcessIds(state.pid, table);
878
+ const comparableCandidatePids = collectComparableWindowsDescendantProcessIds(root, table);
879
+ const hasInvalidCandidateEdge = [...candidatePids].some((pid) => !comparableCandidatePids.has(pid));
880
+ const hasUnobservedCandidate = [...candidatePids].some((pid) => !identityMatches(descendants.get(pid), table));
881
+ const observedPidWasReused = (candidateTable) => observedIdentities.some((identity) => {
882
+ const entry = candidateTable.get(identity.pid);
883
+ return entry !== undefined && entry.startTime !== identity.startTime;
884
+ });
885
+ const rootTarget = identityMatches(root, table) ? root : undefined;
886
+ const liveDescendants = [...descendants.values()].filter((identity) => identityMatches(identity, table));
251
887
  const liveDescendantPids = new Set(liveDescendants.map((identity) => identity.pid));
252
888
  const targets = rootTarget
253
889
  ? [rootTarget]
254
890
  : liveDescendants.filter((identity) => !liveDescendantPids.has(table.get(identity.pid).ppid));
255
- const taskkillResults = await Promise.all(targets.map((identity) => runTaskkill(identity.pid, execFileImpl)));
891
+ const taskkillResults = hasInvalidCandidateEdge || hasUnobservedCandidate
892
+ ? []
893
+ : await Promise.all(targets.map((identity) => runTaskkill(identity.pid, execFileImpl)));
256
894
  if (taskkillResults.some((result) => !result)) {
257
895
  throw cleanupUnconfirmed('taskkill did not confirm termination.');
258
896
  }
259
897
  const verifiedTable = await readWindowsProcessTable(execFileImpl);
260
898
  if (!verifiedTable)
261
899
  throw cleanupUnconfirmed('Windows termination verification failed.');
900
+ const verifiedCandidatePids = collectDescendantProcessIds(state.pid, verifiedTable);
262
901
  const verifiedIdentities = [rootTarget, ...liveDescendants].filter((identity) => identity !== undefined);
263
902
  if (verifiedIdentities.some((identity) => identityMatches(identity, verifiedTable))) {
264
903
  throw cleanupUnconfirmed('a verified Windows process is still running.');
265
904
  }
266
- if (requireConfirmedCleanup && targets.length === 0) {
267
- throw cleanupUnconfirmed('no inherited-pipe process identity was observed.');
905
+ if (hasInvalidCandidateEdge) {
906
+ throw cleanupUnconfirmed(hasUnobservedCandidate
907
+ ? 'an unobserved Windows descendant candidate has invalid process creation order.'
908
+ : 'a Windows descendant candidate has invalid process creation order.');
909
+ }
910
+ if (hasUnobservedCandidate) {
911
+ throw cleanupUnconfirmed('an unobserved Windows descendant candidate was found after the root exited.');
912
+ }
913
+ if (verifiedCandidatePids.size > 0) {
914
+ throw cleanupUnconfirmed('a Windows descendant candidate is still running.');
915
+ }
916
+ const unobservedNormalCompletionIsAllowed = allowUnobservedProcessOnNormalCompletion &&
917
+ state.initialWindowsLookupSucceeded &&
918
+ !table.has(state.pid) &&
919
+ candidatePids.size === 0 &&
920
+ !verifiedTable.has(state.pid) &&
921
+ verifiedCandidatePids.size === 0;
922
+ if (requireConfirmedCleanup &&
923
+ targets.length === 0 &&
924
+ (observedPidWasReused(table) ||
925
+ observedPidWasReused(verifiedTable) ||
926
+ (observedIdentities.length === 0 && !unobservedNormalCompletionIsAllowed))) {
927
+ throw cleanupUnconfirmed(observedIdentities.length > 0
928
+ ? 'an observed Windows process identifier was reused.'
929
+ : 'no inherited-pipe process identity was observed.');
268
930
  }
269
931
  }
270
- async function terminatePosixProcessTree(state, requireConfirmedCleanup, allowInspectionUnavailable) {
932
+ async function terminatePosixProcessTree(state, requireConfirmedCleanup, allowInspectionUnavailable, requireDescendantsExitedBeforeCompletion) {
271
933
  await state.initialLookup;
272
- const execFileImpl = state.dependencies.execFile ?? defaultExecFile;
934
+ state.disposed = true;
935
+ stopObservingPosixState(state);
273
936
  const kill = state.dependencies.kill ?? process.kill.bind(process);
274
- const deadline = Date.now() + PROCESS_TREE_COMMAND_TIMEOUT_MS;
275
- let snapshot = await readPosixProcessTable(execFileImpl, state.markerEnvironmentVariable, state.marker);
937
+ let snapshot = await readImmediatePosixSnapshot(state, 'termination-start');
276
938
  if (!snapshot) {
277
939
  if (!requireConfirmedCleanup && allowInspectionUnavailable)
278
940
  return;
279
- throw cleanupUnconfirmed('POSIX process identity lookup failed.');
941
+ throw state.failure ?? cleanupUnconfirmed('POSIX process identity lookup failed.');
280
942
  }
281
- if (state.marker !== undefined && !snapshot.markerLookupSucceeded) {
282
- throw cleanupUnconfirmed('POSIX process marker lookup failed.');
943
+ if (requireDescendantsExitedBeforeCompletion) {
944
+ latchLiveDescendantCompletionFailure(state, snapshot);
283
945
  }
284
- let markerLookupEverFailed = state.marker !== undefined && !snapshot.markerLookupSucceeded;
285
- refreshObservedProcesses(state, snapshot.table, snapshot.markerPids);
286
- const observedAtTermination = new Set([...state.descendants.values()]
287
- .filter((identity) => posixIdentityMatches(identity, snapshot))
288
- .map((identity) => `${identity.pid}:${identity.startTime}`));
289
- while (Date.now() < deadline) {
290
- if (posixIdentityMatches(state.root, snapshot)) {
291
- signalIdentity(state.root, snapshot, 'SIGSTOP', kill);
292
- }
293
- for (const identity of [...state.descendants.values()].reverse()) {
294
- signalIdentity(identity, snapshot, 'SIGSTOP', kill);
295
- }
296
- const next = await readPosixProcessTable(execFileImpl, state.markerEnvironmentVariable, state.marker, Math.min(PROCESS_TREE_LOOKUP_TIMEOUT_MS, Math.max(1, deadline - Date.now())));
297
- if (!next)
298
- break;
299
- markerLookupEverFailed ||=
300
- state.marker !== undefined && !next.markerLookupSucceeded;
301
- if (markerLookupEverFailed)
302
- break;
303
- snapshot = next;
304
- const previousSize = state.descendants.size;
305
- refreshObservedProcesses(state, snapshot.table, snapshot.markerPids);
306
- if (posixIdentityMatches(state.root, snapshot)) {
307
- signalIdentity(state.root, snapshot, 'SIGSTOP', kill);
946
+ const liveIdentities = (current) => [state.root, ...state.descendants.values()].filter((identity) => posixIdentityMatches(identity, current) &&
947
+ !current.table.get(identity.pid)?.state?.startsWith('Z'));
948
+ const liveMarkerPids = (current) => state.marker === undefined
949
+ ? []
950
+ : [...current.markerPids].filter((pid) => pid !== state.lease?.sentinel.pid &&
951
+ !current.table.get(pid)?.state?.startsWith('Z'));
952
+ const signalOwned = (current, signal) => {
953
+ if (state.marker !== undefined && !current.markerLookupSucceeded)
954
+ return;
955
+ signalProcessGroup(safelyOwnedProcessGroup(state, current), signal, kill);
956
+ for (const identity of liveIdentities(current).reverse()) {
957
+ signalIdentity(identity, current, signal, kill);
308
958
  }
309
- for (const identity of [...state.descendants.values()].reverse()) {
310
- signalIdentity(identity, snapshot, 'SIGSTOP', kill);
959
+ };
960
+ const initialTargets = liveIdentities(snapshot);
961
+ const initialMarkerPids = liveMarkerPids(snapshot);
962
+ const initialProcessGroup = safelyOwnedProcessGroup(state, snapshot);
963
+ if (initialTargets.length === 0 &&
964
+ initialMarkerPids.length === 0 &&
965
+ initialProcessGroup === undefined) {
966
+ if (requireConfirmedCleanup && state.marker === undefined) {
967
+ throw cleanupUnconfirmed('no inherited-pipe process identity was observed.');
311
968
  }
312
- const allFrozen = [state.root, ...state.descendants.values()].every((identity) => {
313
- if (!posixIdentityMatches(identity, snapshot))
314
- return true;
315
- const processState = snapshot.table.get(identity.pid)?.state;
316
- return processState?.startsWith('T') || processState?.startsWith('Z');
317
- });
318
- if (allFrozen && state.descendants.size === previousSize)
319
- break;
320
- }
321
- const killed = [state.root, ...state.descendants.values()].filter((identity) => identity !== undefined &&
322
- posixIdentityMatches(identity, snapshot));
323
- for (const identity of killed.reverse()) {
324
- signalIdentity(identity, snapshot, 'SIGKILL', kill);
969
+ if (state.failure)
970
+ throw state.failure;
971
+ return;
325
972
  }
326
- if (markerLookupEverFailed) {
327
- throw cleanupUnconfirmed('POSIX process marker lookup failed.');
973
+ signalOwned(snapshot, 'SIGTERM');
974
+ await new Promise((resolve) => {
975
+ setTimeout(resolve, PROCESS_TREE_TERMINATION_GRACE_MS);
976
+ });
977
+ const killSnapshot = await readImmediatePosixSnapshot(state, 'termination-kill');
978
+ if (!killSnapshot) {
979
+ throw state.failure ?? cleanupUnconfirmed('POSIX termination audit failed.');
328
980
  }
329
- if (!requireConfirmedCleanup)
330
- return;
331
- const verified = await readPosixProcessTable(execFileImpl, state.markerEnvironmentVariable, state.marker);
332
- if (!verified)
333
- throw cleanupUnconfirmed('POSIX termination verification failed.');
334
- if (!verified.markerLookupSucceeded) {
335
- throw cleanupUnconfirmed('POSIX process marker verification failed.');
336
- }
337
- const stillRunning = killed.some((identity) => {
338
- if (!posixIdentityMatches(identity, verified))
339
- return false;
340
- return !verified.table.get(identity.pid)?.state?.startsWith('Z');
981
+ snapshot = killSnapshot;
982
+ signalOwned(snapshot, 'SIGKILL');
983
+ await new Promise((resolve) => {
984
+ setTimeout(resolve, PROCESS_TREE_TERMINATION_SETTLE_MS);
341
985
  });
342
- if (stillRunning)
343
- throw cleanupUnconfirmed('a verified POSIX descendant is still running.');
344
- const unobservedMarkedProcess = state.marker === undefined
345
- ? false
346
- : [...verified.markerPids].some((pid) => !verified.table.get(pid)?.state?.startsWith('Z'));
347
- if (unobservedMarkedProcess) {
348
- throw cleanupUnconfirmed('an unverified marker process is still running.');
986
+ const verified = await readImmediatePosixSnapshot(state, 'termination-verification');
987
+ if (!verified) {
988
+ throw state.failure ?? cleanupUnconfirmed('POSIX termination verification failed.');
349
989
  }
350
- if (requireConfirmedCleanup &&
351
- state.marker !== undefined &&
352
- state.root === undefined &&
353
- state.descendants.size === 0) {
354
- throw cleanupUnconfirmed('no marked process identity was observed.');
990
+ if (liveIdentities(verified).length > 0) {
991
+ throw state.failure ?? cleanupUnconfirmed('a verified POSIX descendant is still running.');
355
992
  }
356
- if (requireConfirmedCleanup && state.marker === undefined && observedAtTermination.size === 0) {
357
- throw cleanupUnconfirmed('no inherited-pipe process identity was observed.');
993
+ if (liveMarkerPids(verified).length > 0) {
994
+ throw state.failure ?? cleanupUnconfirmed('an unverified marker process is still running.');
358
995
  }
996
+ if (state.failure)
997
+ throw state.failure;
359
998
  }
360
999
  export async function terminateProcessTree(pid, killDirectChild, dependencies = {}, options = {}) {
361
1000
  const platform = dependencies.platform ?? process.platform;
@@ -367,10 +1006,10 @@ export async function terminateProcessTree(pid, killDirectChild, dependencies =
367
1006
  throw cleanupUnconfirmed('process observation is invalid.');
368
1007
  }
369
1008
  if (platform === 'win32') {
370
- await terminateWindowsProcessTree(state, options.requireConfirmedCleanup ?? false);
1009
+ await terminateWindowsProcessTree(state, options.requireConfirmedCleanup ?? false, options.allowUnobservedWindowsProcessOnNormalCompletion ?? false);
371
1010
  return;
372
1011
  }
373
- await terminatePosixProcessTree(state, options.requireConfirmedCleanup ?? false, options.allowInspectionUnavailable ?? false);
1012
+ await terminatePosixProcessTree(state, options.requireConfirmedCleanup ?? false, options.allowInspectionUnavailable ?? false, options.requireDescendantsExitedBeforeCompletion ?? false);
374
1013
  }
375
1014
  finally {
376
1015
  observation.dispose();