@lesliechan721/agent-skill-eval 0.1.0-beta.5 → 0.1.0-beta.51

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