@lesliechan721/agent-skill-eval 0.1.0-beta.6 → 0.1.0-beta.60

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