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

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.
@@ -3,9 +3,14 @@ import { randomUUID } from 'node:crypto';
3
3
  import { once } from 'node:events';
4
4
  const DEFAULT_PROCESS_MARKER_ENV = 'AW_NATIVE_CLI_PROCESS_MARKER';
5
5
  const PROCESS_TREE_COMMAND_TIMEOUT_MS = 1_000;
6
- const PROCESS_TREE_OBSERVATION_INTERVAL_MS = 50;
6
+ const POSIX_PROCESS_TREE_COMMAND_TIMEOUT_MS = 2_000;
7
+ const PROCESS_TREE_IDENTITY_INTERVAL_MS = 250;
8
+ const PROCESS_TREE_MARKER_INTERVAL_MS = 1_000;
9
+ const PROCESS_TREE_TERMINATION_GRACE_MS = 250;
10
+ const PROCESS_TREE_TERMINATION_SETTLE_MS = 50;
7
11
  const PROCESS_TREE_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
8
12
  const observationStates = new WeakMap();
13
+ const posixSamplers = new WeakMap();
9
14
  const defaultExecFile = (command, args, options, callback) => execFile(command, args, options, (error, stdout) => callback(error, stdout));
10
15
  function safeCommandFailureValue(value) {
11
16
  if (typeof value === 'number' && Number.isFinite(value))
@@ -84,27 +89,46 @@ function parsePosixProcessTable(stdout) {
84
89
  }
85
90
  return table;
86
91
  }
87
- function parseMarkerProcessIds(stdout, markerEnvironmentVariable, marker) {
92
+ function parsePosixMarkerScan(stdout) {
93
+ const table = new Map();
94
+ const commands = new Map();
95
+ for (const line of stdout.split('\n')) {
96
+ const actual = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S{3}\s+\S{3}\s+\d+\s+\S+\s+\d{4})\s+(.*)$/u.exec(line);
97
+ const synthetic = actual ??
98
+ /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(.*)$/u.exec(line);
99
+ if (!synthetic)
100
+ continue;
101
+ const pid = Number(synthetic[1]);
102
+ table.set(pid, {
103
+ ppid: Number(synthetic[2]),
104
+ pgid: Number(synthetic[3]),
105
+ state: synthetic[4],
106
+ startTime: synthetic[5].replace(/\s+/gu, ' '),
107
+ });
108
+ commands.set(pid, synthetic[6]);
109
+ }
110
+ return { table, commands };
111
+ }
112
+ function parseMarkerProcessIds(scan, markerEnvironmentVariable, marker) {
88
113
  const processIds = new Set();
89
114
  const assignment = `${markerEnvironmentVariable}=${marker}`;
90
- for (const line of stdout.split('\n')) {
91
- const match = /^\s*(\d+)\s+(.+)$/u.exec(line);
92
- if (match?.[2].split(/\s+/u).includes(assignment))
93
- processIds.add(Number(match[1]));
115
+ for (const [pid, command] of scan.commands) {
116
+ if (command.split(/\s+/u).includes(assignment))
117
+ processIds.add(pid);
94
118
  }
95
119
  return processIds;
96
120
  }
97
- async function readPosixProcessTable(execFileImpl, markerEnvironmentVariable, marker, timeoutMs = PROCESS_TREE_COMMAND_TIMEOUT_MS, confirmUnmarkedPid) {
121
+ async function readPosixProcessTable(execFileImpl, markerEnvironmentVariable, marker, timeoutMs = POSIX_PROCESS_TREE_COMMAND_TIMEOUT_MS, confirmUnmarkedPid) {
98
122
  const identityArgs = ['-A', '-o', 'pid=,ppid=,pgid=,state=,lstart='];
99
- const markerArgs = ['eww', '-A', '-o', 'pid=,command='];
123
+ const markerArgs = ['eww', '-A', '-o', 'pid=,ppid=,pgid=,state=,lstart=,command='];
100
124
  const lookupStartedAt = Date.now();
101
- const [identityOutput, initialMarkerResult] = await Promise.all([
102
- runCommand('ps', identityArgs, execFileImpl, timeoutMs),
125
+ const [identityResult, initialMarkerResult] = await Promise.all([
126
+ runCommandWithDiagnostics('ps', identityArgs, execFileImpl, timeoutMs),
103
127
  marker === undefined
104
128
  ? Promise.resolve({ output: '' })
105
129
  : runCommandWithDiagnostics('ps', markerArgs, execFileImpl, timeoutMs),
106
130
  ]);
107
- if (identityOutput === null)
131
+ if (identityResult.output === null)
108
132
  return null;
109
133
  let markerResult = initialMarkerResult;
110
134
  let markerLookupAttempts = marker === undefined ? 0 : 1;
@@ -114,20 +138,23 @@ async function readPosixProcessTable(execFileImpl, markerEnvironmentVariable, ma
114
138
  markerResult = await runCommandWithDiagnostics('ps', markerArgs, execFileImpl, retryTimeoutMs);
115
139
  }
116
140
  const markerOutput = markerResult.output;
117
- const table = parsePosixProcessTable(identityOutput);
118
- const markerPids = marker === undefined || markerOutput === null
141
+ const markerScan = markerOutput === null ? undefined : parsePosixMarkerScan(markerOutput);
142
+ const table = markerScan?.table ?? parsePosixProcessTable(identityResult.output);
143
+ const markerPids = marker === undefined || !markerScan
119
144
  ? new Set()
120
- : parseMarkerProcessIds(markerOutput, markerEnvironmentVariable, marker);
145
+ : parseMarkerProcessIds(markerScan, markerEnvironmentVariable, marker);
121
146
  const confirmedUnmarkedPids = new Set();
122
147
  const unmarkedEntry = confirmUnmarkedPid === undefined || markerPids.has(confirmUnmarkedPid)
123
148
  ? undefined
124
149
  : table.get(confirmUnmarkedPid);
125
150
  if (unmarkedEntry && !unmarkedEntry.state?.startsWith('Z')) {
126
- const confirmationOutput = await runCommand('ps', identityArgs, execFileImpl, timeoutMs);
151
+ const confirmationOutput = await runCommand('ps', markerArgs, execFileImpl, timeoutMs);
127
152
  if (confirmationOutput === null)
128
153
  return null;
129
- const confirmedEntry = parsePosixProcessTable(confirmationOutput).get(confirmUnmarkedPid);
154
+ const confirmationScan = parsePosixMarkerScan(confirmationOutput);
155
+ const confirmedEntry = confirmationScan.table.get(confirmUnmarkedPid);
130
156
  if (confirmedEntry?.startTime === unmarkedEntry.startTime &&
157
+ !parseMarkerProcessIds(confirmationScan, markerEnvironmentVariable, marker).has(confirmUnmarkedPid) &&
131
158
  !confirmedEntry.state?.startsWith('Z')) {
132
159
  confirmedUnmarkedPids.add(confirmUnmarkedPid);
133
160
  }
@@ -141,6 +168,12 @@ async function readPosixProcessTable(execFileImpl, markerEnvironmentVariable, ma
141
168
  confirmedUnmarkedPids,
142
169
  };
143
170
  }
171
+ function readPosixIdentityTable(execFileImpl) {
172
+ return runCommandWithDiagnostics('ps', ['-A', '-o', 'pid=,ppid=,pgid=,state=,lstart='], execFileImpl, POSIX_PROCESS_TREE_COMMAND_TIMEOUT_MS);
173
+ }
174
+ function readPosixMarkerOutput(execFileImpl) {
175
+ return runCommandWithDiagnostics('ps', ['eww', '-A', '-o', 'pid=,ppid=,pgid=,state=,lstart=,command='], execFileImpl, POSIX_PROCESS_TREE_COMMAND_TIMEOUT_MS);
176
+ }
144
177
  function posixMarkerLookupFailure(snapshot, stage) {
145
178
  const fields = [
146
179
  `stage=${stage}`,
@@ -236,9 +269,12 @@ function parseWindowsProcessTable(stdout) {
236
269
  const match = /^\s*(\d+)\|(\d+)\|(.+?)\s*$/u.exec(line);
237
270
  if (!match)
238
271
  continue;
272
+ const startTime = match[3];
273
+ const creationTime = /^\d+$/u.test(startTime) ? BigInt(startTime) : undefined;
239
274
  table.set(Number(match[1]), {
240
275
  ppid: Number(match[2]),
241
- startTime: match[3],
276
+ startTime,
277
+ ...(creationTime !== undefined ? { creationTime } : {}),
242
278
  });
243
279
  }
244
280
  return table;
@@ -251,7 +287,62 @@ function readWindowsProcessTable(execFileImpl) {
251
287
  ].join(' ');
252
288
  return runCommand('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], execFileImpl, PROCESS_TREE_COMMAND_TIMEOUT_MS).then((stdout) => stdout === null ? null : parseWindowsProcessTable(stdout));
253
289
  }
254
- function refreshDescendants(root, knownDescendants, table) {
290
+ function collectDescendantProcessIds(rootPid, table) {
291
+ const childrenByParent = new Map();
292
+ for (const [processId, entry] of table) {
293
+ const children = childrenByParent.get(entry.ppid) ?? [];
294
+ children.push(processId);
295
+ childrenByParent.set(entry.ppid, children);
296
+ }
297
+ const descendants = new Set();
298
+ const visited = new Set([rootPid]);
299
+ const pending = [rootPid];
300
+ while (pending.length > 0) {
301
+ const parent = pending.shift();
302
+ for (const childPid of childrenByParent.get(parent) ?? []) {
303
+ if (visited.has(childPid))
304
+ continue;
305
+ visited.add(childPid);
306
+ descendants.add(childPid);
307
+ pending.push(childPid);
308
+ }
309
+ }
310
+ return descendants;
311
+ }
312
+ function collectComparableWindowsDescendantProcessIds(root, table) {
313
+ if (root?.creationTime === undefined)
314
+ return new Set();
315
+ const childrenByParent = new Map();
316
+ for (const [processId, entry] of table) {
317
+ const children = childrenByParent.get(entry.ppid) ?? [];
318
+ children.push(processId);
319
+ childrenByParent.set(entry.ppid, children);
320
+ }
321
+ const descendants = new Set();
322
+ const visited = new Set([root.pid]);
323
+ const pending = [root];
324
+ while (pending.length > 0) {
325
+ const parent = pending.shift();
326
+ for (const childPid of childrenByParent.get(parent.pid) ?? []) {
327
+ if (visited.has(childPid))
328
+ continue;
329
+ visited.add(childPid);
330
+ const entry = table.get(childPid);
331
+ if (entry.creationTime === undefined ||
332
+ parent.creationTime === undefined ||
333
+ entry.creationTime < parent.creationTime)
334
+ continue;
335
+ descendants.add(childPid);
336
+ pending.push({
337
+ pid: childPid,
338
+ startTime: entry.startTime,
339
+ creationTime: entry.creationTime,
340
+ });
341
+ }
342
+ }
343
+ return descendants;
344
+ }
345
+ function refreshDescendants(root, knownDescendants, table, requireComparableCreationOrder = false) {
255
346
  const childrenByParent = new Map();
256
347
  for (const [processId, entry] of table) {
257
348
  const children = childrenByParent.get(entry.ppid) ?? [];
@@ -259,20 +350,30 @@ function refreshDescendants(root, knownDescendants, table) {
259
350
  childrenByParent.set(entry.ppid, children);
260
351
  }
261
352
  const roots = [root, ...knownDescendants.values()].filter((identity) => identity !== undefined && table.get(identity.pid)?.startTime === identity.startTime);
262
- const pending = roots.map((identity) => identity.pid);
263
- const visited = new Set(pending);
353
+ const pending = [...roots];
354
+ const visited = new Set(pending.map((identity) => identity.pid));
264
355
  while (pending.length > 0) {
265
356
  const parent = pending.shift();
266
- for (const descendantPid of childrenByParent.get(parent) ?? []) {
357
+ for (const descendantPid of childrenByParent.get(parent.pid) ?? []) {
267
358
  if (visited.has(descendantPid))
268
359
  continue;
269
360
  visited.add(descendantPid);
270
361
  const entry = table.get(descendantPid);
362
+ if (requireComparableCreationOrder &&
363
+ (parent.creationTime === undefined ||
364
+ entry.creationTime === undefined ||
365
+ entry.creationTime < parent.creationTime))
366
+ continue;
271
367
  const knownIdentity = knownDescendants.get(descendantPid);
272
368
  if (knownIdentity && knownIdentity.startTime !== entry.startTime)
273
369
  continue;
274
- knownDescendants.set(descendantPid, { pid: descendantPid, startTime: entry.startTime });
275
- pending.push(descendantPid);
370
+ const identity = {
371
+ pid: descendantPid,
372
+ startTime: entry.startTime,
373
+ ...(entry.creationTime !== undefined ? { creationTime: entry.creationTime } : {}),
374
+ };
375
+ knownDescendants.set(descendantPid, identity);
376
+ pending.push(identity);
276
377
  }
277
378
  }
278
379
  }
@@ -282,8 +383,13 @@ function refreshObservedProcesses(state, table, markerPids = new Set(), allowRoo
282
383
  allowRootWithoutMarker ||
283
384
  (state.rootDiscoveryPending && (state.platform === 'win32' || state.marker === undefined)))) {
284
385
  const entry = table.get(state.pid);
285
- if (entry)
286
- state.root = { pid: state.pid, startTime: entry.startTime };
386
+ if (entry && (state.platform !== 'win32' || entry.creationTime !== undefined)) {
387
+ state.root = {
388
+ pid: state.pid,
389
+ startTime: entry.startTime,
390
+ ...(entry.creationTime !== undefined ? { creationTime: entry.creationTime } : {}),
391
+ };
392
+ }
287
393
  }
288
394
  state.rootDiscoveryPending = false;
289
395
  for (const processId of markerPids) {
@@ -294,12 +400,20 @@ function refreshObservedProcesses(state, table, markerPids = new Set(), allowRoo
294
400
  const entry = table.get(processId);
295
401
  if (!entry)
296
402
  continue;
403
+ if (state.platform === 'win32' && entry.creationTime === undefined)
404
+ continue;
297
405
  const known = state.descendants.get(processId);
298
406
  if (!known || known.startTime === entry.startTime) {
299
- state.descendants.set(processId, { pid: processId, startTime: entry.startTime });
407
+ state.descendants.set(processId, {
408
+ pid: processId,
409
+ startTime: entry.startTime,
410
+ ...(entry.creationTime !== undefined ? { creationTime: entry.creationTime } : {}),
411
+ });
300
412
  }
301
413
  }
302
- refreshDescendants(state.root, state.descendants, table);
414
+ if (state.platform === 'win32' || state.marker === undefined) {
415
+ refreshDescendants(state.root, state.descendants, table, state.platform === 'win32');
416
+ }
303
417
  }
304
418
  function latchObservationFailure(state, error) {
305
419
  if (state.failure)
@@ -321,19 +435,30 @@ function acceptPosixSnapshot(state, snapshot, stage) {
321
435
  }
322
436
  }
323
437
  const rootEntry = snapshot.table.get(state.pid);
438
+ let confirmedUnmarkedRoot = snapshot.confirmedUnmarkedPids.has(state.pid);
439
+ if (state.marker !== undefined &&
440
+ rootEntry !== undefined &&
441
+ !rootEntry.state?.startsWith('Z') &&
442
+ (state.root === undefined || state.root.startTime === rootEntry.startTime) &&
443
+ !snapshot.markerPids.has(state.pid)) {
444
+ confirmedUnmarkedRoot ||=
445
+ state.unmarkedRootCandidate?.startTime === rootEntry.startTime;
446
+ state.unmarkedRootCandidate = { pid: state.pid, startTime: rootEntry.startTime };
447
+ }
448
+ else {
449
+ state.unmarkedRootCandidate = undefined;
450
+ }
324
451
  if (state.marker !== undefined &&
325
452
  state.dependencies.enforceCooperativeProcessGroup === true &&
326
453
  rootEntry !== undefined &&
327
454
  !rootEntry.state?.startsWith('Z') &&
328
- snapshot.confirmedUnmarkedPids.has(state.pid)) {
329
- state.root = { pid: state.pid, startTime: rootEntry.startTime };
455
+ confirmedUnmarkedRoot) {
330
456
  latchObservationFailure(state, cleanupUnconfirmed('custom process contract violation: a live provider process lost its process marker.'));
331
457
  }
332
458
  refreshObservedProcesses(state, snapshot.table, snapshot.markerPids);
333
459
  if (state.marker !== undefined) {
334
460
  const providerMarkerPids = [...snapshot.markerPids].filter((pid) => pid !== state.lease?.sentinel.pid);
335
- if (state.dependencies.enforceCooperativeProcessGroup === true &&
336
- state.rootPgidMode === undefined) {
461
+ if (state.rootPgidMode === undefined) {
337
462
  if (state.root?.pid === state.pid &&
338
463
  rootEntry?.startTime === state.root.startTime &&
339
464
  rootEntry.pgid !== undefined &&
@@ -344,6 +469,9 @@ function acceptPosixSnapshot(state, snapshot, stage) {
344
469
  }
345
470
  else {
346
471
  state.rootPgidMode = 'shared';
472
+ if (state.dependencies.enforceCooperativeProcessGroup === true) {
473
+ latchObservationFailure(state, cleanupUnconfirmed('custom process contract violation: the provider root left its initial process group.'));
474
+ }
347
475
  }
348
476
  }
349
477
  else if (rootEntry === undefined &&
@@ -360,7 +488,8 @@ function acceptPosixSnapshot(state, snapshot, stage) {
360
488
  : state.rootPgidMode === 'missing'
361
489
  ? state.pid
362
490
  : undefined;
363
- if (expectedPgid !== undefined &&
491
+ if (state.dependencies.enforceCooperativeProcessGroup === true &&
492
+ expectedPgid !== undefined &&
364
493
  providerMarkerPids.some((pid) => {
365
494
  const entry = snapshot.table.get(pid);
366
495
  return entry?.pgid !== undefined && entry.pgid !== expectedPgid;
@@ -369,31 +498,215 @@ function acceptPosixSnapshot(state, snapshot, stage) {
369
498
  }
370
499
  const liveKnownIdentities = [state.root, ...state.descendants.values()].filter((identity) => identityMatches(identity, snapshot.table) &&
371
500
  !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));
501
+ const markerlessKnownIdentity = liveKnownIdentities.some((identity) => !snapshot.markerPids.has(identity.pid));
374
502
  let markerlessInitialGroupMember = false;
375
- if (state.rootPgid !== undefined) {
503
+ const groupLeader = state.rootPgid === undefined
504
+ ? undefined
505
+ : snapshot.table.get(state.rootPgid);
506
+ const groupLeaderWasReused = state.rootPgidMode === 'owned' &&
507
+ groupLeader !== undefined &&
508
+ groupLeader.startTime !== state.root?.startTime;
509
+ if (state.rootPgid !== undefined && !groupLeaderWasReused) {
376
510
  for (const [pid, entry] of snapshot.table) {
377
511
  if (entry.pgid !== state.rootPgid || entry.state?.startsWith('Z'))
378
512
  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
513
  if (pid !== state.pid && !snapshot.markerPids.has(pid)) {
386
514
  markerlessInitialGroupMember = true;
387
515
  }
388
516
  }
389
517
  }
390
- if (markerlessKnownIdentity || markerlessInitialGroupMember) {
518
+ if (state.dependencies.enforceCooperativeProcessGroup === true &&
519
+ (markerlessKnownIdentity || markerlessInitialGroupMember)) {
391
520
  latchObservationFailure(state, cleanupUnconfirmed('custom process contract violation: a live provider process lost its process marker.'));
392
521
  }
393
522
  }
394
- state.lastSnapshot = snapshot;
395
523
  return true;
396
524
  }
525
+ function getPosixProcessSampler(execFileImpl) {
526
+ const existing = posixSamplers.get(execFileImpl);
527
+ if (existing)
528
+ return existing;
529
+ const sampler = {
530
+ execFile: execFileImpl,
531
+ states: new Set(),
532
+ identityScannedAt: Number.NEGATIVE_INFINITY,
533
+ markerScannedAt: Number.NEGATIVE_INFINITY,
534
+ };
535
+ posixSamplers.set(execFileImpl, sampler);
536
+ return sampler;
537
+ }
538
+ function snapshotFromSampler(state, sampler) {
539
+ const markerLookupSucceeded = state.marker === undefined || sampler.markerFailure === undefined;
540
+ const table = state.marker === undefined || !markerLookupSucceeded
541
+ ? sampler.identityTable
542
+ : sampler.markerScan?.table;
543
+ if (!table)
544
+ return undefined;
545
+ const markerPids = state.marker === undefined || !markerLookupSucceeded || sampler.markerScan === undefined
546
+ ? new Set()
547
+ : parseMarkerProcessIds(sampler.markerScan, state.markerEnvironmentVariable, state.marker);
548
+ return {
549
+ table,
550
+ markerPids,
551
+ markerLookupSucceeded,
552
+ markerLookupAttempts: state.marker === undefined ? 0 : 1,
553
+ ...(sampler.markerFailure ? { markerLookupFailure: sampler.markerFailure } : {}),
554
+ confirmedUnmarkedPids: new Set(),
555
+ };
556
+ }
557
+ function deliverSamplerIdentity(sampler) {
558
+ for (const state of sampler.states) {
559
+ if (state.disposed)
560
+ continue;
561
+ if (sampler.identityFailure) {
562
+ latchObservationFailure(state, cleanupUnconfirmed('POSIX process identity lookup failed.'));
563
+ continue;
564
+ }
565
+ if (state.marker !== undefined) {
566
+ if (sampler.identityTable) {
567
+ refreshDescendants(state.root, state.descendants, sampler.identityTable);
568
+ }
569
+ const expectedPgid = state.rootPgidMode === 'owned'
570
+ ? state.rootPgid
571
+ : state.rootPgidMode === 'missing'
572
+ ? state.pid
573
+ : undefined;
574
+ if (state.dependencies.enforceCooperativeProcessGroup === true &&
575
+ expectedPgid !== undefined &&
576
+ [state.root, ...state.descendants.values()].some((identity) => {
577
+ if (!identity || !sampler.identityTable)
578
+ return false;
579
+ const entry = sampler.identityTable.get(identity.pid);
580
+ return entry?.startTime === identity.startTime &&
581
+ entry.pgid !== undefined && entry.pgid !== expectedPgid;
582
+ })) {
583
+ latchObservationFailure(state, cleanupUnconfirmed('custom process contract violation: a marked process left its initial process group.'));
584
+ }
585
+ continue;
586
+ }
587
+ const snapshot = snapshotFromSampler(state, sampler);
588
+ if (snapshot)
589
+ acceptPosixSnapshot(state, snapshot, 'observation');
590
+ }
591
+ }
592
+ function deliverSamplerMarkers(sampler, stage) {
593
+ for (const state of sampler.states) {
594
+ if (state.disposed || state.marker === undefined)
595
+ continue;
596
+ const snapshot = snapshotFromSampler(state, sampler);
597
+ if (snapshot)
598
+ acceptPosixSnapshot(state, snapshot, stage);
599
+ }
600
+ }
601
+ function scanSamplerIdentity(sampler, force) {
602
+ if (sampler.identityInFlight)
603
+ return sampler.identityInFlight;
604
+ const now = Date.now();
605
+ if (!force &&
606
+ Number.isFinite(sampler.identityScannedAt) &&
607
+ now - sampler.identityScannedAt < PROCESS_TREE_IDENTITY_INTERVAL_MS)
608
+ return Promise.resolve();
609
+ sampler.identityScannedAt = now;
610
+ sampler.identityInFlight = readPosixIdentityTable(sampler.execFile)
611
+ .then((result) => {
612
+ sampler.identityFailure = result.failure;
613
+ sampler.identityTable = result.output === null
614
+ ? undefined
615
+ : parsePosixProcessTable(result.output);
616
+ deliverSamplerIdentity(sampler);
617
+ })
618
+ .finally(() => {
619
+ sampler.identityInFlight = undefined;
620
+ });
621
+ return sampler.identityInFlight;
622
+ }
623
+ function runMarkerScan(sampler) {
624
+ sampler.markerScannedAt = Date.now();
625
+ sampler.markerInFlight = readPosixMarkerOutput(sampler.execFile)
626
+ .then((result) => {
627
+ sampler.markerFailure = result.failure;
628
+ sampler.markerScan = result.output === null
629
+ ? undefined
630
+ : parsePosixMarkerScan(result.output);
631
+ deliverSamplerMarkers(sampler, 'observation');
632
+ })
633
+ .finally(() => {
634
+ sampler.markerInFlight = undefined;
635
+ });
636
+ return sampler.markerInFlight;
637
+ }
638
+ function scanSamplerMarkers(sampler, force) {
639
+ if (sampler.markerInFlight)
640
+ return sampler.markerInFlight;
641
+ const now = Date.now();
642
+ if (!force &&
643
+ Number.isFinite(sampler.markerScannedAt) &&
644
+ now - sampler.markerScannedAt < PROCESS_TREE_MARKER_INTERVAL_MS)
645
+ return Promise.resolve();
646
+ return runMarkerScan(sampler);
647
+ }
648
+ function schedulePosixSampler(sampler) {
649
+ if (sampler.timer)
650
+ clearTimeout(sampler.timer);
651
+ sampler.timer = undefined;
652
+ if (sampler.states.size === 0)
653
+ return;
654
+ const now = Date.now();
655
+ const identityDelay = Math.max(0, PROCESS_TREE_IDENTITY_INTERVAL_MS - (now - sampler.identityScannedAt));
656
+ const markerDelay = [...sampler.states].some((state) => state.marker !== undefined)
657
+ ? Math.max(0, PROCESS_TREE_MARKER_INTERVAL_MS - (now - sampler.markerScannedAt))
658
+ : Number.POSITIVE_INFINITY;
659
+ const delayMs = Math.min(identityDelay, markerDelay);
660
+ sampler.timer = setTimeout(() => {
661
+ sampler.timer = undefined;
662
+ const current = Date.now();
663
+ const scans = [];
664
+ if (current - sampler.identityScannedAt >= PROCESS_TREE_IDENTITY_INTERVAL_MS) {
665
+ scans.push(scanSamplerIdentity(sampler, false));
666
+ }
667
+ if (markerDelay !== Number.POSITIVE_INFINITY &&
668
+ current - sampler.markerScannedAt >= PROCESS_TREE_MARKER_INTERVAL_MS) {
669
+ scans.push(scanSamplerMarkers(sampler, false));
670
+ }
671
+ void Promise.all(scans).finally(() => schedulePosixSampler(sampler));
672
+ }, delayMs);
673
+ sampler.timer.unref();
674
+ }
675
+ function stopObservingPosixState(state) {
676
+ const sampler = state.sampler;
677
+ if (!sampler)
678
+ return;
679
+ state.sampler = undefined;
680
+ sampler.states.delete(state);
681
+ schedulePosixSampler(sampler);
682
+ }
683
+ async function readImmediatePosixSnapshot(state, stage) {
684
+ const sampler = state.sampler ?? getPosixProcessSampler(state.dependencies.execFile ?? defaultExecFile);
685
+ if (state.marker === undefined)
686
+ await scanSamplerIdentity(sampler, true);
687
+ else
688
+ await scanSamplerMarkers(sampler, true);
689
+ if (state.marker === undefined && sampler.identityFailure) {
690
+ latchObservationFailure(state, cleanupUnconfirmed('POSIX process identity lookup failed.'));
691
+ return undefined;
692
+ }
693
+ if (state.marker !== undefined && sampler.markerFailure) {
694
+ latchObservationFailure(state, posixMarkerLookupFailure({
695
+ table: new Map(),
696
+ markerPids: new Set(),
697
+ markerLookupSucceeded: false,
698
+ markerLookupAttempts: 1,
699
+ markerLookupFailure: sampler.markerFailure,
700
+ confirmedUnmarkedPids: new Set(),
701
+ }, stage));
702
+ return undefined;
703
+ }
704
+ const snapshot = snapshotFromSampler(state, sampler);
705
+ if (!snapshot)
706
+ return undefined;
707
+ acceptPosixSnapshot(state, snapshot, stage);
708
+ return snapshot;
709
+ }
397
710
  function createProcessTreeObservation(pid, marker, dependencies, startLookup, lease) {
398
711
  const observation = {
399
712
  pid,
@@ -410,8 +723,7 @@ function createProcessTreeObservation(pid, marker, dependencies, startLookup, le
410
723
  const state = observationStates.get(observation);
411
724
  if (state) {
412
725
  state.disposed = true;
413
- if (state.pollTimer)
414
- clearTimeout(state.pollTimer);
726
+ stopObservingPosixState(state);
415
727
  }
416
728
  },
417
729
  };
@@ -423,6 +735,7 @@ function createProcessTreeObservation(pid, marker, dependencies, startLookup, le
423
735
  lease,
424
736
  dependencies,
425
737
  rootDiscoveryPending: true,
738
+ initialWindowsLookupSucceeded: false,
426
739
  descendants: new Map(),
427
740
  failureListeners: new Set(),
428
741
  disposed: false,
@@ -432,34 +745,37 @@ function createProcessTreeObservation(pid, marker, dependencies, startLookup, le
432
745
  if (!startLookup)
433
746
  return observation;
434
747
  const execFileImpl = dependencies.execFile ?? defaultExecFile;
435
- const refresh = () => state.platform === 'win32'
436
- ? readWindowsProcessTable(execFileImpl).then((table) => {
437
- if (!state.disposed && table)
748
+ if (state.platform === 'win32') {
749
+ state.initialLookup = readWindowsProcessTable(execFileImpl).then((table) => {
750
+ if (!state.disposed && table) {
751
+ state.initialWindowsLookupSucceeded = true;
438
752
  refreshObservedProcesses(state, table);
439
- })
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;
448
753
  }
449
- acceptPosixSnapshot(state, snapshot, 'observation');
450
754
  });
451
- const scheduleRefresh = () => {
755
+ return observation;
756
+ }
757
+ const sampler = getPosixProcessSampler(execFileImpl);
758
+ const periodic = dependencies.enforceCooperativeProcessGroup === true;
759
+ if (periodic) {
760
+ state.sampler = sampler;
761
+ sampler.states.add(state);
762
+ }
763
+ state.initialLookup = (marker === undefined
764
+ ? scanSamplerIdentity(sampler, true)
765
+ : Promise.all([
766
+ ...(periodic ? [scanSamplerIdentity(sampler, true)] : []),
767
+ scanSamplerMarkers(sampler, true),
768
+ ])).then(() => {
452
769
  if (state.disposed)
453
770
  return;
454
- state.pollTimer = setTimeout(() => {
455
- state.pollTimer = undefined;
456
- void refresh().finally(scheduleRefresh);
457
- }, PROCESS_TREE_OBSERVATION_INTERVAL_MS);
458
- state.pollTimer.unref();
459
- };
460
- state.initialLookup = refresh();
461
- if (state.platform !== 'win32')
462
- void state.initialLookup.finally(scheduleRefresh);
771
+ if (periodic)
772
+ return;
773
+ const snapshot = snapshotFromSampler(state, sampler);
774
+ if (snapshot)
775
+ acceptPosixSnapshot(state, snapshot, 'observation');
776
+ });
777
+ if (periodic)
778
+ void state.initialLookup.finally(() => schedulePosixSampler(sampler));
463
779
  return observation;
464
780
  }
465
781
  export function observeProcessTree(pid, marker, dependencies = {}, lease) {
@@ -489,8 +805,7 @@ function signalIdentity(identity, snapshot, signal, kill) {
489
805
  }
490
806
  }
491
807
  function safelyOwnedProcessGroup(state, snapshot) {
492
- if (state.dependencies.enforceCooperativeProcessGroup !== true ||
493
- state.marker === undefined ||
808
+ if (state.marker === undefined ||
494
809
  state.rootPgid === undefined ||
495
810
  state.rootPgid !== state.pid)
496
811
  return undefined;
@@ -498,6 +813,13 @@ function safelyOwnedProcessGroup(state, snapshot) {
498
813
  const members = [...snapshot.table].filter(([, entry]) => entry.pgid === pgid && !entry.state?.startsWith('Z'));
499
814
  if (members.length === 0)
500
815
  return undefined;
816
+ if (state.rootPgidMode === 'owned') {
817
+ const currentLeader = snapshot.table.get(pgid);
818
+ if (currentLeader !== undefined &&
819
+ currentLeader.startTime !== state.root?.startTime)
820
+ return undefined;
821
+ return pgid;
822
+ }
501
823
  const known = [state.root, ...state.descendants.values()];
502
824
  if (members.some(([pid, entry]) => !known.some((identity) => identity?.pid === pid && identity.startTime === entry.startTime)))
503
825
  return undefined;
@@ -527,7 +849,7 @@ function latchLiveDescendantCompletionFailure(state, snapshot) {
527
849
  async function runTaskkill(pid, execFileImpl) {
528
850
  return (await runCommand('taskkill', ['/PID', String(pid), '/T', '/F'], execFileImpl, PROCESS_TREE_COMMAND_TIMEOUT_MS)) !== null;
529
851
  }
530
- async function terminateWindowsProcessTree(state, requireConfirmedCleanup) {
852
+ async function terminateWindowsProcessTree(state, requireConfirmedCleanup, allowUnobservedProcessOnNormalCompletion) {
531
853
  const execFileImpl = state.dependencies.execFile ?? defaultExecFile;
532
854
  if (!requireConfirmedCleanup) {
533
855
  if (!(await runTaskkill(state.pid, execFileImpl))) {
@@ -535,139 +857,142 @@ async function terminateWindowsProcessTree(state, requireConfirmedCleanup) {
535
857
  }
536
858
  return;
537
859
  }
860
+ const identitiesBeforeNormalCompletion = allowUnobservedProcessOnNormalCompletion
861
+ ? {
862
+ root: state.root,
863
+ descendants: new Map(state.descendants),
864
+ }
865
+ : undefined;
538
866
  await state.initialLookup;
539
867
  const table = await readWindowsProcessTable(execFileImpl);
540
868
  if (!table) {
541
869
  throw cleanupUnconfirmed('Windows process identity lookup failed.');
542
870
  }
543
871
  refreshObservedProcesses(state, table);
544
- const rootTarget = identityMatches(state.root, table) ? state.root : undefined;
545
- const liveDescendants = [...state.descendants.values()].filter((identity) => identityMatches(identity, table));
872
+ const root = identitiesBeforeNormalCompletion
873
+ ? identitiesBeforeNormalCompletion.root
874
+ : state.root;
875
+ const descendants = identitiesBeforeNormalCompletion?.descendants ?? state.descendants;
876
+ const observedIdentities = [root, ...descendants.values()].filter((identity) => identity !== undefined);
877
+ const candidatePids = collectDescendantProcessIds(state.pid, table);
878
+ const comparableCandidatePids = collectComparableWindowsDescendantProcessIds(root, table);
879
+ const hasInvalidCandidateEdge = [...candidatePids].some((pid) => !comparableCandidatePids.has(pid));
880
+ const hasUnobservedCandidate = [...candidatePids].some((pid) => !identityMatches(descendants.get(pid), table));
881
+ const observedPidWasReused = (candidateTable) => observedIdentities.some((identity) => {
882
+ const entry = candidateTable.get(identity.pid);
883
+ return entry !== undefined && entry.startTime !== identity.startTime;
884
+ });
885
+ const rootTarget = identityMatches(root, table) ? root : undefined;
886
+ const liveDescendants = [...descendants.values()].filter((identity) => identityMatches(identity, table));
546
887
  const liveDescendantPids = new Set(liveDescendants.map((identity) => identity.pid));
547
888
  const targets = rootTarget
548
889
  ? [rootTarget]
549
890
  : liveDescendants.filter((identity) => !liveDescendantPids.has(table.get(identity.pid).ppid));
550
- const taskkillResults = await Promise.all(targets.map((identity) => runTaskkill(identity.pid, execFileImpl)));
891
+ const taskkillResults = hasInvalidCandidateEdge || hasUnobservedCandidate
892
+ ? []
893
+ : await Promise.all(targets.map((identity) => runTaskkill(identity.pid, execFileImpl)));
551
894
  if (taskkillResults.some((result) => !result)) {
552
895
  throw cleanupUnconfirmed('taskkill did not confirm termination.');
553
896
  }
554
897
  const verifiedTable = await readWindowsProcessTable(execFileImpl);
555
898
  if (!verifiedTable)
556
899
  throw cleanupUnconfirmed('Windows termination verification failed.');
900
+ const verifiedCandidatePids = collectDescendantProcessIds(state.pid, verifiedTable);
557
901
  const verifiedIdentities = [rootTarget, ...liveDescendants].filter((identity) => identity !== undefined);
558
902
  if (verifiedIdentities.some((identity) => identityMatches(identity, verifiedTable))) {
559
903
  throw cleanupUnconfirmed('a verified Windows process is still running.');
560
904
  }
561
- if (requireConfirmedCleanup && targets.length === 0) {
562
- throw cleanupUnconfirmed('no inherited-pipe process identity was observed.');
905
+ if (hasInvalidCandidateEdge) {
906
+ throw cleanupUnconfirmed(hasUnobservedCandidate
907
+ ? 'an unobserved Windows descendant candidate has invalid process creation order.'
908
+ : 'a Windows descendant candidate has invalid process creation order.');
909
+ }
910
+ if (hasUnobservedCandidate) {
911
+ throw cleanupUnconfirmed('an unobserved Windows descendant candidate was found after the root exited.');
912
+ }
913
+ if (verifiedCandidatePids.size > 0) {
914
+ throw cleanupUnconfirmed('a Windows descendant candidate is still running.');
915
+ }
916
+ const unobservedNormalCompletionIsAllowed = allowUnobservedProcessOnNormalCompletion &&
917
+ state.initialWindowsLookupSucceeded &&
918
+ !table.has(state.pid) &&
919
+ candidatePids.size === 0 &&
920
+ !verifiedTable.has(state.pid) &&
921
+ verifiedCandidatePids.size === 0;
922
+ if (requireConfirmedCleanup &&
923
+ targets.length === 0 &&
924
+ (observedPidWasReused(table) ||
925
+ observedPidWasReused(verifiedTable) ||
926
+ (observedIdentities.length === 0 && !unobservedNormalCompletionIsAllowed))) {
927
+ throw cleanupUnconfirmed(observedIdentities.length > 0
928
+ ? 'an observed Windows process identifier was reused.'
929
+ : 'no inherited-pipe process identity was observed.');
563
930
  }
564
931
  }
565
932
  async function terminatePosixProcessTree(state, requireConfirmedCleanup, allowInspectionUnavailable, requireDescendantsExitedBeforeCompletion) {
566
933
  await state.initialLookup;
567
934
  state.disposed = true;
568
- if (state.pollTimer)
569
- clearTimeout(state.pollTimer);
570
- const execFileImpl = state.dependencies.execFile ?? defaultExecFile;
935
+ stopObservingPosixState(state);
571
936
  const kill = state.dependencies.kill ?? process.kill.bind(process);
572
- const deadline = Date.now() + PROCESS_TREE_COMMAND_TIMEOUT_MS;
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;
937
+ let snapshot = await readImmediatePosixSnapshot(state, 'termination-start');
587
938
  if (!snapshot) {
588
939
  if (!requireConfirmedCleanup && allowInspectionUnavailable)
589
940
  return;
590
941
  throw state.failure ?? cleanupUnconfirmed('POSIX process identity lookup failed.');
591
942
  }
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
943
  if (requireDescendantsExitedBeforeCompletion) {
598
944
  latchLiveDescendantCompletionFailure(state, snapshot);
599
945
  }
600
- const observedAtTermination = new Set([...state.descendants.values()]
601
- .filter((identity) => posixIdentityMatches(identity, snapshot))
602
- .map((identity) => `${identity.pid}:${identity.startTime}`));
603
- while (Date.now() < deadline) {
604
- signalProcessGroup(processGroupCurrentlyVerified ? safelyOwnedProcessGroup(state, snapshot) : undefined, 'SIGSTOP', kill);
605
- if (posixIdentityMatches(state.root, snapshot)) {
606
- signalIdentity(state.root, snapshot, 'SIGSTOP', kill);
607
- }
608
- for (const identity of [...state.descendants.values()].reverse()) {
609
- signalIdentity(identity, snapshot, 'SIGSTOP', kill);
610
- }
611
- const next = await inspect(Math.max(1, deadline - Date.now()), 'termination-freeze');
612
- if (!next) {
613
- processGroupCurrentlyVerified = false;
614
- break;
615
- }
616
- snapshot = next;
617
- processGroupCurrentlyVerified = true;
618
- const previousSize = state.descendants.size;
619
- signalProcessGroup(safelyOwnedProcessGroup(state, snapshot), 'SIGSTOP', kill);
620
- if (posixIdentityMatches(state.root, snapshot)) {
621
- signalIdentity(state.root, snapshot, 'SIGSTOP', kill);
946
+ const liveIdentities = (current) => [state.root, ...state.descendants.values()].filter((identity) => posixIdentityMatches(identity, current) &&
947
+ !current.table.get(identity.pid)?.state?.startsWith('Z'));
948
+ const liveMarkerPids = (current) => state.marker === undefined
949
+ ? []
950
+ : [...current.markerPids].filter((pid) => pid !== state.lease?.sentinel.pid &&
951
+ !current.table.get(pid)?.state?.startsWith('Z'));
952
+ const signalOwned = (current, signal) => {
953
+ if (state.marker !== undefined && !current.markerLookupSucceeded)
954
+ return;
955
+ signalProcessGroup(safelyOwnedProcessGroup(state, current), signal, kill);
956
+ for (const identity of liveIdentities(current).reverse()) {
957
+ signalIdentity(identity, current, signal, kill);
622
958
  }
623
- for (const identity of [...state.descendants.values()].reverse()) {
624
- signalIdentity(identity, snapshot, 'SIGSTOP', kill);
959
+ };
960
+ const initialTargets = liveIdentities(snapshot);
961
+ const initialMarkerPids = liveMarkerPids(snapshot);
962
+ const initialProcessGroup = safelyOwnedProcessGroup(state, snapshot);
963
+ if (initialTargets.length === 0 &&
964
+ initialMarkerPids.length === 0 &&
965
+ initialProcessGroup === undefined) {
966
+ if (requireConfirmedCleanup && state.marker === undefined) {
967
+ throw cleanupUnconfirmed('no inherited-pipe process identity was observed.');
625
968
  }
626
- const allFrozen = [state.root, ...state.descendants.values()].every((identity) => {
627
- if (!posixIdentityMatches(identity, snapshot))
628
- return true;
629
- const processState = snapshot.table.get(identity.pid)?.state;
630
- return processState?.startsWith('T') || processState?.startsWith('Z');
631
- });
632
- if (allFrozen && state.descendants.size === previousSize)
633
- break;
634
- }
635
- const killed = [state.root, ...state.descendants.values()].filter((identity) => identity !== undefined &&
636
- posixIdentityMatches(identity, snapshot));
637
- const ownedPgid = processGroupCurrentlyVerified
638
- ? safelyOwnedProcessGroup(state, snapshot)
639
- : undefined;
640
- for (const identity of killed.reverse()) {
641
- signalIdentity(identity, snapshot, 'SIGKILL', kill);
642
- }
643
- signalProcessGroup(ownedPgid, 'SIGKILL', kill);
644
- if (!requireConfirmedCleanup) {
645
969
  if (state.failure)
646
970
  throw state.failure;
647
971
  return;
648
972
  }
649
- const verified = await inspect(PROCESS_TREE_COMMAND_TIMEOUT_MS, 'termination-verification');
973
+ signalOwned(snapshot, 'SIGTERM');
974
+ await new Promise((resolve) => {
975
+ setTimeout(resolve, PROCESS_TREE_TERMINATION_GRACE_MS);
976
+ });
977
+ const killSnapshot = await readImmediatePosixSnapshot(state, 'termination-kill');
978
+ if (!killSnapshot) {
979
+ throw state.failure ?? cleanupUnconfirmed('POSIX termination audit failed.');
980
+ }
981
+ snapshot = killSnapshot;
982
+ signalOwned(snapshot, 'SIGKILL');
983
+ await new Promise((resolve) => {
984
+ setTimeout(resolve, PROCESS_TREE_TERMINATION_SETTLE_MS);
985
+ });
986
+ const verified = await readImmediatePosixSnapshot(state, 'termination-verification');
650
987
  if (!verified) {
651
988
  throw state.failure ?? cleanupUnconfirmed('POSIX termination verification failed.');
652
989
  }
653
- const stillRunning = killed.some((identity) => {
654
- if (!posixIdentityMatches(identity, verified))
655
- return false;
656
- return !verified.table.get(identity.pid)?.state?.startsWith('Z');
657
- });
658
- if (stillRunning) {
990
+ if (liveIdentities(verified).length > 0) {
659
991
  throw state.failure ?? cleanupUnconfirmed('a verified POSIX descendant is still running.');
660
992
  }
661
- const unobservedMarkedProcess = state.marker === undefined
662
- ? false
663
- : [...verified.markerPids].some((pid) => pid !== state.lease?.sentinel.pid &&
664
- !verified.table.get(pid)?.state?.startsWith('Z'));
665
- if (unobservedMarkedProcess) {
993
+ if (liveMarkerPids(verified).length > 0) {
666
994
  throw state.failure ?? cleanupUnconfirmed('an unverified marker process is still running.');
667
995
  }
668
- if (requireConfirmedCleanup && state.marker === undefined && observedAtTermination.size === 0) {
669
- throw cleanupUnconfirmed('no inherited-pipe process identity was observed.');
670
- }
671
996
  if (state.failure)
672
997
  throw state.failure;
673
998
  }
@@ -681,7 +1006,7 @@ export async function terminateProcessTree(pid, killDirectChild, dependencies =
681
1006
  throw cleanupUnconfirmed('process observation is invalid.');
682
1007
  }
683
1008
  if (platform === 'win32') {
684
- await terminateWindowsProcessTree(state, options.requireConfirmedCleanup ?? false);
1009
+ await terminateWindowsProcessTree(state, options.requireConfirmedCleanup ?? false, options.allowUnobservedWindowsProcessOnNormalCompletion ?? false);
685
1010
  return;
686
1011
  }
687
1012
  await terminatePosixProcessTree(state, options.requireConfirmedCleanup ?? false, options.allowInspectionUnavailable ?? false, options.requireDescendantsExitedBeforeCompletion ?? false);