@deeeed/metamask-harness 0.77.0 → 0.78.0
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/CHANGELOG.md +8 -0
- package/README.md +8 -5
- package/adapters/extension/build-handoff.cjs +2 -2
- package/adapters/extension/ensure-browser.sh +10 -12
- package/adapters/extension/inject.mjs +10 -7
- package/adapters/extension/launch-browser.cjs +7 -10
- package/adapters/extension/live.sh +1 -2
- package/adapters/extension/reattach.sh +3 -3
- package/adapters/extension/sidepanel-toggle.sh +4 -4
- package/adapters/extension/wallet-fixture-state.cjs +2 -2
- package/adapters/manifest.json +0 -24
- package/adapters/shared/harness-source-fingerprint.mjs +16 -2
- package/adapters/terminal/launch.mjs +2 -2
- package/adapters/terminal/wallet-host.mjs +1 -1
- package/dist/adapters/extension/doctor-checks.js +2 -2
- package/dist/adapters/extension/launched-browser.js +1 -1
- package/dist/adapters/extension/runtime.js +4 -4
- package/dist/adapters/extension/validation-process-ownership.js +1 -1
- package/dist/commands/launch/extension.js +1 -1
- package/docs/cli-contract.md +6 -17
- package/library/actions/extension/platform/cdp.mjs +5 -7
- package/library/actions/terminal/platform/page.mjs +2 -2
- package/package.json +2 -1
- package/adapters/extension/lib/chrome-args.cjs +0 -365
- package/adapters/extension/lib/extension-id.cjs +0 -36
- package/adapters/extension/lib/macos-focus.cjs +0 -108
- package/adapters/extension/lib/playwright-cdp.cjs +0 -50
- package/adapters/extension/lib/validation-launch-supervisor.cjs +0 -292
- package/adapters/extension/lib/validation-process-ownership.cjs +0 -125
- package/adapters/shared/browser-cdp.cjs +0 -542
- package/adapters/shared/browser-resolver.cjs +0 -1007
|
@@ -1,292 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const dgram = require('node:dgram');
|
|
5
|
-
const { execFileSync, spawn } = require('node:child_process');
|
|
6
|
-
const {
|
|
7
|
-
clearValidationPortQuarantine,
|
|
8
|
-
createRuntimeIdentityNonce,
|
|
9
|
-
hasDetachedLaunchUnproven,
|
|
10
|
-
markValidationPortLaunchUnproven,
|
|
11
|
-
quarantineDetachedLaunch,
|
|
12
|
-
quarantineValidationPort,
|
|
13
|
-
readValidationPortQuarantine,
|
|
14
|
-
validationLaunchQuarantineError,
|
|
15
|
-
} = require('./chrome-args.cjs');
|
|
16
|
-
const { stopProfileProcesses } = require('./validation-process-ownership.cjs');
|
|
17
|
-
|
|
18
|
-
const port = Number(process.argv[2]);
|
|
19
|
-
if (!Number.isInteger(port) || port <= 0 || port > 65535 || typeof process.send !== 'function') {
|
|
20
|
-
process.exit(2);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const socket = dgram.createSocket('udp4');
|
|
24
|
-
let launchChild;
|
|
25
|
-
let launchLease;
|
|
26
|
-
let ownsPortLease = false;
|
|
27
|
-
let launchRequest;
|
|
28
|
-
let launchFinished = false;
|
|
29
|
-
let finishStarted = false;
|
|
30
|
-
let finalizing = false;
|
|
31
|
-
let releaseRequest;
|
|
32
|
-
let profileCleaned = false;
|
|
33
|
-
|
|
34
|
-
process.stdout.on('error', () => {});
|
|
35
|
-
process.stderr.on('error', () => {});
|
|
36
|
-
|
|
37
|
-
function send(message, callback = () => {}) {
|
|
38
|
-
if (!process.connected) {
|
|
39
|
-
callback();
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
process.send(message, callback);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function signalLaunchTree(signal) {
|
|
46
|
-
if (!launchChild?.pid) return;
|
|
47
|
-
if (process.platform === 'win32') launchChild.kill(signal);
|
|
48
|
-
else signalPids(launchTreePids(), signal);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function launchTreeExists() {
|
|
52
|
-
if (!launchChild?.pid) return false;
|
|
53
|
-
if (process.platform !== 'win32') return launchTreePids().length > 0;
|
|
54
|
-
try {
|
|
55
|
-
process.kill(launchChild.pid, 0);
|
|
56
|
-
return true;
|
|
57
|
-
} catch (error) {
|
|
58
|
-
return error.code !== 'ESRCH';
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function launchTreePids() {
|
|
63
|
-
const output = execFileSync('ps', ['-axo', 'pid=,pgid='], {
|
|
64
|
-
encoding: 'utf8',
|
|
65
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
66
|
-
});
|
|
67
|
-
const pids = [];
|
|
68
|
-
for (const line of output.split('\n')) {
|
|
69
|
-
const match = line.match(/^\s*(\d+)\s+(\d+)\s*$/u);
|
|
70
|
-
if (match && Number(match[2]) === launchChild.pid) pids.push(Number(match[1]));
|
|
71
|
-
}
|
|
72
|
-
return pids.filter((pid) => pid !== process.pid);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function delay(milliseconds) {
|
|
76
|
-
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async function stopRemainingLaunchTree() {
|
|
80
|
-
if (!launchTreeExists()) return;
|
|
81
|
-
signalLaunchTree('SIGTERM');
|
|
82
|
-
const termDeadline = Date.now() + 1_000;
|
|
83
|
-
while (launchTreeExists() && Date.now() < termDeadline) await delay(50);
|
|
84
|
-
if (!launchTreeExists()) return;
|
|
85
|
-
signalLaunchTree('SIGKILL');
|
|
86
|
-
const killDeadline = Date.now() + 1_000;
|
|
87
|
-
while (launchTreeExists() && Date.now() < killDeadline) await delay(50);
|
|
88
|
-
if (launchTreeExists()) throw new Error(`Extension validation launcher process group ${launchChild.pid} survived SIGKILL.`);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function signalPids(pids, signal) {
|
|
92
|
-
for (const pid of pids) {
|
|
93
|
-
try {
|
|
94
|
-
process.kill(pid, signal);
|
|
95
|
-
} catch (error) {
|
|
96
|
-
if (error.code !== 'ESRCH') throw error;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function closeSocket() {
|
|
102
|
-
return new Promise((resolve) => socket.close(resolve));
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async function cleanProfile(quietMs) {
|
|
106
|
-
await stopProfileProcesses(launchRequest.profile, { quietMs });
|
|
107
|
-
profileCleaned = true;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function quarantineProfile(error) {
|
|
111
|
-
let quarantineError;
|
|
112
|
-
try {
|
|
113
|
-
quarantineDetachedLaunch(launchRequest.profile, error.message);
|
|
114
|
-
} catch (markerError) {
|
|
115
|
-
quarantineError = markerError;
|
|
116
|
-
}
|
|
117
|
-
try {
|
|
118
|
-
quarantineValidationPort(port, launchRequest.profile, error.message);
|
|
119
|
-
} catch (markerError) {
|
|
120
|
-
quarantineError ||= markerError;
|
|
121
|
-
}
|
|
122
|
-
return quarantineError
|
|
123
|
-
? new Error(`${error.message} Extension validation could not persist quarantine: ${quarantineError.message}`)
|
|
124
|
-
: error;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async function finalize(preserveProfileOwner) {
|
|
128
|
-
if (finalizing) return;
|
|
129
|
-
finalizing = true;
|
|
130
|
-
let finalError;
|
|
131
|
-
if (launchRequest && !preserveProfileOwner && !profileCleaned) {
|
|
132
|
-
try {
|
|
133
|
-
await cleanProfile(2_000);
|
|
134
|
-
} catch (error) {
|
|
135
|
-
finalError = quarantineProfile(error);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
if (launchRequest && ownsPortLease && !finalError) {
|
|
139
|
-
try {
|
|
140
|
-
const detachedLaunchUnproven = hasDetachedLaunchUnproven(launchRequest.profile);
|
|
141
|
-
if (preserveProfileOwner && detachedLaunchUnproven) {
|
|
142
|
-
throw new Error('Extension validation cannot release an unproven detached browser launch.');
|
|
143
|
-
}
|
|
144
|
-
if (!detachedLaunchUnproven) {
|
|
145
|
-
clearValidationPortQuarantine(port, launchRequest.profile, launchLease);
|
|
146
|
-
ownsPortLease = false;
|
|
147
|
-
}
|
|
148
|
-
} catch (error) {
|
|
149
|
-
finalError = quarantineProfile(error);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
await closeSocket();
|
|
153
|
-
if (finalError) send({ type: 'release-error', message: finalError.message }, () => process.exit(1));
|
|
154
|
-
else process.exit(0);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async function finish(result) {
|
|
158
|
-
if (finishStarted) return;
|
|
159
|
-
finishStarted = true;
|
|
160
|
-
let finalResult = result;
|
|
161
|
-
let launchTreeError;
|
|
162
|
-
try {
|
|
163
|
-
await stopRemainingLaunchTree();
|
|
164
|
-
} catch (error) {
|
|
165
|
-
launchTreeError = quarantineProfile(error);
|
|
166
|
-
}
|
|
167
|
-
if (launchRequest && (launchTreeError || result.exitCode !== 0 || result.timedOut || result.error)) {
|
|
168
|
-
let detachedLaunchUnproven = false;
|
|
169
|
-
try {
|
|
170
|
-
detachedLaunchUnproven = hasDetachedLaunchUnproven(launchRequest.profile);
|
|
171
|
-
} catch (error) {
|
|
172
|
-
launchTreeError ||= quarantineProfile(error);
|
|
173
|
-
}
|
|
174
|
-
try {
|
|
175
|
-
await cleanProfile(detachedLaunchUnproven || result.timedOut || result.error || launchTreeError ? 2_000 : 0);
|
|
176
|
-
} catch (error) {
|
|
177
|
-
launchTreeError ||= quarantineProfile(error);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
if (launchTreeError) {
|
|
181
|
-
finalResult = { type: 'result', exitCode: null, timedOut: result.timedOut, error: launchTreeError.message };
|
|
182
|
-
}
|
|
183
|
-
launchFinished = true;
|
|
184
|
-
send(finalResult);
|
|
185
|
-
if (releaseRequest !== undefined || !process.connected) void finalize(releaseRequest === true);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function startLaunch(message) {
|
|
189
|
-
if (
|
|
190
|
-
launchChild ||
|
|
191
|
-
!message ||
|
|
192
|
-
typeof message.command !== 'string' ||
|
|
193
|
-
!Array.isArray(message.args) ||
|
|
194
|
-
typeof message.profile !== 'string'
|
|
195
|
-
) {
|
|
196
|
-
send({ type: 'error', message: 'Invalid extension validation supervisor launch request.' }, () => process.exit(2));
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
const timeoutMs = Number(message.timeoutMs);
|
|
200
|
-
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
201
|
-
send({ type: 'error', message: 'Invalid extension validation supervisor timeout.' }, () => process.exit(2));
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
launchRequest = message;
|
|
205
|
-
const lease = createRuntimeIdentityNonce();
|
|
206
|
-
launchLease = lease;
|
|
207
|
-
try {
|
|
208
|
-
markValidationPortLaunchUnproven(port, message.profile, lease);
|
|
209
|
-
ownsPortLease = true;
|
|
210
|
-
} catch (error) {
|
|
211
|
-
void finish({ type: 'result', exitCode: null, timedOut: false, error: error.message });
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
launchChild = spawn(message.command, message.args, {
|
|
215
|
-
cwd: message.cwd,
|
|
216
|
-
env: {
|
|
217
|
-
...message.env,
|
|
218
|
-
MM_HARNESS_VALIDATION_PORT_LEASE: lease,
|
|
219
|
-
},
|
|
220
|
-
detached: process.platform !== 'win32',
|
|
221
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
222
|
-
});
|
|
223
|
-
launchChild.stdout.pipe(process.stdout);
|
|
224
|
-
launchChild.stderr.pipe(process.stderr);
|
|
225
|
-
let timedOut = false;
|
|
226
|
-
let killTimer;
|
|
227
|
-
const timeout = setTimeout(() => {
|
|
228
|
-
timedOut = true;
|
|
229
|
-
try {
|
|
230
|
-
signalLaunchTree('SIGTERM');
|
|
231
|
-
} catch (error) {
|
|
232
|
-
void finish({ type: 'result', exitCode: null, timedOut, error: error.message });
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
killTimer = setTimeout(() => {
|
|
236
|
-
try {
|
|
237
|
-
signalLaunchTree('SIGKILL');
|
|
238
|
-
} catch (error) {
|
|
239
|
-
void finish({ type: 'result', exitCode: null, timedOut, error: error.message });
|
|
240
|
-
}
|
|
241
|
-
}, 1_000);
|
|
242
|
-
killTimer.unref();
|
|
243
|
-
}, timeoutMs);
|
|
244
|
-
launchChild.once('error', (error) => {
|
|
245
|
-
clearTimeout(timeout);
|
|
246
|
-
if (killTimer) clearTimeout(killTimer);
|
|
247
|
-
void finish({ type: 'result', exitCode: null, timedOut, error: error.message });
|
|
248
|
-
});
|
|
249
|
-
launchChild.once('close', (exitCode) => {
|
|
250
|
-
clearTimeout(timeout);
|
|
251
|
-
if (killTimer) clearTimeout(killTimer);
|
|
252
|
-
void finish({ type: 'result', exitCode, timedOut });
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
process.on('message', (message) => {
|
|
257
|
-
if (message?.type === 'run') startLaunch(message);
|
|
258
|
-
if (message?.type === 'release') {
|
|
259
|
-
releaseRequest = message.preserveProfileOwner === true;
|
|
260
|
-
if (launchFinished) void finalize(releaseRequest);
|
|
261
|
-
}
|
|
262
|
-
if (message?.type === 'cancel' && !launchChild) {
|
|
263
|
-
releaseRequest = false;
|
|
264
|
-
void finish({ type: 'cancelled', timedOut: false });
|
|
265
|
-
}
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
process.on('disconnect', () => {
|
|
269
|
-
releaseRequest = false;
|
|
270
|
-
if (!launchChild) {
|
|
271
|
-
void finish({ type: 'cancelled', timedOut: false });
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
launchChild.stdout.unpipe(process.stdout);
|
|
275
|
-
launchChild.stderr.unpipe(process.stderr);
|
|
276
|
-
launchChild.stdout.resume();
|
|
277
|
-
launchChild.stderr.resume();
|
|
278
|
-
if (launchFinished) void finalize(false);
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
socket.once('error', (error) => {
|
|
282
|
-
const message = error.code === 'EADDRINUSE'
|
|
283
|
-
? `Extension validation runtime is already preparing on port ${port}.`
|
|
284
|
-
: error.message;
|
|
285
|
-
send({ type: 'error', message }, () => process.exit(1));
|
|
286
|
-
});
|
|
287
|
-
const existingQuarantine = readValidationPortQuarantine(port);
|
|
288
|
-
if (existingQuarantine) {
|
|
289
|
-
send({ type: 'error', message: validationLaunchQuarantineError(port, existingQuarantine.profile).message }, () => process.exit(1));
|
|
290
|
-
} else {
|
|
291
|
-
socket.bind({ address: '127.0.0.1', port, exclusive: true }, () => send({ type: 'ready' }));
|
|
292
|
-
}
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { execFileSync, spawnSync } = require('node:child_process');
|
|
4
|
-
|
|
5
|
-
function commandHasExactProfile(command, profile) {
|
|
6
|
-
const expected = `--user-data-dir=${profile}`;
|
|
7
|
-
let offset = command.indexOf(expected);
|
|
8
|
-
while (offset !== -1) {
|
|
9
|
-
const before = offset === 0 ? '' : command[offset - 1];
|
|
10
|
-
const after = command[offset + expected.length] || '';
|
|
11
|
-
if ((!before || /\s/u.test(before)) && (!after || /\s/u.test(after))) return true;
|
|
12
|
-
offset = command.indexOf(expected, offset + 1);
|
|
13
|
-
}
|
|
14
|
-
return false;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function profileProcessPids(profile) {
|
|
18
|
-
const output = execFileSync('ps', ['-ww', '-axo', 'pid=,command='], {
|
|
19
|
-
encoding: 'utf8',
|
|
20
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
21
|
-
});
|
|
22
|
-
const pids = [];
|
|
23
|
-
for (const line of output.split('\n')) {
|
|
24
|
-
const match = line.match(/^\s*(\d+)\s+(.*)$/u);
|
|
25
|
-
if (!match || !commandHasExactProfile(match[2], profile)) continue;
|
|
26
|
-
const pid = Number(match[1]);
|
|
27
|
-
if (pid !== process.pid && pid !== process.ppid) pids.push(pid);
|
|
28
|
-
}
|
|
29
|
-
return [...new Set(pids)];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function signalPids(pids, signal) {
|
|
33
|
-
for (const pid of pids) {
|
|
34
|
-
try {
|
|
35
|
-
process.kill(pid, signal);
|
|
36
|
-
} catch (error) {
|
|
37
|
-
if (error.code !== 'ESRCH') throw error;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function delay(milliseconds) {
|
|
43
|
-
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// A process that already exited but has not been reaped yet (zombie) still
|
|
47
|
-
// answers `kill(pid, 0)`. The synchronous stop loop below never yields to the
|
|
48
|
-
// event loop, so a spawned child that dies immediately stays a zombie until the
|
|
49
|
-
// launcher returns; it must not count as a surviving browser.
|
|
50
|
-
function pidAlive(pid) {
|
|
51
|
-
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
52
|
-
try {
|
|
53
|
-
process.kill(pid, 0);
|
|
54
|
-
} catch (error) {
|
|
55
|
-
if (error.code === 'ESRCH') return false;
|
|
56
|
-
throw error;
|
|
57
|
-
}
|
|
58
|
-
// Requires macOS or procps ps; a ps that rejects these flags prints nothing, which is treated as dead.
|
|
59
|
-
const state = spawnSync('ps', ['-o', 'stat=', '-p', String(pid)], {
|
|
60
|
-
encoding: 'utf8',
|
|
61
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
62
|
-
}).stdout.trim();
|
|
63
|
-
return state !== '' && !state.startsWith('Z');
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function uniquePids(pids) {
|
|
67
|
-
return [...new Set(pids)].filter((pid) => Number.isInteger(pid) && pid > 0 && pid !== process.pid);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Reap every live process that still owns this profile, then refuse to continue
|
|
71
|
-
// if any survive. `open -n` otherwise starts a second headed Chrome on the same
|
|
72
|
-
// user-data-dir while the previous instance is still dying.
|
|
73
|
-
function stopProfileProcessesSync(profile, { extraPids = [], timeoutMs = 5_000, waitForAppearanceMs = 0 } = {}) {
|
|
74
|
-
const deadline = Date.now() + timeoutMs;
|
|
75
|
-
const appearUntil = Date.now() + Math.max(0, waitForAppearanceMs);
|
|
76
|
-
let ownersSince = 0;
|
|
77
|
-
while (Date.now() < deadline) {
|
|
78
|
-
const pids = uniquePids([
|
|
79
|
-
...profileProcessPids(profile),
|
|
80
|
-
...extraPids.filter(pidAlive),
|
|
81
|
-
]);
|
|
82
|
-
if (pids.length === 0) {
|
|
83
|
-
if (Date.now() >= appearUntil) return;
|
|
84
|
-
} else {
|
|
85
|
-
if (ownersSince === 0) ownersSince = Date.now();
|
|
86
|
-
signalPids(pids, Date.now() - ownersSince >= 500 ? 'SIGKILL' : 'SIGTERM');
|
|
87
|
-
}
|
|
88
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
|
|
89
|
-
}
|
|
90
|
-
const remaining = uniquePids([
|
|
91
|
-
...profileProcessPids(profile),
|
|
92
|
-
...extraPids.filter(pidAlive),
|
|
93
|
-
]);
|
|
94
|
-
if (remaining.length > 0) {
|
|
95
|
-
throw new Error(
|
|
96
|
-
`Owned Chrome processes survived stop (pid ${remaining.join(', ')}). ` +
|
|
97
|
-
'Next: mm-harness stop --adapter extension --target <checkout>',
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async function stopProfileProcesses(profile, { quietMs = 0, timeoutMs = 5_000 } = {}) {
|
|
103
|
-
const deadline = Date.now() + timeoutMs;
|
|
104
|
-
let quietSince = Date.now();
|
|
105
|
-
let ownersSince = 0;
|
|
106
|
-
while (Date.now() < deadline) {
|
|
107
|
-
const pids = profileProcessPids(profile);
|
|
108
|
-
if (pids.length === 0) {
|
|
109
|
-
ownersSince = 0;
|
|
110
|
-
if (Date.now() - quietSince >= quietMs) return;
|
|
111
|
-
} else {
|
|
112
|
-
if (ownersSince === 0) ownersSince = Date.now();
|
|
113
|
-
quietSince = Date.now();
|
|
114
|
-
signalPids(pids, Date.now() - ownersSince >= 500 ? 'SIGKILL' : 'SIGTERM');
|
|
115
|
-
}
|
|
116
|
-
await delay(50);
|
|
117
|
-
}
|
|
118
|
-
const remaining = profileProcessPids(profile);
|
|
119
|
-
if (remaining.length > 0) {
|
|
120
|
-
throw new Error(`Extension validation profile processes survived cleanup: ${remaining.join(', ')}.`);
|
|
121
|
-
}
|
|
122
|
-
throw new Error(`Extension validation profile did not remain quiescent for ${quietMs}ms.`);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
module.exports = { profileProcessPids, stopProfileProcesses, stopProfileProcessesSync };
|