@lesliechan721/agent-skill-eval 0.1.0-beta.1 → 0.1.0-beta.10

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 (101) hide show
  1. package/dist/bin.js +167 -39
  2. package/dist/bin.js.map +1 -1
  3. package/dist/codex-eval-app-server.d.ts +199 -0
  4. package/dist/codex-eval-app-server.js +1364 -0
  5. package/dist/codex-eval-app-server.js.map +1 -0
  6. package/dist/codex-eval-cases.d.ts +43 -0
  7. package/dist/codex-eval-cases.js +327 -0
  8. package/dist/codex-eval-cases.js.map +1 -0
  9. package/dist/codex-eval-isolation.d.ts +79 -0
  10. package/dist/codex-eval-isolation.js +420 -0
  11. package/dist/codex-eval-isolation.js.map +1 -0
  12. package/dist/codex-eval-process-tree.d.ts +38 -0
  13. package/dist/codex-eval-process-tree.js +803 -0
  14. package/dist/codex-eval-process-tree.js.map +1 -0
  15. package/dist/codex-eval-runtime-config.d.ts +43 -0
  16. package/dist/codex-eval-runtime-config.js +607 -0
  17. package/dist/codex-eval-runtime-config.js.map +1 -0
  18. package/dist/codex-trigger-eval.d.ts +82 -0
  19. package/dist/codex-trigger-eval.js +790 -0
  20. package/dist/codex-trigger-eval.js.map +1 -0
  21. package/dist/codex-workflow.d.ts +191 -0
  22. package/dist/codex-workflow.js +1832 -0
  23. package/dist/codex-workflow.js.map +1 -0
  24. package/dist/credentials.d.ts +5 -0
  25. package/dist/credentials.js +30 -0
  26. package/dist/credentials.js.map +1 -0
  27. package/dist/custom-process.d.ts +60 -0
  28. package/dist/custom-process.js +423 -0
  29. package/dist/custom-process.js.map +1 -0
  30. package/dist/eval-audit.d.ts +38 -0
  31. package/dist/eval-audit.js +71 -0
  32. package/dist/eval-audit.js.map +1 -0
  33. package/dist/eval-file.d.ts +9 -0
  34. package/dist/eval-file.js +15 -0
  35. package/dist/eval-file.js.map +1 -0
  36. package/dist/eval-process-management.d.ts +21 -0
  37. package/dist/eval-process-management.js +84 -0
  38. package/dist/eval-process-management.js.map +1 -0
  39. package/dist/eval-result.d.ts +79 -0
  40. package/dist/eval-result.js +144 -0
  41. package/dist/eval-result.js.map +1 -0
  42. package/dist/eval-run.d.ts +27 -0
  43. package/dist/eval-run.js +564 -0
  44. package/dist/eval-run.js.map +1 -0
  45. package/dist/eval-spec.d.ts +57 -0
  46. package/dist/eval-spec.js +120 -0
  47. package/dist/eval-spec.js.map +1 -0
  48. package/dist/eval-suite-binding.d.ts +4 -0
  49. package/dist/eval-suite-binding.js +19 -0
  50. package/dist/eval-suite-binding.js.map +1 -0
  51. package/dist/index.d.ts +36 -9
  52. package/dist/index.js +19 -6
  53. package/dist/index.js.map +1 -1
  54. package/dist/json-schema.js +75 -5
  55. package/dist/json-schema.js.map +1 -1
  56. package/dist/output-path.js +2 -2
  57. package/dist/process-tree.d.ts +19 -1
  58. package/dist/process-tree.js +391 -77
  59. package/dist/process-tree.js.map +1 -1
  60. package/dist/process.d.ts +0 -2
  61. package/dist/process.js +76 -25
  62. package/dist/process.js.map +1 -1
  63. package/dist/provider-doctor.d.ts +45 -0
  64. package/dist/provider-doctor.js +112 -0
  65. package/dist/provider-doctor.js.map +1 -0
  66. package/dist/providers.d.ts +83 -0
  67. package/dist/providers.js +632 -0
  68. package/dist/providers.js.map +1 -0
  69. package/dist/run-paths.d.ts +8 -0
  70. package/dist/run-paths.js +71 -0
  71. package/dist/run-paths.js.map +1 -0
  72. package/dist/runtime-skill.d.ts +6 -0
  73. package/dist/runtime-skill.js +87 -3
  74. package/dist/runtime-skill.js.map +1 -1
  75. package/dist/schemas/digests.json +3 -3
  76. package/dist/schemas/eval-audit.schema.json +64 -0
  77. package/dist/schemas/eval-result.schema.json +232 -0
  78. package/dist/schemas/eval-spec.schema.json +109 -0
  79. package/dist/schemas.d.ts +1 -3
  80. package/dist/schemas.js +0 -17
  81. package/dist/schemas.js.map +1 -1
  82. package/dist/selected-cases.d.ts +30 -0
  83. package/dist/selected-cases.js +388 -0
  84. package/dist/selected-cases.js.map +1 -0
  85. package/dist/types.d.ts +2 -34
  86. package/package.json +8 -3
  87. package/dist/bundle.d.ts +0 -93
  88. package/dist/bundle.js +0 -216
  89. package/dist/bundle.js.map +0 -1
  90. package/dist/capabilities.d.ts +0 -2
  91. package/dist/capabilities.js +0 -38
  92. package/dist/capabilities.js.map +0 -1
  93. package/dist/plan.d.ts +0 -79
  94. package/dist/plan.js +0 -285
  95. package/dist/plan.js.map +0 -1
  96. package/dist/run.d.ts +0 -2
  97. package/dist/run.js +0 -592
  98. package/dist/run.js.map +0 -1
  99. package/dist/schemas/aw-skill-eval-execution-plan.schema.json +0 -220
  100. package/dist/schemas/eval-run-bundle.schema.json +0 -408
  101. package/dist/schemas/eval-runtime-capabilities.schema.json +0 -85
@@ -1,12 +1,33 @@
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
6
  const PROCESS_TREE_OBSERVATION_INTERVAL_MS = 50;
6
7
  const PROCESS_TREE_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
7
8
  const observationStates = new WeakMap();
8
9
  const defaultExecFile = (command, args, options, callback) => execFile(command, args, options, (error, stdout) => callback(error, stdout));
9
- function runCommand(command, args, execFileImpl, timeoutMs) {
10
+ function safeCommandFailureValue(value) {
11
+ if (typeof value === 'number' && Number.isFinite(value))
12
+ return value;
13
+ if (typeof value !== 'string' || !/^[A-Za-z0-9_.-]{1,64}$/u.test(value))
14
+ return undefined;
15
+ return value;
16
+ }
17
+ function commandFailure(reason, error) {
18
+ const details = error && typeof error === 'object'
19
+ ? error
20
+ : {};
21
+ const code = safeCommandFailureValue(details.code);
22
+ const signal = safeCommandFailureValue(details.signal);
23
+ return {
24
+ reason,
25
+ ...(code !== undefined ? { code } : {}),
26
+ ...(typeof signal === 'string' ? { signal } : {}),
27
+ ...(typeof details.killed === 'boolean' ? { killed: details.killed } : {}),
28
+ };
29
+ }
30
+ function runCommandWithDiagnostics(command, args, execFileImpl, timeoutMs) {
10
31
  return new Promise((resolve) => {
11
32
  let settled = false;
12
33
  const finish = (result) => {
@@ -16,30 +37,49 @@ function runCommand(command, args, execFileImpl, timeoutMs) {
16
37
  clearTimeout(timeout);
17
38
  resolve(result);
18
39
  };
19
- const timeout = setTimeout(() => finish(null), timeoutMs);
40
+ const timeout = setTimeout(() => finish({
41
+ output: null,
42
+ failure: commandFailure('deadline-exceeded'),
43
+ }), timeoutMs);
20
44
  timeout.unref();
21
45
  try {
22
46
  execFileImpl(command, args, {
23
47
  timeout: timeoutMs,
24
48
  windowsHide: true,
25
49
  maxBuffer: PROCESS_TREE_MAX_BUFFER_BYTES,
26
- }, (error, stdout) => finish(error ? null : stdout));
50
+ }, (error, stdout) => finish(error
51
+ ? { output: null, failure: commandFailure('callback-error', error) }
52
+ : { output: stdout }));
27
53
  }
28
- catch {
29
- finish(null);
54
+ catch (error) {
55
+ finish({ output: null, failure: commandFailure('synchronous-error', error) });
30
56
  }
31
57
  });
32
58
  }
59
+ function runCommand(command, args, execFileImpl, timeoutMs) {
60
+ return runCommandWithDiagnostics(command, args, execFileImpl, timeoutMs)
61
+ .then((result) => result.output);
62
+ }
33
63
  function parsePosixProcessTable(stdout) {
34
64
  const table = new Map();
35
65
  for (const line of stdout.split('\n')) {
36
- const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.+?)\s*$/u.exec(line);
37
- if (!match)
66
+ const groupMatch = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(.+?)\s*$/u.exec(line);
67
+ if (groupMatch) {
68
+ table.set(Number(groupMatch[1]), {
69
+ ppid: Number(groupMatch[2]),
70
+ pgid: Number(groupMatch[3]),
71
+ state: groupMatch[4],
72
+ startTime: groupMatch[5],
73
+ });
38
74
  continue;
39
- table.set(Number(match[1]), {
40
- ppid: Number(match[2]),
41
- state: match[3],
42
- startTime: match[4],
75
+ }
76
+ const legacyMatch = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.+?)\s*$/u.exec(line);
77
+ if (!legacyMatch)
78
+ continue;
79
+ table.set(Number(legacyMatch[1]), {
80
+ ppid: Number(legacyMatch[2]),
81
+ state: legacyMatch[3],
82
+ startTime: legacyMatch[4],
43
83
  });
44
84
  }
45
85
  return table;
@@ -49,28 +89,147 @@ function parseMarkerProcessIds(stdout, markerEnvironmentVariable, marker) {
49
89
  const assignment = `${markerEnvironmentVariable}=${marker}`;
50
90
  for (const line of stdout.split('\n')) {
51
91
  const match = /^\s*(\d+)\s+(.+)$/u.exec(line);
52
- if (match?.[2].includes(assignment))
92
+ if (match?.[2].split(/\s+/u).includes(assignment))
53
93
  processIds.add(Number(match[1]));
54
94
  }
55
95
  return processIds;
56
96
  }
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),
97
+ async function readPosixProcessTable(execFileImpl, markerEnvironmentVariable, marker, timeoutMs = PROCESS_TREE_COMMAND_TIMEOUT_MS, confirmUnmarkedPid) {
98
+ const identityArgs = ['-A', '-o', 'pid=,ppid=,pgid=,state=,lstart='];
99
+ const markerArgs = ['eww', '-A', '-o', 'pid=,command='];
100
+ const lookupStartedAt = Date.now();
101
+ const [identityOutput, initialMarkerResult] = await Promise.all([
102
+ runCommand('ps', identityArgs, execFileImpl, timeoutMs),
60
103
  marker === undefined
61
- ? Promise.resolve('')
62
- : runCommand('ps', ['eww', '-A', '-o', 'pid=,command='], execFileImpl, timeoutMs),
104
+ ? Promise.resolve({ output: '' })
105
+ : runCommandWithDiagnostics('ps', markerArgs, execFileImpl, timeoutMs),
63
106
  ]);
64
107
  if (identityOutput === null)
65
108
  return null;
109
+ let markerResult = initialMarkerResult;
110
+ let markerLookupAttempts = marker === undefined ? 0 : 1;
111
+ const retryTimeoutMs = timeoutMs - (Date.now() - lookupStartedAt);
112
+ if (marker !== undefined && markerResult.output === null && retryTimeoutMs > 0) {
113
+ markerLookupAttempts += 1;
114
+ markerResult = await runCommandWithDiagnostics('ps', markerArgs, execFileImpl, retryTimeoutMs);
115
+ }
116
+ const markerOutput = markerResult.output;
117
+ const table = parsePosixProcessTable(identityOutput);
118
+ const markerPids = marker === undefined || markerOutput === null
119
+ ? new Set()
120
+ : parseMarkerProcessIds(markerOutput, markerEnvironmentVariable, marker);
121
+ const confirmedUnmarkedPids = new Set();
122
+ const unmarkedEntry = confirmUnmarkedPid === undefined || markerPids.has(confirmUnmarkedPid)
123
+ ? undefined
124
+ : table.get(confirmUnmarkedPid);
125
+ if (unmarkedEntry && !unmarkedEntry.state?.startsWith('Z')) {
126
+ const confirmationOutput = await runCommand('ps', identityArgs, execFileImpl, timeoutMs);
127
+ if (confirmationOutput === null)
128
+ return null;
129
+ const confirmedEntry = parsePosixProcessTable(confirmationOutput).get(confirmUnmarkedPid);
130
+ if (confirmedEntry?.startTime === unmarkedEntry.startTime &&
131
+ !confirmedEntry.state?.startsWith('Z')) {
132
+ confirmedUnmarkedPids.add(confirmUnmarkedPid);
133
+ }
134
+ }
66
135
  return {
67
- table: parsePosixProcessTable(identityOutput),
68
- markerPids: marker === undefined || markerOutput === null
69
- ? new Set()
70
- : parseMarkerProcessIds(markerOutput, markerEnvironmentVariable, marker),
136
+ table,
137
+ markerPids,
71
138
  markerLookupSucceeded: marker === undefined || markerOutput !== null,
139
+ markerLookupAttempts,
140
+ ...(markerResult.failure ? { markerLookupFailure: markerResult.failure } : {}),
141
+ confirmedUnmarkedPids,
142
+ };
143
+ }
144
+ function posixMarkerLookupFailure(snapshot, stage) {
145
+ const fields = [
146
+ `stage=${stage}`,
147
+ `attempts=${snapshot.markerLookupAttempts}`,
148
+ `reason=${snapshot.markerLookupFailure?.reason ?? 'unknown'}`,
149
+ ];
150
+ if (snapshot.markerLookupFailure?.code !== undefined) {
151
+ fields.push(`code=${snapshot.markerLookupFailure.code}`);
152
+ }
153
+ if (snapshot.markerLookupFailure?.signal !== undefined) {
154
+ fields.push(`signal=${snapshot.markerLookupFailure.signal}`);
155
+ }
156
+ if (snapshot.markerLookupFailure?.killed !== undefined) {
157
+ fields.push(`killed=${snapshot.markerLookupFailure.killed}`);
158
+ }
159
+ return cleanupUnconfirmed(`POSIX process marker lookup failed (${fields.join(', ')}).`);
160
+ }
161
+ async function defaultCreateSentinel(markerEnvironmentVariable, marker) {
162
+ const child = spawn(process.execPath, ['-e', 'setInterval(() => {}, 60_000)'], {
163
+ env: { [markerEnvironmentVariable]: marker },
164
+ stdio: 'ignore',
165
+ windowsHide: true,
166
+ });
167
+ await once(child, 'spawn');
168
+ if (child.pid === undefined)
169
+ throw cleanupUnconfirmed('inspection sentinel did not start.');
170
+ return {
171
+ pid: child.pid,
172
+ stop: async () => {
173
+ if (child.exitCode !== null || child.signalCode !== null)
174
+ return;
175
+ const closed = once(child, 'close').then(() => undefined);
176
+ try {
177
+ child.kill('SIGKILL');
178
+ }
179
+ catch {
180
+ // The controlled sentinel can exit before release.
181
+ }
182
+ await closed;
183
+ },
72
184
  };
73
185
  }
186
+ function validateLeaseSnapshot(snapshot, lease, stage) {
187
+ if (!snapshot.markerLookupSucceeded) {
188
+ return posixMarkerLookupFailure(snapshot, stage);
189
+ }
190
+ const sentinelEntry = snapshot.table.get(lease.sentinel.pid);
191
+ if (sentinelEntry?.startTime !== lease.sentinel.startTime ||
192
+ !snapshot.markerPids.has(lease.sentinel.pid)) {
193
+ return cleanupUnconfirmed('POSIX inspection sentinel could not be verified.');
194
+ }
195
+ return undefined;
196
+ }
197
+ export async function acquireProcessTreeInspectionLease(dependencies = {}) {
198
+ const platform = dependencies.platform ?? process.platform;
199
+ if (platform !== 'darwin' && platform !== 'linux') {
200
+ throw cleanupUnconfirmed(`process inspection is unsupported on ${platform}.`);
201
+ }
202
+ const markerEnvironmentVariable = dependencies.markerEnvironmentVariable ?? DEFAULT_PROCESS_MARKER_ENV;
203
+ const marker = randomUUID();
204
+ const sentinel = await (dependencies.createSentinel ?? defaultCreateSentinel)(markerEnvironmentVariable, marker);
205
+ try {
206
+ const snapshot = await readPosixProcessTable(dependencies.execFile ?? defaultExecFile, markerEnvironmentVariable, marker);
207
+ if (!snapshot)
208
+ throw cleanupUnconfirmed('POSIX process identity lookup failed.');
209
+ if (!snapshot.markerLookupSucceeded) {
210
+ throw posixMarkerLookupFailure(snapshot, 'inspection-preflight');
211
+ }
212
+ const entry = snapshot.table.get(sentinel.pid);
213
+ if (!entry || !snapshot.markerPids.has(sentinel.pid)) {
214
+ throw cleanupUnconfirmed('POSIX inspection sentinel could not be verified.');
215
+ }
216
+ let released = false;
217
+ return {
218
+ marker,
219
+ sentinel: { pid: sentinel.pid, startTime: entry.startTime },
220
+ release: async () => {
221
+ if (released)
222
+ return;
223
+ released = true;
224
+ await sentinel.stop();
225
+ },
226
+ };
227
+ }
228
+ catch (error) {
229
+ await sentinel.stop();
230
+ throw error;
231
+ }
232
+ }
74
233
  function parseWindowsProcessTable(stdout) {
75
234
  const table = new Map();
76
235
  for (const line of stdout.split('\n')) {
@@ -128,6 +287,8 @@ function refreshObservedProcesses(state, table, markerPids = new Set(), allowRoo
128
287
  }
129
288
  state.rootDiscoveryPending = false;
130
289
  for (const processId of markerPids) {
290
+ if (processId === state.lease?.sentinel.pid)
291
+ continue;
131
292
  if (processId === state.root?.pid)
132
293
  continue;
133
294
  const entry = table.get(processId);
@@ -140,9 +301,111 @@ function refreshObservedProcesses(state, table, markerPids = new Set(), allowRoo
140
301
  }
141
302
  refreshDescendants(state.root, state.descendants, table);
142
303
  }
143
- function createProcessTreeObservation(pid, marker, dependencies, startLookup) {
304
+ function latchObservationFailure(state, error) {
305
+ if (state.failure)
306
+ return;
307
+ state.failure = error;
308
+ for (const listener of state.failureListeners)
309
+ listener(error);
310
+ }
311
+ function acceptPosixSnapshot(state, snapshot, stage) {
312
+ if (!snapshot.markerLookupSucceeded && state.marker !== undefined) {
313
+ latchObservationFailure(state, posixMarkerLookupFailure(snapshot, stage));
314
+ return false;
315
+ }
316
+ if (state.lease) {
317
+ const leaseFailure = validateLeaseSnapshot(snapshot, state.lease, stage);
318
+ if (leaseFailure) {
319
+ latchObservationFailure(state, leaseFailure);
320
+ return false;
321
+ }
322
+ }
323
+ const rootEntry = snapshot.table.get(state.pid);
324
+ if (state.marker !== undefined &&
325
+ state.dependencies.enforceCooperativeProcessGroup === true &&
326
+ rootEntry !== undefined &&
327
+ !rootEntry.state?.startsWith('Z') &&
328
+ snapshot.confirmedUnmarkedPids.has(state.pid)) {
329
+ state.root = { pid: state.pid, startTime: rootEntry.startTime };
330
+ latchObservationFailure(state, cleanupUnconfirmed('custom process contract violation: a live provider process lost its process marker.'));
331
+ }
332
+ refreshObservedProcesses(state, snapshot.table, snapshot.markerPids);
333
+ if (state.marker !== undefined) {
334
+ const providerMarkerPids = [...snapshot.markerPids].filter((pid) => pid !== state.lease?.sentinel.pid);
335
+ if (state.dependencies.enforceCooperativeProcessGroup === true &&
336
+ state.rootPgidMode === undefined) {
337
+ if (state.root?.pid === state.pid &&
338
+ rootEntry?.startTime === state.root.startTime &&
339
+ rootEntry.pgid !== undefined &&
340
+ snapshot.markerPids.has(state.pid)) {
341
+ if (rootEntry.pgid === state.pid) {
342
+ state.rootPgid = rootEntry.pgid;
343
+ state.rootPgidMode = 'owned';
344
+ }
345
+ else {
346
+ state.rootPgidMode = 'shared';
347
+ }
348
+ }
349
+ else if (rootEntry === undefined &&
350
+ (providerMarkerPids.some((pid) => snapshot.table.get(pid)?.pgid !== undefined) ||
351
+ [...snapshot.table].some(([pid, entry]) => pid !== state.lease?.sentinel.pid &&
352
+ entry.pgid === state.pid &&
353
+ !entry.state?.startsWith('Z')))) {
354
+ state.rootPgid = state.pid;
355
+ state.rootPgidMode = 'missing';
356
+ }
357
+ }
358
+ const expectedPgid = state.rootPgidMode === 'owned'
359
+ ? state.rootPgid
360
+ : state.rootPgidMode === 'missing'
361
+ ? state.pid
362
+ : undefined;
363
+ if (expectedPgid !== undefined &&
364
+ providerMarkerPids.some((pid) => {
365
+ const entry = snapshot.table.get(pid);
366
+ return entry?.pgid !== undefined && entry.pgid !== expectedPgid;
367
+ })) {
368
+ latchObservationFailure(state, cleanupUnconfirmed('custom process contract violation: a marked process left its initial process group.'));
369
+ }
370
+ const liveKnownIdentities = [state.root, ...state.descendants.values()].filter((identity) => identityMatches(identity, snapshot.table) &&
371
+ !snapshot.table.get(identity.pid)?.state?.startsWith('Z'));
372
+ const markerlessKnownIdentity = state.dependencies.enforceCooperativeProcessGroup === true &&
373
+ liveKnownIdentities.some((identity) => identity.pid !== state.pid && !snapshot.markerPids.has(identity.pid));
374
+ let markerlessInitialGroupMember = false;
375
+ if (state.rootPgid !== undefined) {
376
+ for (const [pid, entry] of snapshot.table) {
377
+ if (entry.pgid !== state.rootPgid || entry.state?.startsWith('Z'))
378
+ continue;
379
+ if (pid !== state.root?.pid) {
380
+ const known = state.descendants.get(pid);
381
+ if (!known || known.startTime === entry.startTime) {
382
+ state.descendants.set(pid, { pid, startTime: entry.startTime });
383
+ }
384
+ }
385
+ if (pid !== state.pid && !snapshot.markerPids.has(pid)) {
386
+ markerlessInitialGroupMember = true;
387
+ }
388
+ }
389
+ }
390
+ if (markerlessKnownIdentity || markerlessInitialGroupMember) {
391
+ latchObservationFailure(state, cleanupUnconfirmed('custom process contract violation: a live provider process lost its process marker.'));
392
+ }
393
+ }
394
+ state.lastSnapshot = snapshot;
395
+ return true;
396
+ }
397
+ function createProcessTreeObservation(pid, marker, dependencies, startLookup, lease) {
144
398
  const observation = {
145
399
  pid,
400
+ onFailure: (listener) => {
401
+ const state = observationStates.get(observation);
402
+ if (!state)
403
+ return () => undefined;
404
+ state.failureListeners.add(listener);
405
+ if (state.failure)
406
+ listener(state.failure);
407
+ return () => state.failureListeners.delete(listener);
408
+ },
146
409
  dispose: () => {
147
410
  const state = observationStates.get(observation);
148
411
  if (state) {
@@ -157,9 +420,11 @@ function createProcessTreeObservation(pid, marker, dependencies, startLookup) {
157
420
  platform: dependencies.platform ?? process.platform,
158
421
  marker,
159
422
  markerEnvironmentVariable: dependencies.markerEnvironmentVariable ?? DEFAULT_PROCESS_MARKER_ENV,
423
+ lease,
160
424
  dependencies,
161
425
  rootDiscoveryPending: true,
162
426
  descendants: new Map(),
427
+ failureListeners: new Set(),
163
428
  disposed: false,
164
429
  initialLookup: Promise.resolve(),
165
430
  };
@@ -172,10 +437,16 @@ function createProcessTreeObservation(pid, marker, dependencies, startLookup) {
172
437
  if (!state.disposed && table)
173
438
  refreshObservedProcesses(state, table);
174
439
  })
175
- : readPosixProcessTable(execFileImpl, state.markerEnvironmentVariable, marker).then((snapshot) => {
176
- if (!state.disposed && snapshot) {
177
- refreshObservedProcesses(state, snapshot.table, snapshot.markerPids, !snapshot.markerLookupSucceeded);
440
+ : readPosixProcessTable(execFileImpl, state.markerEnvironmentVariable, marker, PROCESS_TREE_COMMAND_TIMEOUT_MS, dependencies.enforceCooperativeProcessGroup === true && marker !== undefined
441
+ ? state.pid
442
+ : undefined).then((snapshot) => {
443
+ if (state.disposed)
444
+ return;
445
+ if (!snapshot) {
446
+ latchObservationFailure(state, cleanupUnconfirmed('POSIX process identity lookup failed.'));
447
+ return;
178
448
  }
449
+ acceptPosixSnapshot(state, snapshot, 'observation');
179
450
  });
180
451
  const scheduleRefresh = () => {
181
452
  if (state.disposed)
@@ -191,24 +462,12 @@ function createProcessTreeObservation(pid, marker, dependencies, startLookup) {
191
462
  void state.initialLookup.finally(scheduleRefresh);
192
463
  return observation;
193
464
  }
194
- export function observeProcessTree(pid, marker, dependencies = {}) {
195
- return createProcessTreeObservation(pid, marker, dependencies, true);
465
+ export function observeProcessTree(pid, marker, dependencies = {}, lease) {
466
+ return createProcessTreeObservation(pid, marker, dependencies, true, lease);
196
467
  }
197
468
  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
- }
469
+ const lease = await acquireProcessTreeInspectionLease(dependencies);
470
+ await lease.release();
212
471
  }
213
472
  function cleanupUnconfirmed(detail) {
214
473
  return new Error(`Process tree cleanup unconfirmed: ${detail}`);
@@ -229,6 +488,42 @@ function signalIdentity(identity, snapshot, signal, kill) {
229
488
  // The process can exit between identity verification and the signal.
230
489
  }
231
490
  }
491
+ function safelyOwnedProcessGroup(state, snapshot) {
492
+ if (state.dependencies.enforceCooperativeProcessGroup !== true ||
493
+ state.marker === undefined ||
494
+ state.rootPgid === undefined ||
495
+ state.rootPgid !== state.pid)
496
+ return undefined;
497
+ const pgid = state.rootPgid;
498
+ const members = [...snapshot.table].filter(([, entry]) => entry.pgid === pgid && !entry.state?.startsWith('Z'));
499
+ if (members.length === 0)
500
+ return undefined;
501
+ const known = [state.root, ...state.descendants.values()];
502
+ if (members.some(([pid, entry]) => !known.some((identity) => identity?.pid === pid && identity.startTime === entry.startTime)))
503
+ return undefined;
504
+ return pgid;
505
+ }
506
+ function signalProcessGroup(pgid, signal, kill) {
507
+ if (pgid === undefined)
508
+ return;
509
+ try {
510
+ kill(-pgid, signal);
511
+ }
512
+ catch {
513
+ // The verified process group can exit before the signal.
514
+ }
515
+ }
516
+ function latchLiveDescendantCompletionFailure(state, snapshot) {
517
+ if (state.dependencies.enforceCooperativeProcessGroup !== true)
518
+ return;
519
+ const liveDescendant = [...state.descendants.values()].some((identity) => identity.pid !== state.pid &&
520
+ identity.pid !== state.lease?.sentinel.pid &&
521
+ posixIdentityMatches(identity, snapshot) &&
522
+ !snapshot.table.get(identity.pid)?.state?.startsWith('Z'));
523
+ if (!liveDescendant)
524
+ return;
525
+ latchObservationFailure(state, cleanupUnconfirmed('custom process contract violation: all provider descendants must exit before completion.'));
526
+ }
232
527
  async function runTaskkill(pid, execFileImpl) {
233
528
  return (await runCommand('taskkill', ['/PID', String(pid), '/T', '/F'], execFileImpl, PROCESS_TREE_COMMAND_TIMEOUT_MS)) !== null;
234
529
  }
@@ -267,42 +562,61 @@ async function terminateWindowsProcessTree(state, requireConfirmedCleanup) {
267
562
  throw cleanupUnconfirmed('no inherited-pipe process identity was observed.');
268
563
  }
269
564
  }
270
- async function terminatePosixProcessTree(state, requireConfirmedCleanup, allowInspectionUnavailable) {
565
+ async function terminatePosixProcessTree(state, requireConfirmedCleanup, allowInspectionUnavailable, requireDescendantsExitedBeforeCompletion) {
271
566
  await state.initialLookup;
567
+ state.disposed = true;
568
+ if (state.pollTimer)
569
+ clearTimeout(state.pollTimer);
272
570
  const execFileImpl = state.dependencies.execFile ?? defaultExecFile;
273
571
  const kill = state.dependencies.kill ?? process.kill.bind(process);
274
572
  const deadline = Date.now() + PROCESS_TREE_COMMAND_TIMEOUT_MS;
275
- let snapshot = await readPosixProcessTable(execFileImpl, state.markerEnvironmentVariable, state.marker);
573
+ const inspect = async (timeoutMs, stage) => {
574
+ const next = await readPosixProcessTable(execFileImpl, state.markerEnvironmentVariable, state.marker, timeoutMs, state.dependencies.enforceCooperativeProcessGroup === true && state.marker !== undefined
575
+ ? state.pid
576
+ : undefined);
577
+ if (!next) {
578
+ latchObservationFailure(state, cleanupUnconfirmed('POSIX process identity lookup failed.'));
579
+ return undefined;
580
+ }
581
+ return acceptPosixSnapshot(state, next, stage) ? next : undefined;
582
+ };
583
+ let snapshot = await inspect(PROCESS_TREE_COMMAND_TIMEOUT_MS, 'termination-start');
584
+ let processGroupCurrentlyVerified = snapshot !== undefined;
585
+ if (!snapshot)
586
+ snapshot = state.lastSnapshot;
276
587
  if (!snapshot) {
277
588
  if (!requireConfirmedCleanup && allowInspectionUnavailable)
278
589
  return;
279
- throw cleanupUnconfirmed('POSIX process identity lookup failed.');
590
+ throw state.failure ?? cleanupUnconfirmed('POSIX process identity lookup failed.');
280
591
  }
281
- if (state.marker !== undefined && !snapshot.markerLookupSucceeded) {
282
- throw cleanupUnconfirmed('POSIX process marker lookup failed.');
592
+ if (!processGroupCurrentlyVerified && safelyOwnedProcessGroup(state, snapshot) === undefined) {
593
+ if (!requireConfirmedCleanup && allowInspectionUnavailable)
594
+ return;
595
+ throw state.failure ?? cleanupUnconfirmed('POSIX process identity lookup failed.');
596
+ }
597
+ if (requireDescendantsExitedBeforeCompletion) {
598
+ latchLiveDescendantCompletionFailure(state, snapshot);
283
599
  }
284
- let markerLookupEverFailed = state.marker !== undefined && !snapshot.markerLookupSucceeded;
285
- refreshObservedProcesses(state, snapshot.table, snapshot.markerPids);
286
600
  const observedAtTermination = new Set([...state.descendants.values()]
287
601
  .filter((identity) => posixIdentityMatches(identity, snapshot))
288
602
  .map((identity) => `${identity.pid}:${identity.startTime}`));
289
603
  while (Date.now() < deadline) {
604
+ signalProcessGroup(processGroupCurrentlyVerified ? safelyOwnedProcessGroup(state, snapshot) : undefined, 'SIGSTOP', kill);
290
605
  if (posixIdentityMatches(state.root, snapshot)) {
291
606
  signalIdentity(state.root, snapshot, 'SIGSTOP', kill);
292
607
  }
293
608
  for (const identity of [...state.descendants.values()].reverse()) {
294
609
  signalIdentity(identity, snapshot, 'SIGSTOP', kill);
295
610
  }
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)
611
+ const next = await inspect(Math.max(1, deadline - Date.now()), 'termination-freeze');
612
+ if (!next) {
613
+ processGroupCurrentlyVerified = false;
302
614
  break;
615
+ }
303
616
  snapshot = next;
617
+ processGroupCurrentlyVerified = true;
304
618
  const previousSize = state.descendants.size;
305
- refreshObservedProcesses(state, snapshot.table, snapshot.markerPids);
619
+ signalProcessGroup(safelyOwnedProcessGroup(state, snapshot), 'SIGSTOP', kill);
306
620
  if (posixIdentityMatches(state.root, snapshot)) {
307
621
  signalIdentity(state.root, snapshot, 'SIGSTOP', kill);
308
622
  }
@@ -320,42 +634,42 @@ async function terminatePosixProcessTree(state, requireConfirmedCleanup, allowIn
320
634
  }
321
635
  const killed = [state.root, ...state.descendants.values()].filter((identity) => identity !== undefined &&
322
636
  posixIdentityMatches(identity, snapshot));
637
+ const ownedPgid = processGroupCurrentlyVerified
638
+ ? safelyOwnedProcessGroup(state, snapshot)
639
+ : undefined;
323
640
  for (const identity of killed.reverse()) {
324
641
  signalIdentity(identity, snapshot, 'SIGKILL', kill);
325
642
  }
326
- if (markerLookupEverFailed) {
327
- throw cleanupUnconfirmed('POSIX process marker lookup failed.');
328
- }
329
- if (!requireConfirmedCleanup)
643
+ signalProcessGroup(ownedPgid, 'SIGKILL', kill);
644
+ if (!requireConfirmedCleanup) {
645
+ if (state.failure)
646
+ throw state.failure;
330
647
  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.');
648
+ }
649
+ const verified = await inspect(PROCESS_TREE_COMMAND_TIMEOUT_MS, 'termination-verification');
650
+ if (!verified) {
651
+ throw state.failure ?? cleanupUnconfirmed('POSIX termination verification failed.');
336
652
  }
337
653
  const stillRunning = killed.some((identity) => {
338
654
  if (!posixIdentityMatches(identity, verified))
339
655
  return false;
340
656
  return !verified.table.get(identity.pid)?.state?.startsWith('Z');
341
657
  });
342
- if (stillRunning)
343
- throw cleanupUnconfirmed('a verified POSIX descendant is still running.');
658
+ if (stillRunning) {
659
+ throw state.failure ?? cleanupUnconfirmed('a verified POSIX descendant is still running.');
660
+ }
344
661
  const unobservedMarkedProcess = state.marker === undefined
345
662
  ? false
346
- : [...verified.markerPids].some((pid) => !verified.table.get(pid)?.state?.startsWith('Z'));
663
+ : [...verified.markerPids].some((pid) => pid !== state.lease?.sentinel.pid &&
664
+ !verified.table.get(pid)?.state?.startsWith('Z'));
347
665
  if (unobservedMarkedProcess) {
348
- throw cleanupUnconfirmed('an unverified marker process is still running.');
349
- }
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.');
666
+ throw state.failure ?? cleanupUnconfirmed('an unverified marker process is still running.');
355
667
  }
356
668
  if (requireConfirmedCleanup && state.marker === undefined && observedAtTermination.size === 0) {
357
669
  throw cleanupUnconfirmed('no inherited-pipe process identity was observed.');
358
670
  }
671
+ if (state.failure)
672
+ throw state.failure;
359
673
  }
360
674
  export async function terminateProcessTree(pid, killDirectChild, dependencies = {}, options = {}) {
361
675
  const platform = dependencies.platform ?? process.platform;
@@ -370,7 +684,7 @@ export async function terminateProcessTree(pid, killDirectChild, dependencies =
370
684
  await terminateWindowsProcessTree(state, options.requireConfirmedCleanup ?? false);
371
685
  return;
372
686
  }
373
- await terminatePosixProcessTree(state, options.requireConfirmedCleanup ?? false, options.allowInspectionUnavailable ?? false);
687
+ await terminatePosixProcessTree(state, options.requireConfirmedCleanup ?? false, options.allowInspectionUnavailable ?? false, options.requireDescendantsExitedBeforeCompletion ?? false);
374
688
  }
375
689
  finally {
376
690
  observation.dispose();