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