@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.
- package/dist/bin.js +167 -39
- package/dist/bin.js.map +1 -1
- package/dist/codex-eval-app-server.d.ts +199 -0
- package/dist/codex-eval-app-server.js +1364 -0
- package/dist/codex-eval-app-server.js.map +1 -0
- package/dist/codex-eval-cases.d.ts +43 -0
- package/dist/codex-eval-cases.js +327 -0
- package/dist/codex-eval-cases.js.map +1 -0
- package/dist/codex-eval-isolation.d.ts +79 -0
- package/dist/codex-eval-isolation.js +420 -0
- package/dist/codex-eval-isolation.js.map +1 -0
- package/dist/codex-eval-process-tree.d.ts +38 -0
- package/dist/codex-eval-process-tree.js +803 -0
- package/dist/codex-eval-process-tree.js.map +1 -0
- package/dist/codex-eval-runtime-config.d.ts +43 -0
- package/dist/codex-eval-runtime-config.js +607 -0
- package/dist/codex-eval-runtime-config.js.map +1 -0
- package/dist/codex-trigger-eval.d.ts +82 -0
- package/dist/codex-trigger-eval.js +790 -0
- package/dist/codex-trigger-eval.js.map +1 -0
- package/dist/codex-workflow.d.ts +191 -0
- package/dist/codex-workflow.js +1832 -0
- package/dist/codex-workflow.js.map +1 -0
- package/dist/credentials.d.ts +5 -0
- package/dist/credentials.js +30 -0
- package/dist/credentials.js.map +1 -0
- package/dist/custom-process.d.ts +60 -0
- package/dist/custom-process.js +423 -0
- package/dist/custom-process.js.map +1 -0
- package/dist/eval-audit.d.ts +38 -0
- package/dist/eval-audit.js +71 -0
- package/dist/eval-audit.js.map +1 -0
- package/dist/eval-file.d.ts +9 -0
- package/dist/eval-file.js +15 -0
- package/dist/eval-file.js.map +1 -0
- package/dist/eval-process-management.d.ts +21 -0
- package/dist/eval-process-management.js +84 -0
- package/dist/eval-process-management.js.map +1 -0
- package/dist/eval-result.d.ts +79 -0
- package/dist/eval-result.js +144 -0
- package/dist/eval-result.js.map +1 -0
- package/dist/eval-run.d.ts +27 -0
- package/dist/eval-run.js +564 -0
- package/dist/eval-run.js.map +1 -0
- package/dist/eval-spec.d.ts +57 -0
- package/dist/eval-spec.js +120 -0
- package/dist/eval-spec.js.map +1 -0
- package/dist/eval-suite-binding.d.ts +4 -0
- package/dist/eval-suite-binding.js +19 -0
- package/dist/eval-suite-binding.js.map +1 -0
- package/dist/index.d.ts +36 -9
- package/dist/index.js +19 -6
- package/dist/index.js.map +1 -1
- package/dist/json-schema.js +75 -5
- package/dist/json-schema.js.map +1 -1
- package/dist/output-path.js +2 -2
- package/dist/process-tree.d.ts +19 -1
- package/dist/process-tree.js +391 -77
- package/dist/process-tree.js.map +1 -1
- package/dist/process.d.ts +0 -2
- package/dist/process.js +76 -25
- package/dist/process.js.map +1 -1
- package/dist/provider-doctor.d.ts +45 -0
- package/dist/provider-doctor.js +112 -0
- package/dist/provider-doctor.js.map +1 -0
- package/dist/providers.d.ts +83 -0
- package/dist/providers.js +632 -0
- package/dist/providers.js.map +1 -0
- package/dist/run-paths.d.ts +8 -0
- package/dist/run-paths.js +71 -0
- package/dist/run-paths.js.map +1 -0
- package/dist/runtime-skill.d.ts +6 -0
- package/dist/runtime-skill.js +87 -3
- package/dist/runtime-skill.js.map +1 -1
- package/dist/schemas/digests.json +3 -3
- package/dist/schemas/eval-audit.schema.json +64 -0
- package/dist/schemas/eval-result.schema.json +232 -0
- package/dist/schemas/eval-spec.schema.json +109 -0
- package/dist/schemas.d.ts +1 -3
- package/dist/schemas.js +0 -17
- package/dist/schemas.js.map +1 -1
- package/dist/selected-cases.d.ts +30 -0
- package/dist/selected-cases.js +388 -0
- package/dist/selected-cases.js.map +1 -0
- package/dist/types.d.ts +2 -34
- package/package.json +8 -3
- package/dist/bundle.d.ts +0 -93
- package/dist/bundle.js +0 -216
- package/dist/bundle.js.map +0 -1
- package/dist/capabilities.d.ts +0 -2
- package/dist/capabilities.js +0 -38
- package/dist/capabilities.js.map +0 -1
- package/dist/plan.d.ts +0 -79
- package/dist/plan.js +0 -285
- package/dist/plan.js.map +0 -1
- package/dist/run.d.ts +0 -2
- package/dist/run.js +0 -592
- package/dist/run.js.map +0 -1
- package/dist/schemas/aw-skill-eval-execution-plan.schema.json +0 -220
- package/dist/schemas/eval-run-bundle.schema.json +0 -408
- package/dist/schemas/eval-runtime-capabilities.schema.json +0 -85
package/dist/process-tree.js
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
|
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
|
|
37
|
-
if (
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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 =
|
|
58
|
-
const [
|
|
59
|
-
|
|
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
|
-
:
|
|
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
|
|
68
|
-
markerPids
|
|
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
|
|
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
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
199
|
-
|
|
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
|
-
|
|
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 (
|
|
282
|
-
|
|
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
|
|
297
|
-
if (!next)
|
|
298
|
-
|
|
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
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
643
|
+
signalProcessGroup(ownedPgid, 'SIGKILL', kill);
|
|
644
|
+
if (!requireConfirmedCleanup) {
|
|
645
|
+
if (state.failure)
|
|
646
|
+
throw state.failure;
|
|
330
647
|
return;
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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) =>
|
|
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();
|