@deeeed/metamask-harness 0.79.0 → 0.80.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 +47 -0
- package/README.md +1 -1
- package/adapters/extension/ensure-browser.sh +2 -2
- package/adapters/extension/inject.mjs +0 -1
- package/adapters/extension/launch-browser.cjs +39 -613
- package/adapters/extension/readiness.mjs +2 -5
- package/adapters/extension/reattach.sh +2 -2
- package/adapters/extension/stamp-runtime-title.cjs +1 -1
- package/adapters/manifest.json +2 -34
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +57 -1358
- package/adapters/mobile/bridge-runtime/teaching.cjs +20 -0
- package/adapters/mobile/inject.sh +16 -0
- package/adapters/mobile/launch-console-forwarder.cjs +1 -2
- package/adapters/mobile/open-device.sh +2 -2
- package/adapters/mobile/reload-app.mjs +4 -4
- package/adapters/mobile/reset-app-data.sh +1 -1
- package/adapters/mobile/start-console-forwarder.sh +3 -2
- package/adapters/mobile/start-metro.sh +17 -12
- package/adapters/mobile/wait-for-bridge.cjs +2 -2
- package/adapters/shared/harness-source-fingerprint.mjs +13 -8
- package/adapters/shared/open-debug.mjs +2 -2
- package/adapters/shared/sync-wallet-fixture.sh +8 -5
- package/adapters/terminal/cleanup.mjs +2 -1
- package/adapters/terminal/inject.mjs +2 -1
- package/bin/mm-harness +9 -28
- package/dist/adapters/checkout-files.js +16 -0
- package/dist/adapters/console-capture.js +14 -1
- package/dist/adapters/core/actions.js +4 -3
- package/dist/adapters/core/overlay.js +1 -1
- package/dist/adapters/core/readiness.js +28 -1
- package/dist/adapters/core/run.js +2 -2
- package/dist/adapters/core/surface.js +16 -1
- package/dist/adapters/extension/browser-pid.js +56 -0
- package/dist/adapters/extension/console-capture.js +1 -1
- package/dist/adapters/extension/doctor-checks.js +1 -1
- package/dist/adapters/extension/ensure-ready.js +5 -4
- package/dist/adapters/extension/extension-id.js +1 -1
- package/dist/adapters/extension/feature-flags.js +1 -1
- package/dist/adapters/extension/fixtures.js +1 -2
- package/dist/adapters/extension/launch.js +4 -5
- package/dist/adapters/extension/launched-browser.js +1 -2
- package/dist/adapters/extension/network-observer.js +1 -1
- package/dist/adapters/extension/overlay.js +1 -1
- package/dist/adapters/extension/product-config.js +1 -1
- package/dist/adapters/extension/readiness.js +1 -1
- package/dist/adapters/extension/reload.js +1 -1
- package/dist/adapters/extension/run.js +24 -10
- package/dist/adapters/extension/runtime-decision.js +2 -1
- package/dist/adapters/extension/runtime.js +9 -5
- package/dist/adapters/extension/surface.js +15 -2
- package/dist/adapters/extension/verify.js +2 -2
- package/dist/adapters/harness-freshness.js +2 -1
- package/dist/adapters/mobile/android-gfxinfo-performance.js +1 -3
- package/dist/adapters/mobile/app-identity.js +10 -0
- package/dist/adapters/mobile/doctor-checks.js +1 -1
- package/dist/adapters/mobile/fixtures.js +1 -1
- package/dist/adapters/mobile/launch.js +27 -28
- package/dist/adapters/mobile/lifecycle.js +1 -1
- package/dist/adapters/mobile/metro-env.js +9 -59
- package/dist/adapters/mobile/performance-observer.js +1 -1
- package/dist/adapters/mobile/prepare-target.js +1 -1
- package/dist/adapters/mobile/prepare.js +3 -4
- package/dist/adapters/mobile/provision.js +1 -1
- package/dist/adapters/mobile/readiness.js +1 -1
- package/dist/adapters/mobile/recording-target.js +15 -0
- package/dist/adapters/mobile/release-artifact-state.js +1 -1
- package/dist/adapters/mobile/reload.js +1 -1
- package/dist/adapters/mobile/run.js +8 -7
- package/dist/adapters/mobile/runtime-decision.js +2 -4
- package/dist/adapters/mobile/source-freshness.js +22 -126
- package/dist/adapters/mobile/surface.js +26 -3
- package/dist/adapters/mobile/ui.js +16 -5
- package/dist/adapters/mobile/verify.js +2 -3
- package/dist/adapters/performance/cdp-trace.js +1 -1
- package/dist/adapters/performance/device-platform.js +1 -1
- package/dist/adapters/slot-ports.js +1 -1
- package/dist/adapters/surface.js +1 -0
- package/dist/adapters/terminal/console-capture.js +1 -1
- package/dist/adapters/terminal/launch.js +1 -1
- package/dist/adapters/terminal/run.js +25 -2
- package/dist/adapters/terminal/surface.js +7 -1
- package/dist/adapters.js +8 -4
- package/dist/advisor-policy.js +1 -1
- package/dist/checkout-view.js +1 -2
- package/dist/cli.js +28 -15
- package/dist/command-contract.js +1 -26
- package/dist/commands/call.js +10 -24
- package/dist/commands/check.js +3 -4
- package/dist/commands/checklist.js +1 -1
- package/dist/commands/completion-candidates.js +2 -10
- package/dist/commands/config.js +2 -2
- package/dist/commands/core-readiness.js +12 -7
- package/dist/commands/debug.js +3 -3
- package/dist/commands/device-target.js +5 -5
- package/dist/commands/doctor.js +2 -14
- package/dist/commands/domain.js +2 -2
- package/dist/commands/ensure-ready.js +1 -7
- package/dist/commands/fixtures.js +5 -6
- package/dist/commands/help.js +3 -4
- package/dist/commands/launch/device.js +10 -17
- package/dist/commands/launch/extension.js +2 -8
- package/dist/commands/launch/mobile.js +1 -1
- package/dist/commands/list-executables.js +1 -3
- package/dist/commands/logs.js +3 -3
- package/dist/commands/manifest.js +1 -12
- package/dist/commands/mobile-device-view.js +4 -2
- package/dist/commands/overlay.js +110 -0
- package/dist/commands/pin.js +1 -1
- package/dist/commands/pr-body.js +2 -2
- package/dist/commands/prepare.js +4 -9
- package/dist/commands/provision.js +3 -12
- package/dist/commands/recipe-advice.js +1 -2
- package/dist/commands/recipe-quality.js +2 -3
- package/dist/commands/resolve-extension.js +1 -7
- package/dist/commands/review.js +4 -4
- package/dist/commands/run-engine.js +12 -51
- package/dist/commands/run.js +4 -25
- package/dist/commands/runtime-decision.js +1 -6
- package/dist/commands/runtime-health.js +1 -7
- package/dist/commands/runtime-launch.js +1 -13
- package/dist/commands/self-test.js +2 -6
- package/dist/commands/status-watch.js +1 -2
- package/dist/commands/status.js +2 -5
- package/dist/commands/task-init.js +2 -3
- package/dist/commands/tutorial.js +2 -2
- package/dist/commands/update.js +1 -1
- package/dist/completions-cache.js +1 -1
- package/dist/console-classifier.js +19 -0
- package/dist/doctor.js +2 -3
- package/dist/funding-execution-context.js +15 -1
- package/dist/host.js +32 -0
- package/dist/index.js +1 -6
- package/dist/live-tsx.js +49 -0
- package/dist/mm-harness-cli.js +2 -4
- package/dist/network-observation.js +4 -4
- package/dist/paths.js +34 -64
- package/dist/performance-observation.js +2 -2
- package/dist/progress.js +1 -1
- package/dist/recipe-assistance-context.js +1 -3
- package/dist/recipe-security.js +2 -59
- package/dist/review/knowledge.js +2 -2
- package/dist/runner.js +5 -3
- package/dist/runtime-context.js +1 -1
- package/dist/task-view.js +2 -3
- package/docs/CONTRIBUTING.md +1 -1
- package/docs/cli-contract.md +2 -2
- package/docs/feature-guard.md +90 -0
- package/library/README.md +3 -1
- package/library/actions/core/perps/_controller.mjs +153 -130
- package/library/actions/core/perps/quote_fees.mjs +58 -0
- package/library/actions/core/wallet/_fixture-accounts.mjs +69 -0
- package/library/actions/core/wallet/list_accounts.mjs +14 -57
- package/library/actions/extension/feature_flags/state.mjs +1 -1
- package/library/actions/extension/platform/cdp.mjs +2 -8
- package/library/actions/harness-exports.mjs +1 -6
- package/library/actions/mobile/app/network-control.mjs +6 -8
- package/library/actions/mobile/perps/prepare_local_snapshot_endpoint.mjs +1 -1
- package/library/actions/mobile/platform/bridge.mjs +8 -8
- package/library/actions/mobile/platform/native-session-name.mjs +21 -1
- package/library/actions/mobile/platform/native-session.mjs +3 -4
- package/library/actions/mobile/platform/observe-ui.mjs +12 -6
- package/library/actions/mobile/ui/native-navigation.mjs +4 -3
- package/library/actions/mobile/wallet/native-ui.mjs +3 -2
- package/library/actions/shared/analytics/_adapter.mjs +1 -1
- package/library/actions/shared/console-findings.mjs +1 -1
- package/library/actions/terminal/platform/runtime.mjs +2 -1
- package/library/manifests/core.action-manifest.json +83 -13
- package/package.json +8 -9
- package/adapters/extension/lib/slot-title.cjs +0 -292
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +0 -472
- package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +0 -125
- package/adapters/mobile/bridge-runtime/lib/cdp-broker.cjs +0 -1055
- package/adapters/mobile/bridge-runtime/lib/cdp-eval.cjs +0 -110
- package/adapters/mobile/bridge-runtime/lib/config.cjs +0 -61
- package/adapters/mobile/bridge-runtime/lib/console-format.cjs +0 -53
- package/adapters/mobile/bridge-runtime/lib/devtools-proxy.cjs +0 -177
- package/adapters/mobile/bridge-runtime/lib/issue-capture.cjs +0 -446
- package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +0 -84
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +0 -403
- package/adapters/mobile/bridge-runtime/lib/ws-client.cjs +0 -116
- package/adapters/mobile/coalesce-metro-log.cjs +0 -24
- package/adapters/mobile/launch-metro.cjs +0 -77
- package/adapters/mobile/metro-config.cjs +0 -98
- package/adapters/mobile/metro-log-generation.cjs +0 -106
- package/dist/adapters/mobile/frame-metrics.js +0 -118
- package/dist/adapters/mobile/video-recorder.js +0 -289
- package/dist/artifact-files.js +0 -92
- package/dist/checkout-lock.js +0 -149
- package/dist/cli-color.js +0 -98
- package/dist/command-journal.js +0 -308
- package/dist/commands/last.js +0 -60
- package/dist/commands/launch/index.js +0 -109
- package/dist/commands/parse-args.js +0 -279
- package/dist/commands/reload.js +0 -41
- package/dist/commands/run-report.js +0 -241
- package/dist/commands/shared.js +0 -323
- package/dist/commands/stop.js +0 -60
- package/dist/devices.js +0 -100
- package/dist/execution-provenance.js +0 -332
- package/dist/harness.js +0 -451
- package/dist/heal-bounds.js +0 -137
- package/dist/json-stream.js +0 -60
- package/dist/leaf-invoke.js +0 -19
- package/dist/library-provenance.js +0 -33
- package/dist/live-adapter-contract.js +0 -380
- package/dist/recording-target.js +0 -120
- package/dist/run-diagnostics.js +0 -364
- package/dist/run-recording.js +0 -481
- package/dist/runtime-proof.js +0 -90
- package/library/actions/mobile/platform/tool-paths.mjs +0 -123
|
@@ -1,59 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// launch-browser.cjs —
|
|
2
|
+
// launch-browser.cjs — the Extension's isolated detached Chrome launch
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// which ignores that flag. The browser choice comes from
|
|
9
|
-
// <runtime>/browser-resolution.json when it names --chrome-bin.
|
|
4
|
+
// Runs @farmslot/adapter-web's launchBrowser with what MetaMask owns: the
|
|
5
|
+
// build-handoff lock (no build swaps the dist mid-launch), home.html and its
|
|
6
|
+
// "MetaMask" title, the harness runtime layouts that prove a browser is ours by
|
|
7
|
+
// its --load-extension path, and the mm-harness rerun command.
|
|
10
8
|
//
|
|
11
|
-
// Inputs (flags, all required):
|
|
9
|
+
// Inputs (flags, all required unless noted):
|
|
12
10
|
// --cdp-port <port> --chrome-bin <path> --profile <dir>
|
|
13
11
|
// --extension-dir <dist> --chrome-log <file> --chrome-pid <file>
|
|
14
|
-
//
|
|
12
|
+
// [--browser-resolution <resolver record>] [--start-url <url>]
|
|
13
|
+
// [--stop-only 1] [--reset-profile 1]
|
|
14
|
+
// MM_HARNESS_FOCUS_SETTLE_MS: wait before the one macOS focus check (default 1000).
|
|
15
15
|
//
|
|
16
|
-
// Outputs:
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
// Exit 0 — spawned; non-zero — invalid/missing args, missing chrome binary,
|
|
20
|
-
// or missing dist manifest (thrown error message on stderr).
|
|
21
|
-
//
|
|
22
|
-
// Never touches: the extension dist (read-only), anything outside the
|
|
23
|
-
// profile/log/pid paths it was given.
|
|
16
|
+
// Outputs: see launchBrowser. Exit 0 — launched (or released with --stop-only);
|
|
17
|
+
// non-zero — invalid/missing args, missing chrome binary or dist manifest, or a
|
|
18
|
+
// failed launch (thrown error message on stderr).
|
|
24
19
|
'use strict';
|
|
25
20
|
|
|
26
|
-
const fs = require('node:fs');
|
|
27
|
-
const os = require('node:os');
|
|
28
21
|
const path = require('node:path');
|
|
29
|
-
const {
|
|
30
|
-
const { extensionIdFromExtensionDir } = require('@farmslot/adapter-web/extension-id');
|
|
31
|
-
// The resolver CLI runs as a bounded child for CDP calls the launcher must not hang on.
|
|
32
|
-
const browserResolverPath = require.resolve('@farmslot/adapter-web/browser-resolver');
|
|
33
|
-
const { CDP_LOAD_UNPACKED, describeBrowser, extensionLaunchArgs, waitForCdpOwner } = require(browserResolverPath);
|
|
34
|
-
const { profileProcessPids, stopProfileProcessesSync } = require('@farmslot/adapter-web/validation-process-ownership');
|
|
22
|
+
const { launchBrowser } = require('@farmslot/adapter-web/launch-browser');
|
|
35
23
|
const { acquireBuildHandoff } = require('./build-handoff.cjs');
|
|
36
|
-
const {
|
|
37
|
-
automationRuntimeArgs,
|
|
38
|
-
clearDetachedLaunchUnproven,
|
|
39
|
-
clearValidationPortQuarantine,
|
|
40
|
-
createRuntimeIdentityNonce,
|
|
41
|
-
hasDetachedLaunchUnproven,
|
|
42
|
-
isolatedProfileArgs,
|
|
43
|
-
markDetachedLaunchUnproven,
|
|
44
|
-
markValidationPortLaunchUnproven,
|
|
45
|
-
remoteDebuggingArgs,
|
|
46
|
-
removeRuntimeIdentity,
|
|
47
|
-
runtimeIdentityArgs,
|
|
48
|
-
readValidationPortQuarantine,
|
|
49
|
-
validationLaunchQuarantineError,
|
|
50
|
-
writeRuntimeIdentity,
|
|
51
|
-
} = require('@farmslot/adapter-web/chrome-args');
|
|
52
|
-
const {
|
|
53
|
-
captureMacFrontmost,
|
|
54
|
-
macBackgroundOpenArgs,
|
|
55
|
-
restoreFrontmostIfOurs,
|
|
56
|
-
} = require('@farmslot/adapter-web/macos-focus');
|
|
57
24
|
|
|
58
25
|
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
59
26
|
console.log(
|
|
@@ -64,368 +31,28 @@ if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
|
64
31
|
|
|
65
32
|
const args = parseArgs(process.argv.slice(2));
|
|
66
33
|
const cdpPort = Number(args['cdp-port']);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
fs.mkdirSync(args.profile, { recursive: true });
|
|
91
|
-
fs.mkdirSync(path.dirname(args['chrome-log']), { recursive: true });
|
|
92
|
-
fs.mkdirSync(path.dirname(args['chrome-pid']), { recursive: true });
|
|
93
|
-
const releaseBuildHandoff = acquireBuildHandoff(runtimeDir);
|
|
94
|
-
process.on('exit', releaseBuildHandoff);
|
|
95
|
-
|
|
96
|
-
// Ownership guard: only ever take over a CDP endpoint we provably launched. A pid
|
|
97
|
-
// on this port is ours iff its command line loads our --user-data-dir; anything
|
|
98
|
-
// else is a browser this harness did not launch (possibly the user's personal
|
|
99
|
-
// Chrome), so we refuse rather than kill it or attach to it.
|
|
100
|
-
const listenerPids = cdpListenerPids(cdpPort);
|
|
101
|
-
const foreignPids = listenerPids.filter((pid) => !processIsOwnedHarnessBrowser(pid, args.profile, args['extension-dir']));
|
|
102
|
-
if (foreignPids.length > 0) {
|
|
103
|
-
throw new Error(
|
|
104
|
-
`Refusing to launch on CDP port ${cdpPort}: it is held by a browser this harness did not launch (pid ${foreignPids.join(', ')}). ` +
|
|
105
|
-
`Next: pick a free --cdp-port, or stop that browser.`,
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
const ownedPids = new Set(listenerPids);
|
|
109
|
-
for (const pid of profileProcessPids(args.profile)) ownedPids.add(pid);
|
|
110
|
-
const previousPid = readPidFile(args['chrome-pid']);
|
|
111
|
-
if (previousPid !== null && processIsOwnedHarnessBrowser(previousPid, args.profile, args['extension-dir'])) ownedPids.add(previousPid);
|
|
112
|
-
try {
|
|
113
|
-
removeRuntimeIdentity(runtimeDir);
|
|
114
|
-
} catch (error) {
|
|
115
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
116
|
-
throw new Error(
|
|
117
|
-
`Failed to invalidate Extension runtime identity under ${runtimeDir}: ${detail}. ` +
|
|
118
|
-
'Next: remove the invalid runtime identity and fix owner permissions, then rerun: mm-harness launch --adapter extension --build',
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
stopProfileProcessesSync(args.profile, { extraPids: [...ownedPids] });
|
|
122
|
-
if (args['reset-profile'] !== undefined) {
|
|
123
|
-
fs.rmSync(args.profile, { recursive: true, force: true });
|
|
124
|
-
fs.mkdirSync(args.profile, { recursive: true });
|
|
125
|
-
}
|
|
126
|
-
removeProfileSingletonLocks(args.profile);
|
|
127
|
-
|
|
128
|
-
// --stop-only: release the profile (owned Chrome terminated, singleton locks
|
|
129
|
-
// cleared) without launching. The prefill step needs the profile's LevelDB
|
|
130
|
-
// unlocked BEFORE it writes; a prior run's Chrome would hold the lock and the
|
|
131
|
-
// write fails with "Database is not open".
|
|
132
|
-
if (args['stop-only'] !== undefined) {
|
|
133
|
-
process.exit(0);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const requestedProfileQuarantined = hasDetachedLaunchUnproven(args.profile);
|
|
137
|
-
const portQuarantine = readValidationPortQuarantine(cdpPort);
|
|
138
|
-
const validationPortLease = process.env.FARMSLOT_VALIDATION_PORT_LEASE;
|
|
139
|
-
const activeValidationLease = Boolean(
|
|
140
|
-
portQuarantine?.profile === path.resolve(args.profile) &&
|
|
141
|
-
portQuarantine.lease === validationPortLease &&
|
|
142
|
-
/^[a-f0-9]{64}$/u.test(validationPortLease || ''),
|
|
143
|
-
);
|
|
144
|
-
if (requestedProfileQuarantined || (portQuarantine && !activeValidationLease)) {
|
|
145
|
-
throw validationLaunchQuarantineError(
|
|
146
|
-
cdpPort,
|
|
147
|
-
portQuarantine?.profile || (requestedProfileQuarantined ? args.profile : undefined),
|
|
148
|
-
requestedProfileQuarantined ? args.profile : undefined,
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const initialUrl = args['start-url'] || extensionHomeUrl(args['extension-dir']) || 'chrome://extensions/';
|
|
153
|
-
const browser = launchBrowserResolution(args['browser-resolution'], args['chrome-bin']);
|
|
154
|
-
const loadsOverCdp = browser.extensionLoading === CDP_LOAD_UNPACKED;
|
|
155
|
-
let cdpLoad = null;
|
|
156
|
-
let owner = null;
|
|
157
|
-
const runtimeNonce = createRuntimeIdentityNonce();
|
|
158
|
-
const startedAt = Date.now();
|
|
159
|
-
const chromeArgs = [
|
|
160
|
-
`--user-data-dir=${args.profile}`,
|
|
161
|
-
// Debug-port trio (address + port + scoped allow-origins) from the shared module,
|
|
162
|
-
// so the DevTools origin allow-list can never drift away from the port.
|
|
163
|
-
...remoteDebuggingArgs(cdpPort),
|
|
164
|
-
'--no-first-run',
|
|
165
|
-
'--disable-first-run-ui',
|
|
166
|
-
...automationRuntimeArgs(),
|
|
167
|
-
...runtimeIdentityArgs(runtimeNonce),
|
|
168
|
-
...isolatedProfileArgs(),
|
|
169
|
-
'--disable-default-apps',
|
|
170
|
-
'--disable-popup-blocking',
|
|
171
|
-
'--disable-extensions-file-access-check',
|
|
172
|
-
'--disable-extensions-content-verification',
|
|
173
|
-
'--disable-features=ExtensionContentVerification,DisableLoadExtensionCommandLineSwitch',
|
|
174
|
-
...extensionLaunchArgs(args['extension-dir'], browser.extensionLoading),
|
|
175
|
-
];
|
|
176
|
-
// Initial tab: caller's dapp URL when provided, else the extension home. The
|
|
177
|
-
// extension home avoids leaving disposable chrome://newtab/ or extensions
|
|
178
|
-
// manager tabs in the operator's slot browser. On macOS the browser starts
|
|
179
|
-
// through Launch Services in the background with no startup window (Chrome
|
|
180
|
-
// activates itself when it opens one), and the first window is opened over
|
|
181
|
-
// CDP as a background window. A CDP-loaded extension does not exist yet at
|
|
182
|
-
// startup, so its window is opened after the load.
|
|
183
|
-
const application = process.platform === 'darwin' ? macApplicationForExecutable(args['chrome-bin']) : null;
|
|
184
|
-
if (application) chromeArgs.push('--no-startup-window');
|
|
185
|
-
else chromeArgs.push(loadsOverCdp ? 'about:blank' : initialUrl);
|
|
186
|
-
const logFd = fs.openSync(args['chrome-log'], 'a');
|
|
187
|
-
const previousFrontmost = captureMacFrontmost();
|
|
188
|
-
let browserPid;
|
|
189
|
-
try {
|
|
190
|
-
try {
|
|
191
|
-
if (application) {
|
|
192
|
-
beginDetachedLaunch(cdpPort, args.profile, activeValidationLease);
|
|
193
|
-
execFileSync('open', macBackgroundOpenArgs(application, chromeArgs), {
|
|
194
|
-
env: sanitizedChildEnv(),
|
|
195
|
-
stdio: ['ignore', logFd, logFd],
|
|
196
|
-
});
|
|
197
|
-
browserPid = waitForOwnedCdpPid(cdpPort, args.profile, args['extension-dir']);
|
|
198
|
-
if (browserPid === null) {
|
|
199
|
-
stopProfileProcessesSync(args.profile, { waitForAppearanceMs: 400 });
|
|
200
|
-
throw new Error(
|
|
201
|
-
`Chrome launched but did not expose an owned CDP listener on 127.0.0.1:${cdpPort}. ` +
|
|
202
|
-
`Next: inspect ${args['chrome-log']}, then rerun: mm-harness launch --adapter extension --build`,
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
} else {
|
|
206
|
-
beginDetachedLaunch(cdpPort, args.profile, activeValidationLease);
|
|
207
|
-
const child = spawn(args['chrome-bin'], chromeArgs, {
|
|
208
|
-
detached: true,
|
|
209
|
-
env: sanitizedChildEnv(),
|
|
210
|
-
stdio: ['ignore', logFd, logFd],
|
|
211
|
-
});
|
|
212
|
-
child.unref();
|
|
213
|
-
browserPid = waitForOwnedCdpPid(cdpPort, args.profile, args['extension-dir']);
|
|
214
|
-
if (browserPid === null) {
|
|
215
|
-
stopProfileProcessesSync(args.profile, { extraPids: child.pid ? [child.pid] : [] });
|
|
216
|
-
throw new Error(
|
|
217
|
-
`Chrome launched but did not expose an owned CDP listener on 127.0.0.1:${cdpPort}. ` +
|
|
218
|
-
`Next: inspect ${args['chrome-log']}, then rerun: mm-harness launch --adapter extension --build`,
|
|
219
|
-
);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
if (loadsOverCdp) cdpLoad = loadExtensionOverCdp(browserPid);
|
|
223
|
-
else if (application) openBackgroundWindow(browserPid, initialUrl);
|
|
224
|
-
owner = cdpLoad?.owner ?? ownerIdentity(browserPid);
|
|
225
|
-
for (const problem of pruneExtraHomeTabs(cdpPort, args['extension-dir'])) {
|
|
226
|
-
process.stderr.write(`[launch] home-tab pruning: ${problem}\n`);
|
|
227
|
-
}
|
|
228
|
-
} finally {
|
|
229
|
-
fs.closeSync(logFd);
|
|
230
|
-
}
|
|
231
|
-
fs.writeFileSync(args['chrome-pid'], `${browserPid}\n`);
|
|
232
|
-
writeBrowserResolution(runtimeDir, {
|
|
233
|
-
...browser,
|
|
234
|
-
launch: {
|
|
235
|
-
pid: browserPid,
|
|
236
|
-
cdpPort,
|
|
237
|
-
startedAt: new Date(startedAt).toISOString(),
|
|
238
|
-
...(owner ? { profile: owner.profile, processStartedAt: owner.startedAt } : {}),
|
|
239
|
-
...(cdpLoad ? { extensionId: cdpLoad.id, otherExtensions: cdpLoad.otherExtensions } : {}),
|
|
240
|
-
},
|
|
241
|
-
});
|
|
242
|
-
try {
|
|
243
|
-
writeRuntimeIdentity(runtimeDir, {
|
|
244
|
-
port: cdpPort,
|
|
245
|
-
pid: browserPid,
|
|
246
|
-
startedAt,
|
|
247
|
-
nonce: runtimeNonce,
|
|
248
|
-
});
|
|
249
|
-
clearDetachedLaunchUnproven(args.profile);
|
|
250
|
-
if (!activeValidationLease) {
|
|
251
|
-
clearValidationPortQuarantine(cdpPort, args.profile);
|
|
252
|
-
}
|
|
253
|
-
} catch (error) {
|
|
254
|
-
terminatePids([browserPid]);
|
|
255
|
-
fs.rmSync(args['chrome-pid'], { force: true });
|
|
256
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
257
|
-
throw new Error(
|
|
258
|
-
`Failed to persist Extension runtime identity under ${runtimeDir}: ${detail}. ` +
|
|
259
|
-
'Next: fix owner write permissions for the runtime directory, then rerun: mm-harness launch --adapter extension --build',
|
|
260
|
-
);
|
|
261
|
-
}
|
|
262
|
-
if (process.platform === 'darwin') {
|
|
263
|
-
const wake = spawn('caffeinate', ['-dims', '-w', String(browserPid)], {
|
|
264
|
-
detached: true,
|
|
265
|
-
stdio: 'ignore',
|
|
266
|
-
});
|
|
267
|
-
wake.unref();
|
|
268
|
-
// Let the first window settle before the one focus check below.
|
|
269
|
-
const settleMs = Number(process.env.MM_HARNESS_FOCUS_SETTLE_MS);
|
|
270
|
-
const waitSec = Number.isFinite(settleMs) && settleMs >= 0 ? settleMs / 1000 : 1;
|
|
271
|
-
if (waitSec > 0) spawnSync('sleep', [String(waitSec)]);
|
|
272
|
-
}
|
|
273
|
-
} finally {
|
|
274
|
-
// One check, at most one restore, and only if OUR browser (this launch's
|
|
275
|
-
// CDP owner pid) holds the front; any other app in front is left alone.
|
|
276
|
-
if (browserPid) {
|
|
277
|
-
const outcome = restoreFrontmostIfOurs(previousFrontmost, [Number(browserPid)]);
|
|
278
|
-
if (outcome === 'restored') process.stderr.write(`[launch] focus: the slot browser took the front; restored ${previousFrontmost.name}\n`);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// The first window of a browser started without one, opened in the background.
|
|
283
|
-
// Without it the slot has no window at all, so a failure fails the launch:
|
|
284
|
-
// the owned browser is stopped (its markers kept if it cannot be).
|
|
285
|
-
function openBackgroundWindow(pid, url) {
|
|
286
|
-
const result = spawnSync(process.execPath, [browserResolverPath, 'open-window', '--port', String(cdpPort), '--url', url], {
|
|
287
|
-
encoding: 'utf8',
|
|
288
|
-
timeout: 30_000,
|
|
289
|
-
});
|
|
290
|
-
if (result.status === 0) return;
|
|
291
|
-
const openError = (result.stderr || result.error?.message || `exit ${result.status}`).trim();
|
|
292
|
-
try {
|
|
293
|
-
stopProfileProcessesSync(args.profile, { extraPids: [pid] });
|
|
294
|
-
} catch (stopError) {
|
|
295
|
-
throw new Error(
|
|
296
|
-
`Chrome started but its start window could not be opened (${openError}), and stopping it failed: ${stopError.message}. ` +
|
|
297
|
-
`The launch markers for port ${cdpPort} and ${args.profile} are kept. Next: stop pid ${pid}, then rerun.`,
|
|
298
|
-
);
|
|
299
|
-
}
|
|
300
|
-
clearDetachedLaunchUnproven(args.profile);
|
|
301
|
-
if (!activeValidationLease) clearValidationPortQuarantine(cdpPort, args.profile);
|
|
302
|
-
throw new Error(`Chrome started but its start window could not be opened: ${openError}. Next: inspect ${args['chrome-log']}, then rerun.`);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// live.sh passes the record its resolver just wrote (--browser-resolution);
|
|
306
|
-
// reuse it when it names the same binary. A direct --chrome-bin without it is
|
|
307
|
-
// the caller's own explicit choice and is recorded as such, whatever an older
|
|
308
|
-
// record of this profile says.
|
|
309
|
-
function launchBrowserResolution(file, chromeBin) {
|
|
310
|
-
if (!file) return explicitResolution(chromeBin);
|
|
311
|
-
try {
|
|
312
|
-
const recorded = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
313
|
-
if (recorded && recorded.bin === chromeBin && typeof recorded.extensionLoading === 'string') {
|
|
314
|
-
const { launch: _previousLaunch, ...resolution } = recorded;
|
|
315
|
-
return resolution;
|
|
316
|
-
}
|
|
317
|
-
} catch {
|
|
318
|
-
// No resolver record for this launch; fall through to a direct description.
|
|
319
|
-
}
|
|
320
|
-
return explicitResolution(chromeBin);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function explicitResolution(chromeBin) {
|
|
324
|
-
return { schemaVersion: 2, ...describeBrowser(chromeBin), source: '--chrome-bin', mode: 'explicit', selection: 'explicit', resolvedAt: new Date().toISOString() };
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function writeBrowserResolution(runtime, resolution) {
|
|
328
|
-
fs.writeFileSync(path.join(runtime, 'browser-resolution.json'), `${JSON.stringify(resolution, null, 2)}\n`);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
function loadExtensionOverCdp(pid) {
|
|
332
|
-
const result = spawnSync(process.execPath, [
|
|
333
|
-
browserResolverPath,
|
|
334
|
-
'load-unpacked',
|
|
335
|
-
'--port', String(cdpPort),
|
|
336
|
-
'--path', args['extension-dir'],
|
|
337
|
-
'--profile', args.profile,
|
|
338
|
-
'--open-url', initialUrl,
|
|
339
|
-
], { encoding: 'utf8', timeout: 130_000 });
|
|
340
|
-
if (result.status === 0) {
|
|
341
|
-
const loaded = JSON.parse(result.stdout);
|
|
342
|
-
process.stderr.write(`[launch] extension ${loaded.id} loaded over CDP\n`);
|
|
343
|
-
for (const entry of loaded.otherExtensions?.disabled ?? []) {
|
|
344
|
-
process.stderr.write(`[launch] disabled ${entry.id} (${entry.name}) in the slot profile\n`);
|
|
345
|
-
}
|
|
346
|
-
for (const entry of loaded.otherExtensions?.policyPinned ?? []) {
|
|
347
|
-
process.stderr.write(`[launch] WARN: enterprise policy keeps ${entry.id} (${entry.name}) enabled in the slot profile\n`);
|
|
348
|
-
}
|
|
349
|
-
return loaded;
|
|
350
|
-
}
|
|
351
|
-
// Ownership was proven; once the browser is stopped the launch markers would
|
|
352
|
-
// only quarantine a port and profile that nothing holds. If it cannot be
|
|
353
|
-
// stopped, the markers stay and both failures are reported.
|
|
354
|
-
const loadError = (result.stderr || result.error?.message || '').trim();
|
|
355
|
-
try {
|
|
356
|
-
stopProfileProcessesSync(args.profile, { extraPids: [pid] });
|
|
357
|
-
} catch (stopError) {
|
|
358
|
-
throw new Error(
|
|
359
|
-
`Chrome started but the extension did not load over CDP (${loadError}), and stopping it failed: ${stopError.message}. ` +
|
|
360
|
-
`The launch markers for port ${cdpPort} and ${args.profile} are kept. Next: stop pid ${pid}, then rerun.`,
|
|
361
|
-
);
|
|
362
|
-
}
|
|
363
|
-
clearDetachedLaunchUnproven(args.profile);
|
|
364
|
-
if (!activeValidationLease) clearValidationPortQuarantine(cdpPort, args.profile);
|
|
365
|
-
throw new Error(
|
|
366
|
-
`Chrome started but the extension did not load over CDP: ${loadError}. ` +
|
|
367
|
-
`Next: inspect ${args['chrome-log']}, or rerun with RECIPE_HARNESS_BROWSER=cft to use Chrome for Testing.`,
|
|
368
|
-
);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// Identity of the browser that holds the profile (pid + start time), recorded
|
|
372
|
-
// so run evidence can later prove it names this exact process.
|
|
373
|
-
// Best effort, so a short budget: a browser that never proves ownership (a
|
|
374
|
-
// stub without a SingletonLock, say) costs seconds, not the full default.
|
|
375
|
-
function ownerIdentity(pid) {
|
|
376
|
-
try {
|
|
377
|
-
const identity = waitForCdpOwner(cdpPort, args.profile, { timeoutMs: 15000 });
|
|
378
|
-
return identity.pid === pid ? identity : null;
|
|
379
|
-
} catch (error) {
|
|
380
|
-
process.stderr.write(`[launch] could not record the browser identity: ${error.message}\n`);
|
|
381
|
-
return null;
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
function sanitizedChildEnv() {
|
|
386
|
-
return {
|
|
387
|
-
...process.env,
|
|
388
|
-
BUNDLED_DEBUGPY_PATH: undefined,
|
|
389
|
-
PYTHONHOME: undefined,
|
|
390
|
-
PYTHONPATH: undefined,
|
|
391
|
-
DYLD_LIBRARY_PATH: undefined,
|
|
392
|
-
DYLD_FALLBACK_LIBRARY_PATH: undefined,
|
|
393
|
-
DYLD_INSERT_LIBRARIES: undefined,
|
|
394
|
-
};
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
function beginDetachedLaunch(port, profile, activeLease) {
|
|
398
|
-
try {
|
|
399
|
-
if (!activeLease) markValidationPortLaunchUnproven(port, profile);
|
|
400
|
-
markDetachedLaunchUnproven(profile);
|
|
401
|
-
} catch (error) {
|
|
402
|
-
if (error.code === 'EEXIST') {
|
|
403
|
-
const quarantine = readValidationPortQuarantine(port);
|
|
404
|
-
throw validationLaunchQuarantineError(port, quarantine?.profile || profile);
|
|
405
|
-
}
|
|
406
|
-
throw error;
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
function macApplicationForExecutable(executable) {
|
|
411
|
-
let current = path.resolve(executable);
|
|
412
|
-
while (current !== path.dirname(current)) {
|
|
413
|
-
if (current.endsWith('.app')) return current;
|
|
414
|
-
current = path.dirname(current);
|
|
415
|
-
}
|
|
416
|
-
return null;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function waitForOwnedCdpPid(port, profile, extensionDir) {
|
|
420
|
-
for (let i = 0; i < 300; i += 1) {
|
|
421
|
-
const pid = cdpListenerPids(port).find((candidate) =>
|
|
422
|
-
processIsOwnedHarnessBrowser(candidate, profile, extensionDir),
|
|
423
|
-
);
|
|
424
|
-
if (pid !== undefined) return pid;
|
|
425
|
-
spawnSync('sleep', ['0.1']);
|
|
426
|
-
}
|
|
427
|
-
return null;
|
|
428
|
-
}
|
|
34
|
+
// Checked here so the error echoes the flag as typed.
|
|
35
|
+
if (!Number.isInteger(cdpPort) || cdpPort <= 0) throw new Error(`Invalid --cdp-port: ${args['cdp-port'] || ''}`);
|
|
36
|
+
launchBrowser({
|
|
37
|
+
cdpPort,
|
|
38
|
+
chromeBin: args['chrome-bin'],
|
|
39
|
+
profile: args.profile,
|
|
40
|
+
extensionDir: args['extension-dir'],
|
|
41
|
+
runtimeDir: args['extension-dir'] ? path.dirname(path.resolve(args['extension-dir'])) : '',
|
|
42
|
+
chromeLog: args['chrome-log'],
|
|
43
|
+
chromePid: args['chrome-pid'],
|
|
44
|
+
browserResolution: args['browser-resolution'],
|
|
45
|
+
startUrl: args['start-url'],
|
|
46
|
+
stopOnly: args['stop-only'] !== undefined,
|
|
47
|
+
resetProfile: args['reset-profile'] !== undefined,
|
|
48
|
+
homePage: 'home.html',
|
|
49
|
+
defaultTitle: 'MetaMask',
|
|
50
|
+
extensionOwnerRoot: harnessOwnerRootFromExtensionDir,
|
|
51
|
+
acquireRuntimeLock: acquireBuildHandoff,
|
|
52
|
+
rerunCommand: 'mm-harness launch --adapter extension --build',
|
|
53
|
+
// Unset or invalid falls back to the package default (1000 ms).
|
|
54
|
+
focusSettleMs: Number(process.env.MM_HARNESS_FOCUS_SETTLE_MS),
|
|
55
|
+
});
|
|
429
56
|
|
|
430
57
|
function parseArgs(argv) {
|
|
431
58
|
const parsed = {};
|
|
@@ -439,216 +66,15 @@ function parseArgs(argv) {
|
|
|
439
66
|
return parsed;
|
|
440
67
|
}
|
|
441
68
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
const home = os.homedir();
|
|
445
|
-
const osDefaults = [
|
|
446
|
-
path.join(home, 'Library/Application Support/Google/Chrome'),
|
|
447
|
-
path.join(home, 'Library/Application Support/Chromium'),
|
|
448
|
-
path.join(home, 'Library/Application Support/Microsoft Edge'),
|
|
449
|
-
path.join(home, '.config/google-chrome'),
|
|
450
|
-
path.join(home, '.config/chromium'),
|
|
451
|
-
path.join(home, '.config/microsoft-edge'),
|
|
452
|
-
];
|
|
453
|
-
const underDefault = (dir) => resolved === dir || resolved.startsWith(`${dir}${path.sep}`);
|
|
454
|
-
if (resolved === home || resolved.includes(`${path.sep}.chrome-farmslot`) || osDefaults.some(underDefault)) {
|
|
455
|
-
throw new Error(
|
|
456
|
-
`Refusing to launch: --profile ${resolved} is a shared or default browser profile; ` +
|
|
457
|
-
`standalone launches must use a per-checkout profile under the harness runtime dir.`,
|
|
458
|
-
);
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
function assertResettableProfile(profile, expectedRuntimeDir) {
|
|
463
|
-
const resolved = path.resolve(profile);
|
|
464
|
-
const runtime = path.resolve(expectedRuntimeDir);
|
|
465
|
-
if (!resolved.startsWith(`${runtime}${path.sep}`)) {
|
|
466
|
-
throw new Error(`Refusing to reset --profile ${resolved}: it must be a dedicated child of ${runtime}.`);
|
|
467
|
-
}
|
|
468
|
-
const runtimeStat = fs.lstatSync(runtime);
|
|
469
|
-
if (!runtimeStat.isDirectory() || runtimeStat.isSymbolicLink()) {
|
|
470
|
-
throw new Error(`Refusing to reset --profile: runtime directory is not a regular directory: ${runtime}.`);
|
|
471
|
-
}
|
|
472
|
-
let current = runtime;
|
|
473
|
-
for (const segment of path.relative(runtime, resolved).split(path.sep)) {
|
|
474
|
-
current = path.join(current, segment);
|
|
475
|
-
if (!fs.existsSync(current)) break;
|
|
476
|
-
const stat = fs.lstatSync(current);
|
|
477
|
-
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
478
|
-
throw new Error(`Refusing to reset --profile: path contains an unsafe entry: ${current}.`);
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// Proof of ownership: a process is ours iff its command line loads the exact
|
|
484
|
-
// --user-data-dir we launch with. If the process can't be inspected we treat it
|
|
485
|
-
// as NOT ours (fail safe — never assume ownership of an opaque process).
|
|
486
|
-
function processIsOwnedHarnessBrowser(pid, profile, extensionDir) {
|
|
487
|
-
const command = processCommand(pid);
|
|
488
|
-
if (!command) return false;
|
|
489
|
-
if (command.includes(`--user-data-dir=${profile}`)) return true;
|
|
490
|
-
const ownerRoot = harnessOwnerRootFromExtensionDir(extensionDir);
|
|
491
|
-
return Boolean(ownerRoot && command.includes(`--load-extension=${ownerRoot}`));
|
|
492
|
-
}
|
|
493
|
-
|
|
69
|
+
// The overlay's live dist and the runtime snapshot both sit under a harness
|
|
70
|
+
// runtime root; a browser loading an extension from there is ours.
|
|
494
71
|
function harnessOwnerRootFromExtensionDir(extensionDir) {
|
|
495
|
-
const resolved = path.resolve(extensionDir);
|
|
496
72
|
for (const marker of [
|
|
497
73
|
`${path.sep}temp${path.sep}recipe${path.sep}harness${path.sep}extension${path.sep}live${path.sep}`,
|
|
498
74
|
`${path.sep}temp${path.sep}recipe${path.sep}runtime${path.sep}runtime-dist`,
|
|
499
75
|
]) {
|
|
500
|
-
const markerIndex =
|
|
501
|
-
if (markerIndex !== -1) return
|
|
76
|
+
const markerIndex = extensionDir.indexOf(marker);
|
|
77
|
+
if (markerIndex !== -1) return extensionDir.slice(0, markerIndex + marker.length);
|
|
502
78
|
}
|
|
503
79
|
return null;
|
|
504
80
|
}
|
|
505
|
-
|
|
506
|
-
function processCommand(pid) {
|
|
507
|
-
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
508
|
-
try {
|
|
509
|
-
return execFileSync('ps', ['-ww', '-o', 'command=', '-p', String(pid)], {
|
|
510
|
-
encoding: 'utf8',
|
|
511
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
512
|
-
});
|
|
513
|
-
} catch {
|
|
514
|
-
return null;
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
function readPidFile(file) {
|
|
519
|
-
if (!fs.existsSync(file)) return null;
|
|
520
|
-
const pid = Number.parseInt(fs.readFileSync(file, 'utf8').trim(), 10);
|
|
521
|
-
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
function cdpListenerPids(port) {
|
|
525
|
-
try {
|
|
526
|
-
return execFileSync('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-t'], {
|
|
527
|
-
encoding: 'utf8',
|
|
528
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
529
|
-
})
|
|
530
|
-
.split(/\s+/u)
|
|
531
|
-
.map((value) => Number.parseInt(value, 10))
|
|
532
|
-
.filter((pid) => Number.isInteger(pid) && pid > 0);
|
|
533
|
-
} catch (error) {
|
|
534
|
-
if (error.status === 1) return [];
|
|
535
|
-
throw error;
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
function terminatePids(pids) {
|
|
540
|
-
const unique = [...new Set(pids)].filter((pid) => pid !== process.pid);
|
|
541
|
-
for (const pid of unique) signalPid(pid, 'SIGTERM');
|
|
542
|
-
for (let i = 0; i < 10 && unique.some(processRespondsToSignal); i += 1) {
|
|
543
|
-
spawnSync('sleep', ['0.2']);
|
|
544
|
-
}
|
|
545
|
-
for (const pid of unique) {
|
|
546
|
-
if (processRespondsToSignal(pid)) signalPid(pid, 'SIGKILL');
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
function signalPid(pid, signal) {
|
|
551
|
-
try {
|
|
552
|
-
process.kill(pid, signal);
|
|
553
|
-
return true;
|
|
554
|
-
} catch (error) {
|
|
555
|
-
if (error.code === 'ESRCH') return false;
|
|
556
|
-
throw error;
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
function processRespondsToSignal(pid) {
|
|
561
|
-
try {
|
|
562
|
-
process.kill(pid, 0);
|
|
563
|
-
return true;
|
|
564
|
-
} catch (error) {
|
|
565
|
-
if (error.code === 'ESRCH') return false;
|
|
566
|
-
throw error;
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
function removeProfileSingletonLocks(profile) {
|
|
571
|
-
for (const name of ['SingletonLock', 'SingletonSocket', 'SingletonCookie']) {
|
|
572
|
-
const file = path.join(profile, name);
|
|
573
|
-
if (fs.existsSync(file)) fs.rmSync(file, { force: true });
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
function extensionHomeUrl(extensionDir) {
|
|
578
|
-
const id = extensionIdFromExtensionDir(extensionDir);
|
|
579
|
-
return id ? `chrome-extension://${id}/home.html` : '';
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
function cdpHttp(port, pathname) {
|
|
583
|
-
const result = spawnSync(process.execPath, ['-e', `
|
|
584
|
-
const http = require('http');
|
|
585
|
-
http.get('http://127.0.0.1:${Number(port)}${pathname}', (res) => {
|
|
586
|
-
let body = '';
|
|
587
|
-
res.on('data', (chunk) => { body += chunk; });
|
|
588
|
-
res.on('end', () => {
|
|
589
|
-
process.stdout.write(body);
|
|
590
|
-
process.exitCode = res.statusCode === 200 ? 0 : 1;
|
|
591
|
-
});
|
|
592
|
-
}).on('error', () => process.exit(1));
|
|
593
|
-
`], { encoding: 'utf8', timeout: 4000 });
|
|
594
|
-
if (result.status !== 0) return null;
|
|
595
|
-
return result.stdout;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
// Best effort by design: CDP ownership is already proven and ensure-ready
|
|
599
|
-
// converges home tabs again during verify, so a pruning failure must not fail
|
|
600
|
-
// the launch. Every failure is returned so the launch log says why a duplicate
|
|
601
|
-
// tab survived instead of hiding it.
|
|
602
|
-
function pruneExtraHomeTabs(port, extensionDir) {
|
|
603
|
-
const id = extensionIdFromExtensionDir(extensionDir);
|
|
604
|
-
if (!id) return ['extension id unavailable; skipped'];
|
|
605
|
-
const raw = cdpHttp(port, '/json/list');
|
|
606
|
-
if (raw === null) return [`GET http://127.0.0.1:${port}/json/list failed`];
|
|
607
|
-
let targets;
|
|
608
|
-
try {
|
|
609
|
-
targets = JSON.parse(raw);
|
|
610
|
-
} catch (error) {
|
|
611
|
-
return [`/json/list returned malformed JSON: ${error.message}`];
|
|
612
|
-
}
|
|
613
|
-
if (!Array.isArray(targets)) return ['/json/list did not return a target list'];
|
|
614
|
-
const problems = [];
|
|
615
|
-
const close = (target) => {
|
|
616
|
-
if (cdpHttp(port, `/json/close/${target.id}`) === null) problems.push(`could not close ${target.url} (${target.id})`);
|
|
617
|
-
};
|
|
618
|
-
const homes = targets.filter((target) => (
|
|
619
|
-
target &&
|
|
620
|
-
target.type === 'page' &&
|
|
621
|
-
typeof target.id === 'string' &&
|
|
622
|
-
typeof target.url === 'string' &&
|
|
623
|
-
target.url.includes(`chrome-extension://${id}/`) &&
|
|
624
|
-
target.url.includes('/home.html')
|
|
625
|
-
));
|
|
626
|
-
const strays = targets.filter((target) => (
|
|
627
|
-
target &&
|
|
628
|
-
target.type === 'page' &&
|
|
629
|
-
typeof target.id === 'string' &&
|
|
630
|
-
typeof target.url === 'string' &&
|
|
631
|
-
(
|
|
632
|
-
target.url === 'about:blank' ||
|
|
633
|
-
target.url === 'chrome://newtab/' ||
|
|
634
|
-
target.url.startsWith('chrome://new-tab-page')
|
|
635
|
-
)
|
|
636
|
-
));
|
|
637
|
-
if (homes.length === 0) return problems;
|
|
638
|
-
const keeper = homes.find((target) => (
|
|
639
|
-
typeof target.title === 'string' &&
|
|
640
|
-
target.title.trim() !== '' &&
|
|
641
|
-
target.title.trim() !== 'MetaMask'
|
|
642
|
-
)) ?? homes[0];
|
|
643
|
-
if (homes.length > 1) {
|
|
644
|
-
for (const extra of homes) {
|
|
645
|
-
if (keeper && extra.id === keeper.id) continue;
|
|
646
|
-
close(extra);
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
for (const stray of strays) {
|
|
650
|
-
if (keeper && stray.id === keeper.id) continue;
|
|
651
|
-
close(stray);
|
|
652
|
-
}
|
|
653
|
-
return problems;
|
|
654
|
-
}
|