@deeeed/metamask-harness 0.50.6 → 0.51.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 -11
- package/README.md +31 -0
- package/adapters/extension/ensure-browser.sh +5 -18
- package/adapters/extension/launch-browser.cjs +59 -139
- package/adapters/extension/lib/macos-focus.cjs +12 -177
- package/adapters/extension/lib/validation-process-ownership.cjs +2 -49
- package/adapters/extension/live.sh +2 -0
- package/adapters/extension/stop-viewers.sh +2 -1
- package/dist/adapters/extension/validation-process-ownership.js +2 -3
- package/dist/adapters/slot-ports.js +13 -59
- package/dist/cli-commands.js +2 -1
- package/dist/cli.js +4 -0
- package/dist/command-contract.js +43 -0
- package/dist/commands/checklist.js +9 -17
- package/dist/commands/prepare.js +236 -0
- package/dist/commands/task-init.js +51 -0
- package/dist/doctor.js +3 -4
- package/dist/mm-harness-cli.js +82 -2
- package/dist/paths.js +4 -0
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,21 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
-
## 0.
|
|
5
|
+
## 0.51.0 - 2026-09-13
|
|
6
6
|
|
|
7
|
-
###
|
|
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.
|
|
7
|
+
### Added
|
|
13
8
|
|
|
14
|
-
|
|
9
|
+
- `mm-harness task init` writes the task directory (task document, checklist, mark shim, and inputs) from a shared execution template, with the MetaMask surface, harness-gated mark command, and detected platform applied by default.
|
|
10
|
+
- `mm-harness prepare` runs the readiness chain for a checkout (`doctor --fix`, `status`, launch, fixtures, `verify`), stops at the first failure, and records one readiness record at `<artifacts-dir>/sandbox.json` with a per-step report.
|
|
15
11
|
|
|
16
|
-
###
|
|
12
|
+
### Changed
|
|
17
13
|
|
|
18
|
-
-
|
|
19
|
-
-
|
|
14
|
+
- Pin the getting-started iOS simulator to the checkout runtime file, and stop the setup prompt from auto-creating a disposable wallet.
|
|
15
|
+
- Resolve the checklist a mark targets through the agent runtime: a missing `checklist-target.json` now means the default checklist and signal instead of silently disabling the terminal-signal guard, and an invalid one raises its own error.
|
|
16
|
+
- Require `@farmslot/agent-runtime` 0.9.0 and `@farmslot/protocol` 0.26.0.
|
|
20
17
|
|
|
21
18
|
## 0.50.4 - 2026-09-10
|
|
22
19
|
|
package/README.md
CHANGED
|
@@ -121,6 +121,37 @@ Mobile and Extension recipes can bracket explicit UI-smoothness windows with
|
|
|
121
121
|
`app.performance_assert`. Capture is never automatic. See
|
|
122
122
|
[UI smoothness capture](docs/PERFORMANCE-CAPTURE.md).
|
|
123
123
|
|
|
124
|
+
## Work a task
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
# Pick a checklist from the shared catalog.
|
|
128
|
+
mm-harness execution-template list --package-templates <catalog> --json
|
|
129
|
+
|
|
130
|
+
# Write the task directory the agent works from.
|
|
131
|
+
mm-harness task init temp/tasks/fix/TAT-1 \
|
|
132
|
+
--flow fix-bug --run-mode autonomous --template fix-bug/autonomous.mobile \
|
|
133
|
+
--package-templates <catalog> --title "Wrong total on the receipt" --ticket TAT-1
|
|
134
|
+
|
|
135
|
+
# Bring the checkout to a proven-ready state before the first step.
|
|
136
|
+
mm-harness prepare --artifacts-dir temp/tasks/fix/TAT-1/artifacts --json
|
|
137
|
+
|
|
138
|
+
# Mark progress; the task's own ./mark shim routes here.
|
|
139
|
+
mm-harness checklist mark temp/tasks/fix/TAT-1 start
|
|
140
|
+
mm-harness checklist mark temp/tasks/fix/TAT-1 1
|
|
141
|
+
mm-harness checklist mark temp/tasks/fix/TAT-1 complete --mark-last
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`task init` writes `TASK.md`, `CHECKLIST.md`, the `mark` shim, and `inputs/`.
|
|
145
|
+
`CHECKLIST.md` is the only file whose boxes count as steps. It writes no
|
|
146
|
+
`checklist-target.json`; absent means `CHECKLIST.md` + `SIGNAL.json`.
|
|
147
|
+
|
|
148
|
+
`prepare` runs `doctor --fix`, `status`, `launch --verify`, `fixtures set`, and
|
|
149
|
+
`verify` in order, stops at the first failure, and records one readiness record
|
|
150
|
+
at `<artifacts-dir>/sandbox.json`. Core skips launch and fixtures; Mobile takes
|
|
151
|
+
`ios`/`android` from the checkout's configured target, then from the devices
|
|
152
|
+
`status` reports, and asks when that is ambiguous. Exit 0 means
|
|
153
|
+
ready. See [the task directory](site/task-dir.html).
|
|
154
|
+
|
|
124
155
|
## Discover and prove
|
|
125
156
|
|
|
126
157
|
```bash
|
|
@@ -301,8 +301,7 @@ const {
|
|
|
301
301
|
runtimeIdentityArgs,
|
|
302
302
|
writeRuntimeIdentity,
|
|
303
303
|
} = require(path.join('${SCRIPT_DIR}', 'lib/chrome-args.cjs'));
|
|
304
|
-
const {
|
|
305
|
-
const { stopProfileProcessesSync } = require(path.join('${SCRIPT_DIR}', 'lib/validation-process-ownership.cjs'));
|
|
304
|
+
const { captureMacFrontmostProcess, restoreMacFrontmostProcess } = require(path.join('${SCRIPT_DIR}', 'lib/macos-focus.cjs'));
|
|
306
305
|
|
|
307
306
|
const SLOT_ID = '${SLOT_ID}';
|
|
308
307
|
const AGENT_DIR = '${AGENT_DIR}';
|
|
@@ -323,13 +322,6 @@ const resumeWebpack = () => {
|
|
|
323
322
|
if (WEBPACK_PID) { try { process.kill(-Number(WEBPACK_PID), 'SIGCONT'); } catch { try { process.kill(Number(WEBPACK_PID), 'SIGCONT'); } catch {} } }
|
|
324
323
|
};
|
|
325
324
|
|
|
326
|
-
const previousFrontmost = captureMacFrontmost();
|
|
327
|
-
let focusHold = null;
|
|
328
|
-
const releaseFocusHold = () => {
|
|
329
|
-
stopMacFocusHold(focusHold);
|
|
330
|
-
preserveMacFrontmost(previousFrontmost);
|
|
331
|
-
};
|
|
332
|
-
|
|
333
325
|
(async () => {
|
|
334
326
|
const runtimeNonce = createRuntimeIdentityNonce();
|
|
335
327
|
const startedAt = Date.now();
|
|
@@ -352,8 +344,8 @@ const releaseFocusHold = () => {
|
|
|
352
344
|
}
|
|
353
345
|
|
|
354
346
|
const chromiumApp = path.dirname(path.dirname(path.dirname(chromium.executablePath())));
|
|
355
|
-
|
|
356
|
-
execFileSync('open',
|
|
347
|
+
const previousFrontmostPid = captureMacFrontmostProcess();
|
|
348
|
+
execFileSync('open', ['-g', '-n', '-a', chromiumApp, '--args', ...args], { stdio: 'ignore' });
|
|
357
349
|
for (let i = 0; i < 60; i++) {
|
|
358
350
|
try {
|
|
359
351
|
const version = await new Promise((resolve, reject) => {
|
|
@@ -404,7 +396,6 @@ const releaseFocusHold = () => {
|
|
|
404
396
|
} catch {}
|
|
405
397
|
}
|
|
406
398
|
if (!browserPid) {
|
|
407
|
-
stopProfileProcessesSync(PROFILE, { waitForAppearanceMs: 400 });
|
|
408
399
|
throw new Error(
|
|
409
400
|
'Reopened Chrome did not expose an owned CDP listener on port ' + CDP_PORT +
|
|
410
401
|
'. Next: inspect the slot browser process, then rerun: mm-harness launch --adapter extension --build'
|
|
@@ -437,12 +428,11 @@ const releaseFocusHold = () => {
|
|
|
437
428
|
try {
|
|
438
429
|
extId = execFileSync(RUNNER_BIN, ['resolve-extension', '--adapter', 'extension', '--target', REPO], { encoding: 'utf8' }).trim();
|
|
439
430
|
} catch (err) {
|
|
440
|
-
releaseFocusHold();
|
|
441
431
|
resumeWebpack();
|
|
442
432
|
console.error('[FAIL] runner resolve-extension failed: ' + (err && err.message ? err.message : String(err)));
|
|
443
433
|
process.exit(1);
|
|
444
434
|
}
|
|
445
|
-
if (!/^[a-p]{32}$/.test(extId)) {
|
|
435
|
+
if (!/^[a-p]{32}$/.test(extId)) { resumeWebpack(); console.error('[FAIL] runner returned invalid extension id: ' + JSON.stringify(extId)); process.exit(1); }
|
|
446
436
|
console.log('[reopen] Extension: ' + extId + ' (via runner resolve-extension)');
|
|
447
437
|
|
|
448
438
|
// Navigate to MetaMask home
|
|
@@ -508,10 +498,8 @@ const releaseFocusHold = () => {
|
|
|
508
498
|
if (process.env.MM_HARNESS_FOCUS_BROWSER === '1') {
|
|
509
499
|
await page.bringToFront().catch(() => {});
|
|
510
500
|
} else {
|
|
511
|
-
|
|
512
|
-
preserveMacFrontmost(previousFrontmost);
|
|
501
|
+
restoreMacFrontmostProcess(previousFrontmostPid);
|
|
513
502
|
}
|
|
514
|
-
releaseFocusHold();
|
|
515
503
|
|
|
516
504
|
fs.writeFileSync(path.join(AGENT_DIR, 'extension.id'), extId);
|
|
517
505
|
console.log('[reopen] Ready \u2014 ' + SLOT_ID + (CDP_PORT ? ' CDP:' + CDP_PORT : ''));
|
|
@@ -521,7 +509,6 @@ const releaseFocusHold = () => {
|
|
|
521
509
|
if (WEBPACK_PID) console.log('[webpack] Resumed');
|
|
522
510
|
process.exit(0);
|
|
523
511
|
})().catch(e => {
|
|
524
|
-
releaseFocusHold();
|
|
525
512
|
resumeWebpack();
|
|
526
513
|
try { removeRuntimeIdentity(AGENT_DIR); } catch {}
|
|
527
514
|
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
|
|
27
|
+
const { profileProcessPids } = require('./lib/validation-process-ownership.cjs');
|
|
28
28
|
const { acquireBuildHandoff } = require('./build-handoff.cjs');
|
|
29
29
|
const {
|
|
30
30
|
automationRuntimeArgs,
|
|
@@ -42,13 +42,7 @@ const {
|
|
|
42
42
|
validationLaunchQuarantineError,
|
|
43
43
|
writeRuntimeIdentity,
|
|
44
44
|
} = require('./lib/chrome-args.cjs');
|
|
45
|
-
const {
|
|
46
|
-
captureMacFrontmost,
|
|
47
|
-
macBackgroundOpenArgs,
|
|
48
|
-
preserveMacFrontmost,
|
|
49
|
-
startMacFocusHold,
|
|
50
|
-
stopMacFocusHold,
|
|
51
|
-
} = require('./lib/macos-focus.cjs');
|
|
45
|
+
const { captureMacFrontmostProcess, restoreMacFrontmostProcess } = require('./lib/macos-focus.cjs');
|
|
52
46
|
|
|
53
47
|
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
54
48
|
console.log(
|
|
@@ -113,7 +107,7 @@ try {
|
|
|
113
107
|
'Next: remove the invalid runtime identity and fix owner permissions, then rerun: mm-harness launch --adapter extension --build',
|
|
114
108
|
);
|
|
115
109
|
}
|
|
116
|
-
|
|
110
|
+
terminatePids([...ownedPids]);
|
|
117
111
|
if (args['reset-profile'] !== undefined) {
|
|
118
112
|
fs.rmSync(args.profile, { recursive: true, force: true });
|
|
119
113
|
fs.mkdirSync(args.profile, { recursive: true });
|
|
@@ -170,83 +164,70 @@ const chromeArgs = [
|
|
|
170
164
|
initialUrl,
|
|
171
165
|
];
|
|
172
166
|
const logFd = fs.openSync(args['chrome-log'], 'a');
|
|
173
|
-
const
|
|
174
|
-
const focusHold = startMacFocusHold(previousFrontmost);
|
|
167
|
+
const previousFrontmostPid = captureMacFrontmostProcess();
|
|
175
168
|
let browserPid;
|
|
176
169
|
try {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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,
|
|
170
|
+
const application = process.platform === 'darwin' ? macApplicationForExecutable(args['chrome-bin']) : null;
|
|
171
|
+
if (application) {
|
|
172
|
+
beginDetachedLaunch(cdpPort, args.profile, activeValidationLease);
|
|
173
|
+
execFileSync('open', ['-g', '-n', '-a', application, '--args', ...chromeArgs], {
|
|
174
|
+
env: sanitizedChildEnv(),
|
|
175
|
+
stdio: ['ignore', logFd, logFd],
|
|
221
176
|
});
|
|
222
|
-
|
|
223
|
-
if (
|
|
224
|
-
|
|
177
|
+
browserPid = waitForOwnedCdpPid(cdpPort, args.profile, args['extension-dir']);
|
|
178
|
+
if (browserPid === null) {
|
|
179
|
+
throw new Error(`Chrome launched but did not expose CDP on 127.0.0.1:${cdpPort}`);
|
|
225
180
|
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
|
|
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)], {
|
|
181
|
+
} else {
|
|
182
|
+
beginDetachedLaunch(cdpPort, args.profile, activeValidationLease);
|
|
183
|
+
const child = spawn(args['chrome-bin'], chromeArgs, {
|
|
237
184
|
detached: true,
|
|
238
|
-
|
|
185
|
+
env: sanitizedChildEnv(),
|
|
186
|
+
stdio: ['ignore', logFd, logFd],
|
|
239
187
|
});
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
188
|
+
child.unref();
|
|
189
|
+
browserPid = waitForOwnedCdpPid(cdpPort, args.profile, args['extension-dir']);
|
|
190
|
+
if (browserPid === null) {
|
|
191
|
+
if (child.pid) terminatePids([child.pid]);
|
|
192
|
+
throw new Error(
|
|
193
|
+
`Chrome launched but did not expose an owned CDP listener on 127.0.0.1:${cdpPort}. ` +
|
|
194
|
+
`Next: inspect ${args['chrome-log']}, then rerun: mm-harness launch --adapter extension --build`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
246
197
|
}
|
|
247
198
|
} finally {
|
|
248
|
-
|
|
249
|
-
|
|
199
|
+
fs.closeSync(logFd);
|
|
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);
|
|
250
231
|
}
|
|
251
232
|
|
|
252
233
|
function sanitizedChildEnv() {
|
|
@@ -445,64 +426,3 @@ function extensionHomeUrl(extensionDir) {
|
|
|
445
426
|
const id = extensionIdFromExtensionDir(extensionDir);
|
|
446
427
|
return id ? `chrome-extension://${id}/home.html` : '';
|
|
447
428
|
}
|
|
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,197 +1,32 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { execFileSync
|
|
4
|
-
|
|
5
|
-
const HOLD_INTERVAL_MS = 20;
|
|
6
|
-
const toolEnv = () => ({ ...process.env });
|
|
7
|
-
|
|
8
|
-
function captureMacFrontmost() {
|
|
9
|
-
if (process.platform !== 'darwin') return null;
|
|
10
|
-
try {
|
|
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
|
-
}
|
|
3
|
+
const { execFileSync } = require('node:child_process');
|
|
28
4
|
|
|
29
5
|
function captureMacFrontmostProcess() {
|
|
30
|
-
|
|
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() {
|
|
6
|
+
if (process.platform !== 'darwin') return null;
|
|
42
7
|
try {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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);
|
|
8
|
+
const value = execFileSync('osascript', [
|
|
9
|
+
'-e',
|
|
10
|
+
'tell application "System Events" to get unix id of first application process whose frontmost is true',
|
|
11
|
+
], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 1_000 }).trim();
|
|
12
|
+
const pid = Number.parseInt(value, 10);
|
|
57
13
|
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
58
14
|
} catch {
|
|
59
15
|
return null;
|
|
60
16
|
}
|
|
61
17
|
}
|
|
62
18
|
|
|
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
|
-
|
|
77
19
|
function restoreMacFrontmostProcess(pid) {
|
|
78
20
|
if (process.platform !== 'darwin' || !Number.isInteger(pid) || pid <= 0) return false;
|
|
79
21
|
try {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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);
|
|
22
|
+
execFileSync('osascript', [
|
|
23
|
+
'-e',
|
|
24
|
+
`tell application "System Events" to set frontmost of first application process whose unix id is ${pid} to true`,
|
|
25
|
+
], { stdio: 'ignore', timeout: 1_000 });
|
|
168
26
|
return true;
|
|
169
27
|
} catch {
|
|
170
28
|
return false;
|
|
171
29
|
}
|
|
172
30
|
}
|
|
173
31
|
|
|
174
|
-
|
|
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
|
-
};
|
|
32
|
+
module.exports = { captureMacFrontmostProcess, restoreMacFrontmostProcess };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { execFileSync
|
|
3
|
+
const { execFileSync } = require('node:child_process');
|
|
4
4
|
|
|
5
5
|
function commandHasExactProfile(command, profile) {
|
|
6
6
|
const expected = `--user-data-dir=${profile}`;
|
|
@@ -43,53 +43,6 @@ 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
|
-
|
|
93
46
|
async function stopProfileProcesses(profile, { quietMs = 0, timeoutMs = 5_000 } = {}) {
|
|
94
47
|
const deadline = Date.now() + timeoutMs;
|
|
95
48
|
let quietSince = Date.now();
|
|
@@ -113,4 +66,4 @@ async function stopProfileProcesses(profile, { quietMs = 0, timeoutMs = 5_000 }
|
|
|
113
66
|
throw new Error(`Extension validation profile did not remain quiescent for ${quietMs}ms.`);
|
|
114
67
|
}
|
|
115
68
|
|
|
116
|
-
module.exports = { profileProcessPids, stopProfileProcesses
|
|
69
|
+
module.exports = { profileProcessPids, stopProfileProcesses };
|