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