@deeeed/metamask-harness 0.50.4 → 0.50.6
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 +16 -0
- package/adapters/extension/ensure-browser.sh +18 -5
- package/adapters/extension/launch-browser.cjs +139 -59
- package/adapters/extension/lib/macos-focus.cjs +177 -12
- package/adapters/extension/lib/validation-process-ownership.cjs +49 -2
- package/adapters/extension/live.sh +0 -2
- package/adapters/extension/stop-viewers.sh +1 -2
- package/dist/adapters/extension/validation-process-ownership.js +3 -2
- package/dist/adapters/slot-ports.js +59 -13
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.50.6 - 2026-09-11
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Stop every Extension Chrome whose `--user-data-dir` is under this checkout's `temp/recipe/runtime/`, including uniquely named recipe profiles. The previous allow-list of `chrome-profile`, `chrome-profile-recipe`, and `chrome-profile-pw` left leftovers that `mm-harness stop` reported as already stopped. Nested `console-tail.mjs` log paths under that runtime dir are reaped the same way. Personal Chrome and other checkouts stay untouched.
|
|
10
|
+
- Close extra MetaMask home tabs after an Extension browser launch. `open -n` with a start URL against a still-live profile was stacking unlock tabs.
|
|
11
|
+
- Reap every Chrome process that still owns the Extension profile before relaunch, and kill a macOS `open -n` browser that never exposes CDP, so repeated launches cannot stack headed Chrome instances on the same user-data-dir.
|
|
12
|
+
- Keep the Extension Chrome window visible, but restore the previously focused macOS app through Launch Services while Chrome starts so the window does not come to the front or take the keyboard. System Events hangs during that activation, so osascript restore cannot hold focus.
|
|
13
|
+
|
|
14
|
+
## 0.50.5 - 2026-09-10
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Avoid disabling and re-enabling the Extension after a fresh browser launch, which could cause `No SW` storage errors in reused wallet profiles. Keep build handoff for updates in an already-running browser.
|
|
19
|
+
- Keep the previously focused macOS app in front while Chrome starts, instead of restoring only after CDP is up. Do not let caffeinate declare user activity during that launch.
|
|
20
|
+
|
|
5
21
|
## 0.50.4 - 2026-09-10
|
|
6
22
|
|
|
7
23
|
- Distinguish Extension startup/provider absence from attempted RPC failures and preserve diagnosed failed tabs during verification.
|
|
@@ -301,7 +301,8 @@ const {
|
|
|
301
301
|
runtimeIdentityArgs,
|
|
302
302
|
writeRuntimeIdentity,
|
|
303
303
|
} = require(path.join('${SCRIPT_DIR}', 'lib/chrome-args.cjs'));
|
|
304
|
-
const {
|
|
304
|
+
const { captureMacFrontmost, macBackgroundOpenArgs, preserveMacFrontmost, startMacFocusHold, stopMacFocusHold } = require(path.join('${SCRIPT_DIR}', 'lib/macos-focus.cjs'));
|
|
305
|
+
const { stopProfileProcessesSync } = require(path.join('${SCRIPT_DIR}', 'lib/validation-process-ownership.cjs'));
|
|
305
306
|
|
|
306
307
|
const SLOT_ID = '${SLOT_ID}';
|
|
307
308
|
const AGENT_DIR = '${AGENT_DIR}';
|
|
@@ -322,6 +323,13 @@ const resumeWebpack = () => {
|
|
|
322
323
|
if (WEBPACK_PID) { try { process.kill(-Number(WEBPACK_PID), 'SIGCONT'); } catch { try { process.kill(Number(WEBPACK_PID), 'SIGCONT'); } catch {} } }
|
|
323
324
|
};
|
|
324
325
|
|
|
326
|
+
const previousFrontmost = captureMacFrontmost();
|
|
327
|
+
let focusHold = null;
|
|
328
|
+
const releaseFocusHold = () => {
|
|
329
|
+
stopMacFocusHold(focusHold);
|
|
330
|
+
preserveMacFrontmost(previousFrontmost);
|
|
331
|
+
};
|
|
332
|
+
|
|
325
333
|
(async () => {
|
|
326
334
|
const runtimeNonce = createRuntimeIdentityNonce();
|
|
327
335
|
const startedAt = Date.now();
|
|
@@ -344,8 +352,8 @@ const resumeWebpack = () => {
|
|
|
344
352
|
}
|
|
345
353
|
|
|
346
354
|
const chromiumApp = path.dirname(path.dirname(path.dirname(chromium.executablePath())));
|
|
347
|
-
|
|
348
|
-
execFileSync('open',
|
|
355
|
+
focusHold = startMacFocusHold(previousFrontmost);
|
|
356
|
+
execFileSync('open', macBackgroundOpenArgs(chromiumApp, args), { stdio: 'ignore' });
|
|
349
357
|
for (let i = 0; i < 60; i++) {
|
|
350
358
|
try {
|
|
351
359
|
const version = await new Promise((resolve, reject) => {
|
|
@@ -396,6 +404,7 @@ const resumeWebpack = () => {
|
|
|
396
404
|
} catch {}
|
|
397
405
|
}
|
|
398
406
|
if (!browserPid) {
|
|
407
|
+
stopProfileProcessesSync(PROFILE, { waitForAppearanceMs: 400 });
|
|
399
408
|
throw new Error(
|
|
400
409
|
'Reopened Chrome did not expose an owned CDP listener on port ' + CDP_PORT +
|
|
401
410
|
'. Next: inspect the slot browser process, then rerun: mm-harness launch --adapter extension --build'
|
|
@@ -428,11 +437,12 @@ const resumeWebpack = () => {
|
|
|
428
437
|
try {
|
|
429
438
|
extId = execFileSync(RUNNER_BIN, ['resolve-extension', '--adapter', 'extension', '--target', REPO], { encoding: 'utf8' }).trim();
|
|
430
439
|
} catch (err) {
|
|
440
|
+
releaseFocusHold();
|
|
431
441
|
resumeWebpack();
|
|
432
442
|
console.error('[FAIL] runner resolve-extension failed: ' + (err && err.message ? err.message : String(err)));
|
|
433
443
|
process.exit(1);
|
|
434
444
|
}
|
|
435
|
-
if (!/^[a-p]{32}$/.test(extId)) { resumeWebpack(); console.error('[FAIL] runner returned invalid extension id: ' + JSON.stringify(extId)); process.exit(1); }
|
|
445
|
+
if (!/^[a-p]{32}$/.test(extId)) { releaseFocusHold(); resumeWebpack(); console.error('[FAIL] runner returned invalid extension id: ' + JSON.stringify(extId)); process.exit(1); }
|
|
436
446
|
console.log('[reopen] Extension: ' + extId + ' (via runner resolve-extension)');
|
|
437
447
|
|
|
438
448
|
// Navigate to MetaMask home
|
|
@@ -498,8 +508,10 @@ const resumeWebpack = () => {
|
|
|
498
508
|
if (process.env.MM_HARNESS_FOCUS_BROWSER === '1') {
|
|
499
509
|
await page.bringToFront().catch(() => {});
|
|
500
510
|
} else {
|
|
501
|
-
|
|
511
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
512
|
+
preserveMacFrontmost(previousFrontmost);
|
|
502
513
|
}
|
|
514
|
+
releaseFocusHold();
|
|
503
515
|
|
|
504
516
|
fs.writeFileSync(path.join(AGENT_DIR, 'extension.id'), extId);
|
|
505
517
|
console.log('[reopen] Ready \u2014 ' + SLOT_ID + (CDP_PORT ? ' CDP:' + CDP_PORT : ''));
|
|
@@ -509,6 +521,7 @@ const resumeWebpack = () => {
|
|
|
509
521
|
if (WEBPACK_PID) console.log('[webpack] Resumed');
|
|
510
522
|
process.exit(0);
|
|
511
523
|
})().catch(e => {
|
|
524
|
+
releaseFocusHold();
|
|
512
525
|
resumeWebpack();
|
|
513
526
|
try { removeRuntimeIdentity(AGENT_DIR); } catch {}
|
|
514
527
|
try { fs.unlinkSync(path.join(AGENT_DIR, 'browser.pid')); } catch {}
|
|
@@ -24,7 +24,7 @@ const os = require('node:os');
|
|
|
24
24
|
const path = require('node:path');
|
|
25
25
|
const { execFileSync, spawn, spawnSync } = require('node:child_process');
|
|
26
26
|
const { extensionIdFromExtensionDir } = require('./lib/extension-id.cjs');
|
|
27
|
-
const { profileProcessPids } = require('./lib/validation-process-ownership.cjs');
|
|
27
|
+
const { profileProcessPids, stopProfileProcessesSync } = require('./lib/validation-process-ownership.cjs');
|
|
28
28
|
const { acquireBuildHandoff } = require('./build-handoff.cjs');
|
|
29
29
|
const {
|
|
30
30
|
automationRuntimeArgs,
|
|
@@ -42,7 +42,13 @@ const {
|
|
|
42
42
|
validationLaunchQuarantineError,
|
|
43
43
|
writeRuntimeIdentity,
|
|
44
44
|
} = require('./lib/chrome-args.cjs');
|
|
45
|
-
const {
|
|
45
|
+
const {
|
|
46
|
+
captureMacFrontmost,
|
|
47
|
+
macBackgroundOpenArgs,
|
|
48
|
+
preserveMacFrontmost,
|
|
49
|
+
startMacFocusHold,
|
|
50
|
+
stopMacFocusHold,
|
|
51
|
+
} = require('./lib/macos-focus.cjs');
|
|
46
52
|
|
|
47
53
|
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
48
54
|
console.log(
|
|
@@ -107,7 +113,7 @@ try {
|
|
|
107
113
|
'Next: remove the invalid runtime identity and fix owner permissions, then rerun: mm-harness launch --adapter extension --build',
|
|
108
114
|
);
|
|
109
115
|
}
|
|
110
|
-
|
|
116
|
+
stopProfileProcessesSync(args.profile, { extraPids: [...ownedPids] });
|
|
111
117
|
if (args['reset-profile'] !== undefined) {
|
|
112
118
|
fs.rmSync(args.profile, { recursive: true, force: true });
|
|
113
119
|
fs.mkdirSync(args.profile, { recursive: true });
|
|
@@ -164,70 +170,83 @@ const chromeArgs = [
|
|
|
164
170
|
initialUrl,
|
|
165
171
|
];
|
|
166
172
|
const logFd = fs.openSync(args['chrome-log'], 'a');
|
|
167
|
-
const
|
|
173
|
+
const previousFrontmost = captureMacFrontmost();
|
|
174
|
+
const focusHold = startMacFocusHold(previousFrontmost);
|
|
168
175
|
let browserPid;
|
|
169
176
|
try {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
177
|
+
try {
|
|
178
|
+
const application = process.platform === 'darwin' ? macApplicationForExecutable(args['chrome-bin']) : null;
|
|
179
|
+
if (application) {
|
|
180
|
+
beginDetachedLaunch(cdpPort, args.profile, activeValidationLease);
|
|
181
|
+
execFileSync('open', macBackgroundOpenArgs(application, chromeArgs), {
|
|
182
|
+
env: sanitizedChildEnv(),
|
|
183
|
+
stdio: ['ignore', logFd, logFd],
|
|
184
|
+
});
|
|
185
|
+
browserPid = waitForOwnedCdpPid(cdpPort, args.profile, args['extension-dir']);
|
|
186
|
+
if (browserPid === null) {
|
|
187
|
+
stopProfileProcessesSync(args.profile, { waitForAppearanceMs: 400 });
|
|
188
|
+
throw new Error(
|
|
189
|
+
`Chrome launched but did not expose an owned CDP listener on 127.0.0.1:${cdpPort}. ` +
|
|
190
|
+
`Next: inspect ${args['chrome-log']}, then rerun: mm-harness launch --adapter extension --build`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
beginDetachedLaunch(cdpPort, args.profile, activeValidationLease);
|
|
195
|
+
const child = spawn(args['chrome-bin'], chromeArgs, {
|
|
196
|
+
detached: true,
|
|
197
|
+
env: sanitizedChildEnv(),
|
|
198
|
+
stdio: ['ignore', logFd, logFd],
|
|
199
|
+
});
|
|
200
|
+
child.unref();
|
|
201
|
+
browserPid = waitForOwnedCdpPid(cdpPort, args.profile, args['extension-dir']);
|
|
202
|
+
if (browserPid === null) {
|
|
203
|
+
stopProfileProcessesSync(args.profile, { extraPids: child.pid ? [child.pid] : [] });
|
|
204
|
+
throw new Error(
|
|
205
|
+
`Chrome launched but did not expose an owned CDP listener on 127.0.0.1:${cdpPort}. ` +
|
|
206
|
+
`Next: inspect ${args['chrome-log']}, then rerun: mm-harness launch --adapter extension --build`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
pruneExtraHomeTabs(cdpPort, args['extension-dir']);
|
|
211
|
+
} finally {
|
|
212
|
+
fs.closeSync(logFd);
|
|
213
|
+
}
|
|
214
|
+
fs.writeFileSync(args['chrome-pid'], `${browserPid}\n`);
|
|
215
|
+
try {
|
|
216
|
+
writeRuntimeIdentity(runtimeDir, {
|
|
217
|
+
port: cdpPort,
|
|
218
|
+
pid: browserPid,
|
|
219
|
+
startedAt,
|
|
220
|
+
nonce: runtimeNonce,
|
|
176
221
|
});
|
|
177
|
-
|
|
178
|
-
if (
|
|
179
|
-
|
|
222
|
+
clearDetachedLaunchUnproven(args.profile);
|
|
223
|
+
if (!activeValidationLease) {
|
|
224
|
+
clearValidationPortQuarantine(cdpPort, args.profile);
|
|
180
225
|
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
|
|
226
|
+
} catch (error) {
|
|
227
|
+
terminatePids([browserPid]);
|
|
228
|
+
fs.rmSync(args['chrome-pid'], { force: true });
|
|
229
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
230
|
+
throw new Error(
|
|
231
|
+
`Failed to persist Extension runtime identity under ${runtimeDir}: ${detail}. ` +
|
|
232
|
+
'Next: fix owner write permissions for the runtime directory, then rerun: mm-harness launch --adapter extension --build',
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
if (process.platform === 'darwin') {
|
|
236
|
+
const wake = spawn('caffeinate', ['-dims', '-w', String(browserPid)], {
|
|
184
237
|
detached: true,
|
|
185
|
-
|
|
186
|
-
stdio: ['ignore', logFd, logFd],
|
|
238
|
+
stdio: 'ignore',
|
|
187
239
|
});
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
`Next: inspect ${args['chrome-log']}, then rerun: mm-harness launch --adapter extension --build`,
|
|
195
|
-
);
|
|
196
|
-
}
|
|
240
|
+
wake.unref();
|
|
241
|
+
// Chrome often activates when the first window paints, which can be after CDP
|
|
242
|
+
// is already listening. Keep the previous app frontmost through that paint.
|
|
243
|
+
const settleMs = Number(process.env.MM_HARNESS_FOCUS_SETTLE_MS);
|
|
244
|
+
const waitSec = Number.isFinite(settleMs) && settleMs >= 0 ? settleMs / 1000 : 1;
|
|
245
|
+
if (waitSec > 0) spawnSync('sleep', [String(waitSec)]);
|
|
197
246
|
}
|
|
198
247
|
} finally {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
fs.writeFileSync(args['chrome-pid'], `${browserPid}\n`);
|
|
202
|
-
try {
|
|
203
|
-
writeRuntimeIdentity(runtimeDir, {
|
|
204
|
-
port: cdpPort,
|
|
205
|
-
pid: browserPid,
|
|
206
|
-
startedAt,
|
|
207
|
-
nonce: runtimeNonce,
|
|
208
|
-
});
|
|
209
|
-
clearDetachedLaunchUnproven(args.profile);
|
|
210
|
-
if (!activeValidationLease) {
|
|
211
|
-
clearValidationPortQuarantine(cdpPort, args.profile);
|
|
212
|
-
}
|
|
213
|
-
} catch (error) {
|
|
214
|
-
terminatePids([browserPid]);
|
|
215
|
-
fs.rmSync(args['chrome-pid'], { force: true });
|
|
216
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
217
|
-
throw new Error(
|
|
218
|
-
`Failed to persist Extension runtime identity under ${runtimeDir}: ${detail}. ` +
|
|
219
|
-
'Next: fix owner write permissions for the runtime directory, then rerun: mm-harness launch --adapter extension --build',
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
if (process.platform === 'darwin') {
|
|
223
|
-
const wake = spawn('caffeinate', ['-dimsu', '-w', String(browserPid)], {
|
|
224
|
-
detached: true,
|
|
225
|
-
stdio: 'ignore',
|
|
226
|
-
});
|
|
227
|
-
wake.unref();
|
|
228
|
-
}
|
|
229
|
-
if (process.env.MM_HARNESS_FOCUS_BROWSER !== '1') {
|
|
230
|
-
restoreMacFrontmostProcess(previousFrontmostPid);
|
|
248
|
+
stopMacFocusHold(focusHold);
|
|
249
|
+
preserveMacFrontmost(previousFrontmost);
|
|
231
250
|
}
|
|
232
251
|
|
|
233
252
|
function sanitizedChildEnv() {
|
|
@@ -426,3 +445,64 @@ function extensionHomeUrl(extensionDir) {
|
|
|
426
445
|
const id = extensionIdFromExtensionDir(extensionDir);
|
|
427
446
|
return id ? `chrome-extension://${id}/home.html` : '';
|
|
428
447
|
}
|
|
448
|
+
|
|
449
|
+
function cdpHttp(port, pathname) {
|
|
450
|
+
const result = spawnSync(process.execPath, ['-e', `
|
|
451
|
+
const http = require('http');
|
|
452
|
+
http.get('http://127.0.0.1:${Number(port)}${pathname}', (res) => {
|
|
453
|
+
let body = '';
|
|
454
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
455
|
+
res.on('end', () => process.stdout.write(body));
|
|
456
|
+
}).on('error', () => process.exit(1));
|
|
457
|
+
`], { encoding: 'utf8', timeout: 4000 });
|
|
458
|
+
if (result.status !== 0) return null;
|
|
459
|
+
return result.stdout;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function pruneExtraHomeTabs(port, extensionDir) {
|
|
463
|
+
const id = extensionIdFromExtensionDir(extensionDir);
|
|
464
|
+
const raw = cdpHttp(port, '/json/list');
|
|
465
|
+
if (!id || !raw) return;
|
|
466
|
+
let targets;
|
|
467
|
+
try {
|
|
468
|
+
targets = JSON.parse(raw);
|
|
469
|
+
} catch {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if (!Array.isArray(targets)) return;
|
|
473
|
+
const homes = targets.filter((target) => (
|
|
474
|
+
target &&
|
|
475
|
+
target.type === 'page' &&
|
|
476
|
+
typeof target.id === 'string' &&
|
|
477
|
+
typeof target.url === 'string' &&
|
|
478
|
+
target.url.includes(`chrome-extension://${id}/`) &&
|
|
479
|
+
target.url.includes('/home.html')
|
|
480
|
+
));
|
|
481
|
+
const strays = targets.filter((target) => (
|
|
482
|
+
target &&
|
|
483
|
+
target.type === 'page' &&
|
|
484
|
+
typeof target.id === 'string' &&
|
|
485
|
+
typeof target.url === 'string' &&
|
|
486
|
+
(
|
|
487
|
+
target.url === 'about:blank' ||
|
|
488
|
+
target.url === 'chrome://newtab/' ||
|
|
489
|
+
target.url.startsWith('chrome://new-tab-page')
|
|
490
|
+
)
|
|
491
|
+
));
|
|
492
|
+
if (homes.length === 0) return;
|
|
493
|
+
const keeper = homes.find((target) => (
|
|
494
|
+
typeof target.title === 'string' &&
|
|
495
|
+
target.title.trim() !== '' &&
|
|
496
|
+
target.title.trim() !== 'MetaMask'
|
|
497
|
+
)) ?? homes[0];
|
|
498
|
+
if (homes.length > 1) {
|
|
499
|
+
for (const extra of homes) {
|
|
500
|
+
if (keeper && extra.id === keeper.id) continue;
|
|
501
|
+
cdpHttp(port, `/json/close/${extra.id}`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
for (const stray of strays) {
|
|
505
|
+
if (keeper && stray.id === keeper.id) continue;
|
|
506
|
+
cdpHttp(port, `/json/close/${stray.id}`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
@@ -1,32 +1,197 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { execFileSync } = require('node:child_process');
|
|
3
|
+
const { execFileSync, spawn } = require('node:child_process');
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
const HOLD_INTERVAL_MS = 20;
|
|
6
|
+
const toolEnv = () => ({ ...process.env });
|
|
7
|
+
|
|
8
|
+
function captureMacFrontmost() {
|
|
6
9
|
if (process.platform !== 'darwin') return null;
|
|
7
10
|
try {
|
|
8
|
-
const
|
|
9
|
-
'
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
const asn = execFileSync('lsappinfo', ['front'], {
|
|
12
|
+
encoding: 'utf8',
|
|
13
|
+
env: toolEnv(),
|
|
14
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
15
|
+
timeout: 200,
|
|
16
|
+
}).trim();
|
|
17
|
+
if (!asn) return null;
|
|
18
|
+
return parseLsappinfo(execFileSync('lsappinfo', ['info', asn], {
|
|
19
|
+
encoding: 'utf8',
|
|
20
|
+
env: toolEnv(),
|
|
21
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
22
|
+
timeout: 200,
|
|
23
|
+
}));
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function captureMacFrontmostProcess() {
|
|
30
|
+
return captureMacFrontmost()?.pid ?? null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseLsappinfo(info) {
|
|
34
|
+
const pid = Number.parseInt((info.match(/\bpid\s*=\s*(\d+)/u) || info.match(/"pid"=(\d+)/u) || [])[1], 10);
|
|
35
|
+
const bundlePath = (info.match(/bundle path="([^"]+)"/u) || info.match(/"LSBundlePath"="([^"]+)"/u) || [])[1];
|
|
36
|
+
const name = (info.match(/^"([^"]+)"/mu) || info.match(/"LSDisplayName"="([^"]+)"/u) || [])[1];
|
|
37
|
+
if (!Number.isInteger(pid) || pid <= 0 || !bundlePath) return null;
|
|
38
|
+
return { pid, bundlePath, name: name || bundlePath };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function currentFrontmostPid() {
|
|
42
|
+
try {
|
|
43
|
+
const asn = execFileSync('lsappinfo', ['front'], {
|
|
44
|
+
encoding: 'utf8',
|
|
45
|
+
env: toolEnv(),
|
|
46
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
47
|
+
timeout: 200,
|
|
48
|
+
}).trim();
|
|
49
|
+
if (!asn) return null;
|
|
50
|
+
const value = execFileSync('lsappinfo', ['info', '-only', 'pid', asn], {
|
|
51
|
+
encoding: 'utf8',
|
|
52
|
+
env: toolEnv(),
|
|
53
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
54
|
+
timeout: 200,
|
|
55
|
+
}).trim();
|
|
56
|
+
const pid = Number.parseInt((value.match(/"pid"=(\d+)/u) || [])[1], 10);
|
|
13
57
|
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
14
58
|
} catch {
|
|
15
59
|
return null;
|
|
16
60
|
}
|
|
17
61
|
}
|
|
18
62
|
|
|
63
|
+
function restoreMacFrontmost(target) {
|
|
64
|
+
if (process.platform !== 'darwin' || !target?.bundlePath) return false;
|
|
65
|
+
try {
|
|
66
|
+
execFileSync('open', ['-a', target.bundlePath], {
|
|
67
|
+
env: toolEnv(),
|
|
68
|
+
stdio: 'ignore',
|
|
69
|
+
timeout: 500,
|
|
70
|
+
});
|
|
71
|
+
return true;
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
19
77
|
function restoreMacFrontmostProcess(pid) {
|
|
20
78
|
if (process.platform !== 'darwin' || !Number.isInteger(pid) || pid <= 0) return false;
|
|
21
79
|
try {
|
|
22
|
-
execFileSync('
|
|
23
|
-
'
|
|
24
|
-
|
|
25
|
-
|
|
80
|
+
const asn = execFileSync('lsappinfo', ['find', `pid=${pid}`], {
|
|
81
|
+
encoding: 'utf8',
|
|
82
|
+
env: toolEnv(),
|
|
83
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
84
|
+
timeout: 200,
|
|
85
|
+
}).trim();
|
|
86
|
+
if (!asn) return false;
|
|
87
|
+
return restoreMacFrontmost(parseLsappinfo(execFileSync('lsappinfo', ['info', asn], {
|
|
88
|
+
encoding: 'utf8',
|
|
89
|
+
env: toolEnv(),
|
|
90
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
91
|
+
timeout: 200,
|
|
92
|
+
})));
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function preserveMacFrontmost(target) {
|
|
99
|
+
if (process.env.MM_HARNESS_FOCUS_BROWSER === '1') return false;
|
|
100
|
+
if (process.platform !== 'darwin' || !target?.bundlePath || !Number.isInteger(target.pid) || target.pid <= 0) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
const current = currentFrontmostPid();
|
|
104
|
+
if (current === target.pid) return true;
|
|
105
|
+
return restoreMacFrontmost(target);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function preserveMacFrontmostProcess(pid) {
|
|
109
|
+
if (process.env.MM_HARNESS_FOCUS_BROWSER === '1') return false;
|
|
110
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
111
|
+
const current = currentFrontmostPid();
|
|
112
|
+
if (current === pid) return true;
|
|
113
|
+
return restoreMacFrontmostProcess(pid);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function macBackgroundOpenArgs(application, chromeArgs) {
|
|
117
|
+
return ['-g', '-n', '-a', application, '--args', ...chromeArgs];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function startMacFocusHold(targetOrPid) {
|
|
121
|
+
if (process.env.MM_HARNESS_FOCUS_BROWSER === '1') return null;
|
|
122
|
+
if (process.platform !== 'darwin') return null;
|
|
123
|
+
const target = typeof targetOrPid === 'number'
|
|
124
|
+
? { pid: targetOrPid, bundlePath: bundlePathForPid(targetOrPid) }
|
|
125
|
+
: targetOrPid;
|
|
126
|
+
if (!target?.bundlePath || !Number.isInteger(target.pid) || target.pid <= 0) return null;
|
|
127
|
+
return spawn(process.execPath, [
|
|
128
|
+
__filename,
|
|
129
|
+
'--hold',
|
|
130
|
+
String(target.pid),
|
|
131
|
+
target.bundlePath,
|
|
132
|
+
String(process.pid),
|
|
133
|
+
], { stdio: 'ignore', env: toolEnv() });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function bundlePathForPid(pid) {
|
|
137
|
+
try {
|
|
138
|
+
const asn = execFileSync('lsappinfo', ['find', `pid=${pid}`], {
|
|
139
|
+
encoding: 'utf8',
|
|
140
|
+
env: toolEnv(),
|
|
141
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
142
|
+
timeout: 200,
|
|
143
|
+
}).trim();
|
|
144
|
+
return parseLsappinfo(execFileSync('lsappinfo', ['info', asn], {
|
|
145
|
+
encoding: 'utf8',
|
|
146
|
+
env: toolEnv(),
|
|
147
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
148
|
+
timeout: 200,
|
|
149
|
+
}))?.bundlePath ?? null;
|
|
150
|
+
} catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function stopMacFocusHold(child) {
|
|
156
|
+
if (!child || !Number.isInteger(child.pid) || child.pid <= 0) return;
|
|
157
|
+
try {
|
|
158
|
+
process.kill(child.pid, 'SIGTERM');
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (error.code !== 'ESRCH') throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function parentAlive(pid) {
|
|
165
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
166
|
+
try {
|
|
167
|
+
process.kill(pid, 0);
|
|
26
168
|
return true;
|
|
27
169
|
} catch {
|
|
28
170
|
return false;
|
|
29
171
|
}
|
|
30
172
|
}
|
|
31
173
|
|
|
32
|
-
|
|
174
|
+
if (require.main === module && process.argv[2] === '--hold') {
|
|
175
|
+
const pid = Number.parseInt(process.argv[3], 10);
|
|
176
|
+
const bundlePath = process.argv[4];
|
|
177
|
+
const parentPid = Number.parseInt(process.argv[5], 10);
|
|
178
|
+
const target = { pid, bundlePath };
|
|
179
|
+
const hold = () => {
|
|
180
|
+
if (!parentAlive(parentPid)) process.exit(0);
|
|
181
|
+
preserveMacFrontmost(target);
|
|
182
|
+
};
|
|
183
|
+
hold();
|
|
184
|
+
setInterval(hold, HOLD_INTERVAL_MS);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = {
|
|
188
|
+
captureMacFrontmost,
|
|
189
|
+
captureMacFrontmostProcess,
|
|
190
|
+
macBackgroundOpenArgs,
|
|
191
|
+
preserveMacFrontmost,
|
|
192
|
+
preserveMacFrontmostProcess,
|
|
193
|
+
restoreMacFrontmost,
|
|
194
|
+
restoreMacFrontmostProcess,
|
|
195
|
+
startMacFocusHold,
|
|
196
|
+
stopMacFocusHold,
|
|
197
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { execFileSync } = require('node:child_process');
|
|
3
|
+
const { execFileSync, spawnSync } = require('node:child_process');
|
|
4
4
|
|
|
5
5
|
function commandHasExactProfile(command, profile) {
|
|
6
6
|
const expected = `--user-data-dir=${profile}`;
|
|
@@ -43,6 +43,53 @@ function delay(milliseconds) {
|
|
|
43
43
|
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
function pidAlive(pid) {
|
|
47
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
48
|
+
try {
|
|
49
|
+
process.kill(pid, 0);
|
|
50
|
+
return true;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error.code === 'ESRCH') return false;
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function uniquePids(pids) {
|
|
58
|
+
return [...new Set(pids)].filter((pid) => Number.isInteger(pid) && pid > 0 && pid !== process.pid);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Reap every live process that still owns this profile, then refuse to continue
|
|
62
|
+
// if any survive. `open -n` otherwise starts a second headed Chrome on the same
|
|
63
|
+
// user-data-dir while the previous instance is still dying.
|
|
64
|
+
function stopProfileProcessesSync(profile, { extraPids = [], timeoutMs = 5_000, waitForAppearanceMs = 0 } = {}) {
|
|
65
|
+
const deadline = Date.now() + timeoutMs;
|
|
66
|
+
const appearUntil = Date.now() + Math.max(0, waitForAppearanceMs);
|
|
67
|
+
let ownersSince = 0;
|
|
68
|
+
while (Date.now() < deadline) {
|
|
69
|
+
const pids = uniquePids([
|
|
70
|
+
...profileProcessPids(profile),
|
|
71
|
+
...extraPids.filter(pidAlive),
|
|
72
|
+
]);
|
|
73
|
+
if (pids.length === 0) {
|
|
74
|
+
if (Date.now() >= appearUntil) return;
|
|
75
|
+
} else {
|
|
76
|
+
if (ownersSince === 0) ownersSince = Date.now();
|
|
77
|
+
signalPids(pids, Date.now() - ownersSince >= 500 ? 'SIGKILL' : 'SIGTERM');
|
|
78
|
+
}
|
|
79
|
+
spawnSync('sleep', ['0.1']);
|
|
80
|
+
}
|
|
81
|
+
const remaining = uniquePids([
|
|
82
|
+
...profileProcessPids(profile),
|
|
83
|
+
...extraPids.filter(pidAlive),
|
|
84
|
+
]);
|
|
85
|
+
if (remaining.length > 0) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Owned Chrome processes survived stop (pid ${remaining.join(', ')}). ` +
|
|
88
|
+
'Next: mm-harness stop --adapter extension --target <checkout>',
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
46
93
|
async function stopProfileProcesses(profile, { quietMs = 0, timeoutMs = 5_000 } = {}) {
|
|
47
94
|
const deadline = Date.now() + timeoutMs;
|
|
48
95
|
let quietSince = Date.now();
|
|
@@ -66,4 +113,4 @@ async function stopProfileProcesses(profile, { quietMs = 0, timeoutMs = 5_000 }
|
|
|
66
113
|
throw new Error(`Extension validation profile did not remain quiescent for ${quietMs}ms.`);
|
|
67
114
|
}
|
|
68
115
|
|
|
69
|
-
module.exports = { profileProcessPids, stopProfileProcesses };
|
|
116
|
+
module.exports = { profileProcessPids, stopProfileProcesses, stopProfileProcessesSync };
|
|
@@ -228,7 +228,6 @@ NODE
|
|
|
228
228
|
quoted_check_infura="$(printf '%q' "$SCRIPT_DIR/check-infura-readiness.cjs")"
|
|
229
229
|
quoted_stamp_title="$(printf '%q' "$SCRIPT_DIR/stamp-runtime-title.cjs")"
|
|
230
230
|
quoted_chrome_launcher="$(printf '%q' "$SCRIPT_DIR/launch-browser.cjs")"
|
|
231
|
-
quoted_build_handoff="$(printf '%q' "$SCRIPT_DIR/build-handoff.cjs")"
|
|
232
231
|
quoted_artifact_state="$(printf '%q' "$SCRIPT_DIR/artifact-runtime-state.cjs")"
|
|
233
232
|
quoted_fixture_state="$(printf '%q' "$FIXTURE_STATE_ABS")"
|
|
234
233
|
quoted_fixture_validation="$(printf '%q' "$FIXTURE_VALIDATION_ABS")"
|
|
@@ -353,7 +352,6 @@ NODE
|
|
|
353
352
|
fi
|
|
354
353
|
prepare_parts+=("$chrome_launch_cmd")
|
|
355
354
|
prepare_parts+=("for i in {1..60}; do curl -fsS --max-time 1 http://127.0.0.1:${CDP_PORT}/json/version >/dev/null 2>&1 && break; sleep 1; done; curl -fsS --max-time 1 http://127.0.0.1:${CDP_PORT}/json/version >/dev/null")
|
|
356
|
-
prepare_parts+=("node ${quoted_build_handoff} --target ${quoted_target} --runtime-dist ${quoted_runtime_dist} --cdp-port ${CDP_PORT}")
|
|
357
355
|
if [ -n "$WALLET_FIXTURE_ABS" ]; then
|
|
358
356
|
prepare_parts+=("bash ${quoted_seed_fixture} seed-cdp --target ${quoted_target} --fixture ${quoted_wallet_fixture} --state ${quoted_fixture_state} --cdp-port ${CDP_PORT} --extension-dir ${quoted_runtime_dist} --extension-id-file ${quoted_extension_id_file} --out ${quoted_fixture_validation}")
|
|
359
357
|
fi
|
|
@@ -29,7 +29,6 @@ done
|
|
|
29
29
|
unset _tv_src
|
|
30
30
|
|
|
31
31
|
runtime_dir="$TARGET/$(recipe_runtime_dir)"
|
|
32
|
-
console_log="$runtime_dir/extension-console.log"
|
|
33
32
|
set +e
|
|
34
33
|
|
|
35
34
|
if command -v tmux_viewer_close_marker >/dev/null 2>&1; then
|
|
@@ -48,7 +47,7 @@ ps -axo pid=,command= 2>/dev/null | while read -r pid command; do
|
|
|
48
47
|
*) continue ;;
|
|
49
48
|
esac
|
|
50
49
|
case "$command" in
|
|
51
|
-
*"
|
|
50
|
+
*"$runtime_dir/"*) kill "$pid" 2>/dev/null || true ;;
|
|
52
51
|
esac
|
|
53
52
|
done
|
|
54
53
|
|
|
@@ -3,8 +3,9 @@ const require2 = createRequire(import.meta.url);
|
|
|
3
3
|
const processOwnership = require2(
|
|
4
4
|
"../../../adapters/extension/lib/validation-process-ownership.cjs"
|
|
5
5
|
);
|
|
6
|
-
const { profileProcessPids, stopProfileProcesses } = processOwnership;
|
|
6
|
+
const { profileProcessPids, stopProfileProcesses, stopProfileProcessesSync } = processOwnership;
|
|
7
7
|
export {
|
|
8
8
|
profileProcessPids,
|
|
9
|
-
stopProfileProcesses
|
|
9
|
+
stopProfileProcesses,
|
|
10
|
+
stopProfileProcessesSync
|
|
10
11
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { recipeRuntimeDir } from "../paths.js";
|
|
5
6
|
import {
|
|
@@ -131,11 +132,53 @@ function stopExtensionWatcher(target) {
|
|
|
131
132
|
}
|
|
132
133
|
return ownedPids.length;
|
|
133
134
|
}
|
|
135
|
+
function commandFlagValue(command, flag) {
|
|
136
|
+
const escaped = escapeRegex(flag);
|
|
137
|
+
const patterns = [
|
|
138
|
+
new RegExp(`(?:^|\\s)${escaped}=([^\\s"]+)`, "u"),
|
|
139
|
+
new RegExp(`(?:^|\\s)${escaped}="([^"]+)"`, "u"),
|
|
140
|
+
new RegExp(`(?:^|\\s)${escaped}='([^']+)'`, "u"),
|
|
141
|
+
new RegExp(`(?:^|\\s)${escaped}\\s+([^\\s"]+)`, "u")
|
|
142
|
+
];
|
|
143
|
+
for (const pattern of patterns) {
|
|
144
|
+
const match = pattern.exec(command);
|
|
145
|
+
if (match?.[1]) return match[1];
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
function pathIsInsideDir(candidate, root) {
|
|
150
|
+
const resolved = path.resolve(candidate);
|
|
151
|
+
const base = path.resolve(root);
|
|
152
|
+
return resolved === base || resolved.startsWith(`${base}${path.sep}`);
|
|
153
|
+
}
|
|
154
|
+
function chromeUserDataDirOwnedByCheckout(command, runtimeAbs) {
|
|
155
|
+
const value = commandFlagValue(command, "--user-data-dir");
|
|
156
|
+
if (!value) return false;
|
|
157
|
+
return pathIsInsideDir(value, runtimeAbs);
|
|
158
|
+
}
|
|
159
|
+
function commandLoadsCheckoutExtension(command, targetAbs) {
|
|
160
|
+
const loaded = commandFlagValue(command, "--load-extension") ?? commandFlagValue(command, "--disable-extensions-except");
|
|
161
|
+
if (!loaded) return false;
|
|
162
|
+
return pathIsInsideDir(loaded, targetAbs);
|
|
163
|
+
}
|
|
164
|
+
function isSharedOrDefaultBrowserProfile(profile) {
|
|
165
|
+
const resolved = path.resolve(profile);
|
|
166
|
+
const home = os.homedir();
|
|
167
|
+
const osDefaults = [
|
|
168
|
+
path.join(home, "Library/Application Support/Google/Chrome"),
|
|
169
|
+
path.join(home, "Library/Application Support/Chromium"),
|
|
170
|
+
path.join(home, "Library/Application Support/Microsoft Edge"),
|
|
171
|
+
path.join(home, ".config/google-chrome"),
|
|
172
|
+
path.join(home, ".config/chromium"),
|
|
173
|
+
path.join(home, ".config/microsoft-edge")
|
|
174
|
+
];
|
|
175
|
+
const underDefault = (dir) => resolved === dir || resolved.startsWith(`${dir}${path.sep}`);
|
|
176
|
+
return resolved === home || resolved.includes(`${path.sep}.chrome-farmslot`) || osDefaults.some(underDefault);
|
|
177
|
+
}
|
|
134
178
|
function stopExtensionRuntime(target) {
|
|
135
179
|
const resolved = path.resolve(target);
|
|
136
180
|
const runtimeAbs = path.join(resolved, recipeRuntimeDir());
|
|
137
|
-
const
|
|
138
|
-
const profiles = process.env.CHROME_USER_DATA_DIR ? [path.resolve(configuredProfile)] : [configuredProfile, path.join(runtimeAbs, "chrome-profile-recipe"), path.join(runtimeAbs, "chrome-profile-pw")].map((value) => path.resolve(value));
|
|
181
|
+
const extraProfile = process.env.CHROME_USER_DATA_DIR ? path.resolve(process.env.CHROME_USER_DATA_DIR) : null;
|
|
139
182
|
let signalled = stopExtensionWatcher(resolved);
|
|
140
183
|
const ownedBrowserPids = /* @__PURE__ */ new Set();
|
|
141
184
|
try {
|
|
@@ -144,7 +187,16 @@ function stopExtensionRuntime(target) {
|
|
|
144
187
|
const match = /^\s*(\d+)\s+(.*)$/u.exec(line);
|
|
145
188
|
if (!match) continue;
|
|
146
189
|
const pid = Number(match[1]);
|
|
147
|
-
if (pid
|
|
190
|
+
if (pid === process.pid) continue;
|
|
191
|
+
if (chromeUserDataDirOwnedByCheckout(match[2], runtimeAbs)) {
|
|
192
|
+
ownedBrowserPids.add(pid);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (!extraProfile || pathIsInsideDir(extraProfile, runtimeAbs) || isSharedOrDefaultBrowserProfile(extraProfile)) continue;
|
|
196
|
+
const configured = commandFlagValue(match[2], "--user-data-dir");
|
|
197
|
+
if (configured && path.resolve(configured) === extraProfile && commandLoadsCheckoutExtension(match[2], resolved)) {
|
|
198
|
+
ownedBrowserPids.add(pid);
|
|
199
|
+
}
|
|
148
200
|
}
|
|
149
201
|
} catch {
|
|
150
202
|
}
|
|
@@ -203,22 +255,16 @@ function processAlive(pid) {
|
|
|
203
255
|
return error.code === "EPERM";
|
|
204
256
|
}
|
|
205
257
|
}
|
|
206
|
-
function commandHasExactArg(command, flag, value) {
|
|
207
|
-
const escapedFlag = escapeRegex(flag);
|
|
208
|
-
const escapedValue = escapeRegex(value);
|
|
209
|
-
return [
|
|
210
|
-
new RegExp(`(?:^|\\s)${escapedFlag}=${escapedValue}(?=\\s|$)`, "u"),
|
|
211
|
-
new RegExp(`(?:^|\\s)${escapedFlag}="${escapedValue}"(?=\\s|$)`, "u"),
|
|
212
|
-
new RegExp(`(?:^|\\s)${escapedFlag}='${escapedValue}'(?=\\s|$)`, "u"),
|
|
213
|
-
new RegExp(`(?:^|\\s)${escapedFlag}\\s+${escapedValue}(?=\\s|$)`, "u")
|
|
214
|
-
].some((pattern) => pattern.test(command));
|
|
215
|
-
}
|
|
216
258
|
function escapeRegex(value) {
|
|
217
259
|
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
218
260
|
}
|
|
219
261
|
export {
|
|
220
262
|
applyKVLines,
|
|
263
|
+
chromeUserDataDirOwnedByCheckout,
|
|
264
|
+
commandFlagValue,
|
|
221
265
|
isExtensionWatcherLive,
|
|
266
|
+
isSharedOrDefaultBrowserProfile,
|
|
267
|
+
pathIsInsideDir,
|
|
222
268
|
resolveExtensionSlotPorts,
|
|
223
269
|
resolveMobileSlotPorts,
|
|
224
270
|
stopExtensionRuntime,
|