@deeeed/metamask-harness 0.2.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 +161 -0
- package/README.md +140 -0
- package/bin/mm-harness +99 -0
- package/docs/CHEATSHEET.md +61 -0
- package/docs/CLI-SPEC.md +915 -0
- package/docs/MENTAL-MODEL.md +295 -0
- package/docs/architecture.md +367 -0
- package/docs/extension-runtime-commands.md +60 -0
- package/docs/harness-cli.md +43 -0
- package/docs/live-adapter-contract.md +188 -0
- package/docs/package-boundaries.md +47 -0
- package/docs/perps-flow-catalog.md +235 -0
- package/docs/recipe-libraries.md +95 -0
- package/docs/runtime-file-conventions.md +36 -0
- package/library/actions/core/perps/_controller.mjs +727 -0
- package/library/actions/core/perps/assert_orders.mjs +53 -0
- package/library/actions/core/perps/assert_positions.mjs +52 -0
- package/library/actions/core/perps/close_orders.mjs +97 -0
- package/library/actions/core/perps/close_positions.mjs +118 -0
- package/library/actions/core/perps/ensure_orders.mjs +40 -0
- package/library/actions/core/perps/ensure_positions.mjs +37 -0
- package/library/actions/core/perps/place_order.mjs +201 -0
- package/library/actions/core/perps/read_account.mjs +30 -0
- package/library/actions/core/perps/read_orders.mjs +27 -0
- package/library/actions/core/perps/read_positions.mjs +27 -0
- package/library/actions/core/perps/start_state.mjs +92 -0
- package/library/actions/core/perps/teardown_state.mjs +86 -0
- package/library/actions/extension/perps/assert_orders.mjs +11 -0
- package/library/actions/extension/perps/assert_positions.mjs +11 -0
- package/library/actions/extension/perps/close_orders.mjs +8 -0
- package/library/actions/extension/perps/close_positions.mjs +8 -0
- package/library/actions/extension/perps/ensure_orders.mjs +4 -0
- package/library/actions/extension/perps/ensure_positions.mjs +4 -0
- package/library/actions/extension/perps/perps.mjs +730 -0
- package/library/actions/extension/perps/place_order.mjs +7 -0
- package/library/actions/extension/perps/read_orders.mjs +4 -0
- package/library/actions/extension/perps/read_positions.mjs +3 -0
- package/library/actions/extension/platform/cdp.mjs +541 -0
- package/library/actions/extension/ui/navigate.mjs +44 -0
- package/library/actions/extension/wallet/ensure_unlocked.mjs +36 -0
- package/library/actions/extension/wallet/read_state.mjs +27 -0
- package/library/actions/extension/wallet/select_account.mjs +48 -0
- package/library/actions/extension/wallet/setup.mjs +35 -0
- package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +185 -0
- package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +1662 -0
- package/library/actions/mobile/bridge-runtime/cdp-bridge.cjs +686 -0
- package/library/actions/mobile/bridge-runtime/lib/cdp-eval.cjs +110 -0
- package/library/actions/mobile/bridge-runtime/lib/config.cjs +39 -0
- package/library/actions/mobile/bridge-runtime/lib/issue-capture.cjs +446 -0
- package/library/actions/mobile/bridge-runtime/lib/target-discovery.cjs +204 -0
- package/library/actions/mobile/bridge-runtime/lib/ws-client.cjs +108 -0
- package/library/actions/mobile/bridge-runtime/setup-wallet.sh +442 -0
- package/library/actions/mobile/perps/assert_orders.mjs +11 -0
- package/library/actions/mobile/perps/assert_positions.mjs +11 -0
- package/library/actions/mobile/perps/close_orders.mjs +8 -0
- package/library/actions/mobile/perps/close_positions.mjs +8 -0
- package/library/actions/mobile/perps/ensure_orders.mjs +4 -0
- package/library/actions/mobile/perps/ensure_positions.mjs +4 -0
- package/library/actions/mobile/perps/perps.mjs +709 -0
- package/library/actions/mobile/perps/place_order.mjs +7 -0
- package/library/actions/mobile/perps/read_orders.mjs +4 -0
- package/library/actions/mobile/perps/read_positions.mjs +3 -0
- package/library/actions/mobile/platform/bridge.mjs +283 -0
- package/library/actions/mobile/ui/navigate.mjs +38 -0
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +107 -0
- package/library/actions/mobile/wallet/home.mjs +35 -0
- package/library/actions/mobile/wallet/read_state.mjs +40 -0
- package/library/actions/mobile/wallet/select_account.mjs +48 -0
- package/library/actions/mobile/wallet/setup.mjs +220 -0
- package/library/flows/perps.flows.json +64 -0
- package/library/library.json +7 -0
- package/library/manifests/core.action-manifest.json +1282 -0
- package/library/manifests/extension.action-manifest.json +1749 -0
- package/library/manifests/mobile.action-manifest.json +1753 -0
- package/library/recipes/action-validation.extension.recipe.json +417 -0
- package/library/recipes/action-validation.mobile.recipe.json +422 -0
- package/library/recipes/order-lifecycle.core.recipe.json +78 -0
- package/library/recipes/perps-lifecycle.recipe.json +194 -0
- package/library/recipes/read-markets.core.recipe.json +38 -0
- package/library/recipes/smoke.extension.recipe.json +31 -0
- package/library/recipes/smoke.mobile.recipe.json +31 -0
- package/library/recipes/trading-lifecycle.core.recipe.json +76 -0
- package/orchestration/compat-overlays/README.md +19 -0
- package/orchestration/compat-overlays/mobile/README.md +13 -0
- package/orchestration/compat-overlays/mobile/rn81-message-event-source.patch +42 -0
- package/orchestration/core/cleanup.sh +37 -0
- package/orchestration/core/inject.sh +154 -0
- package/orchestration/doctor.mjs +72 -0
- package/orchestration/extension/cleanup.mjs +60 -0
- package/orchestration/extension/console-tail.mjs +228 -0
- package/orchestration/extension/ensure-browser.sh +416 -0
- package/orchestration/extension/ensure-ready.ts +185 -0
- package/orchestration/extension/extension-id.ts +107 -0
- package/orchestration/extension/inject.mjs +266 -0
- package/orchestration/extension/launch-browser.cjs +216 -0
- package/orchestration/extension/launch.sh +175 -0
- package/orchestration/extension/live.sh +320 -0
- package/orchestration/extension/pin-remote-flags.cjs +45 -0
- package/orchestration/extension/readiness.mjs +414 -0
- package/orchestration/extension/refresh-build.sh +190 -0
- package/orchestration/extension/runtime-decision.ts +445 -0
- package/orchestration/extension/runtime.ts +407 -0
- package/orchestration/extension/seed-fixture.sh +177 -0
- package/orchestration/extension/sidepanel-toggle.sh +291 -0
- package/orchestration/extension/snapshot-dist.sh +84 -0
- package/orchestration/extension/start-watch.sh +339 -0
- package/orchestration/extension/wallet-fixture-state.cjs +1086 -0
- package/orchestration/lib/activate-repo-node.sh +144 -0
- package/orchestration/lib/cli-color.mjs +84 -0
- package/orchestration/lib/cli-commands.mjs +243 -0
- package/orchestration/lib/cli-home.mjs +354 -0
- package/orchestration/lib/cli-ux.sh +252 -0
- package/orchestration/lib/cli-version.mjs +123 -0
- package/orchestration/lib/ensure-runner-deps.sh +56 -0
- package/orchestration/lib/harness-path.sh +55 -0
- package/orchestration/lib/hash-helpers.sh +44 -0
- package/orchestration/lib/json-field.sh +23 -0
- package/orchestration/lib/log-tui.mjs +304 -0
- package/orchestration/lib/open-debug.mjs +317 -0
- package/orchestration/lib/path-defaults.json +4 -0
- package/orchestration/lib/progress.mjs +107 -0
- package/orchestration/lib/recipe-paths.mjs +26 -0
- package/orchestration/lib/resolve-farmslot-ports.sh +144 -0
- package/orchestration/manifest.json +358 -0
- package/orchestration/mobile/cleanup.sh +192 -0
- package/orchestration/mobile/deps-markers.ts +21 -0
- package/orchestration/mobile/inject.sh +681 -0
- package/orchestration/mobile/launch.sh +137 -0
- package/orchestration/mobile/live.sh +125 -0
- package/orchestration/mobile/runtime-decision.ts +292 -0
- package/orchestration/porcelain/metamask-recipe +99 -0
- package/orchestration/porcelain/mm-recipe +1591 -0
- package/orchestration/porcelain/mme-recipe +1181 -0
- package/package.json +59 -0
- package/runner/extension/verify.sh +511 -0
- package/runner/mobile/verify.sh +501 -0
- package/runner/src/adapters.ts +601 -0
- package/runner/src/cli.ts +1820 -0
- package/runner/src/commands/debug.ts +44 -0
- package/runner/src/commands/fixtures.ts +99 -0
- package/runner/src/commands/launch.ts +397 -0
- package/runner/src/commands/logs.ts +60 -0
- package/runner/src/commands/shared.ts +138 -0
- package/runner/src/completions-cache.ts +86 -0
- package/runner/src/doctor.ts +203 -0
- package/runner/src/harness.ts +516 -0
- package/runner/src/heal-bounds.ts +179 -0
- package/runner/src/index.ts +6 -0
- package/runner/src/live-adapter-contract.ts +274 -0
- package/runner/src/manifest.ts +47 -0
- package/runner/src/mm-harness-cli.ts +488 -0
- package/runner/src/paths.ts +198 -0
- package/runner/src/recording-target.ts +147 -0
- package/runner/src/run-recording.ts +329 -0
- package/runner/src/runner.ts +108 -0
- package/runner/src/types.ts +57 -0
- package/scripts/completions.sh +125 -0
- package/scripts/install-completions.sh +62 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// extension-id.ts — deterministic extension (CRX) id resolution.
|
|
2
|
+
// App control: derives the id from the loaded dist manifest key. Name keeps
|
|
3
|
+
// the extension- prefix: this is the CRX id, not an instance/slot id.
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Single source of truth for "which extension id is this MetaMask build".
|
|
10
|
+
*
|
|
11
|
+
* WHY THIS EXISTS. The id was resolved in three different, drifting places —
|
|
12
|
+
* host `launch-browser.sh` (Playwright candidate loop), `reopen-browser.sh`
|
|
13
|
+
* (`serviceWorkers()[0]`, which grabs Chrome's *component* extensions and broke
|
|
14
|
+
* recovery), and the harness CDP target selector. They disagreed. This module
|
|
15
|
+
* makes the runner the one resolver host launchers defer to.
|
|
16
|
+
*
|
|
17
|
+
* The id is **deterministic**: Chrome derives an unpacked extension's id from
|
|
18
|
+
* the `key` in its manifest (base64-decode → sha256 → first 16 bytes → each
|
|
19
|
+
* nibble mapped to a-p). Since the browser is launched with `--load-extension`
|
|
20
|
+
* pointing at exactly this dist, the computed id IS the loaded extension's id —
|
|
21
|
+
* no service-worker/page guessing, robust even when the MV3 worker is idle.
|
|
22
|
+
*
|
|
23
|
+
* `--cdp-port` (optional) only *verifies* the computed id is actually present in
|
|
24
|
+
* the running browser; it never changes the answer.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export interface ResolveExtensionResult {
|
|
28
|
+
adapter: 'extension';
|
|
29
|
+
target: string;
|
|
30
|
+
extensionId: string | null;
|
|
31
|
+
source: 'manifest-key' | 'cdp-target' | 'none';
|
|
32
|
+
verified: boolean | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const DIST_MANIFEST = 'dist/chrome/manifest.json';
|
|
36
|
+
|
|
37
|
+
/** Chrome's unpacked-extension id derivation from a manifest `key` (base64 DER). */
|
|
38
|
+
export function extensionIdFromKey(keyBase64: string): string {
|
|
39
|
+
const der = Buffer.from(keyBase64, 'base64');
|
|
40
|
+
const hash = crypto.createHash('sha256').update(der).digest();
|
|
41
|
+
let id = '';
|
|
42
|
+
for (let i = 0; i < 16; i += 1) {
|
|
43
|
+
id += String.fromCharCode(97 + (hash[i] >> 4));
|
|
44
|
+
id += String.fromCharCode(97 + (hash[i] & 0xf));
|
|
45
|
+
}
|
|
46
|
+
return id;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function idFromDistManifest(target: string): string | null {
|
|
50
|
+
const manifestPath = path.join(target, DIST_MANIFEST);
|
|
51
|
+
if (!fs.existsSync(manifestPath)) return null;
|
|
52
|
+
try {
|
|
53
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as { key?: unknown };
|
|
54
|
+
return typeof manifest.key === 'string' && manifest.key.length > 0
|
|
55
|
+
? extensionIdFromKey(manifest.key)
|
|
56
|
+
: null;
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** chrome-extension ids visible to CDP (page targets + service workers). */
|
|
63
|
+
async function cdpExtensionIds(cdpPort: number): Promise<string[]> {
|
|
64
|
+
try {
|
|
65
|
+
const res = await fetch(`http://127.0.0.1:${cdpPort}/json/list`, { signal: AbortSignal.timeout(4000) });
|
|
66
|
+
if (!res.ok) return [];
|
|
67
|
+
const targets = (await res.json()) as Array<{ url?: string }>;
|
|
68
|
+
const ids = new Set<string>();
|
|
69
|
+
for (const t of targets) {
|
|
70
|
+
const m = /^chrome-extension:\/\/([^/]+)/u.exec(String(t.url ?? ''));
|
|
71
|
+
if (m) ids.add(m[1]);
|
|
72
|
+
}
|
|
73
|
+
return [...ids];
|
|
74
|
+
} catch {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Resolve the MetaMask extension id for a checkout. Pure file/crypto by default;
|
|
81
|
+
* `--cdp-port` adds a presence check (and a fallback when the dist has no `key`).
|
|
82
|
+
*/
|
|
83
|
+
export async function resolveExtensionId(
|
|
84
|
+
target: string,
|
|
85
|
+
options: { cdpPort?: number } = {},
|
|
86
|
+
): Promise<ResolveExtensionResult> {
|
|
87
|
+
const resolved = path.resolve(target);
|
|
88
|
+
const fromKey = idFromDistManifest(resolved);
|
|
89
|
+
let cdpIds: string[] | null = null;
|
|
90
|
+
if (options.cdpPort) cdpIds = await cdpExtensionIds(options.cdpPort);
|
|
91
|
+
|
|
92
|
+
if (fromKey) {
|
|
93
|
+
return {
|
|
94
|
+
adapter: 'extension',
|
|
95
|
+
target: resolved,
|
|
96
|
+
extensionId: fromKey,
|
|
97
|
+
source: 'manifest-key',
|
|
98
|
+
verified: cdpIds ? cdpIds.includes(fromKey) : null,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// No `key` in the dist manifest (rare). Fall back to a live CDP id if exactly
|
|
102
|
+
// one extension is loaded; otherwise we cannot disambiguate deterministically.
|
|
103
|
+
if (cdpIds && cdpIds.length === 1) {
|
|
104
|
+
return { adapter: 'extension', target: resolved, extensionId: cdpIds[0], source: 'cdp-target', verified: true };
|
|
105
|
+
}
|
|
106
|
+
return { adapter: 'extension', target: resolved, extensionId: null, source: 'none', verified: cdpIds ? false : null };
|
|
107
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// inject.mjs — install the extension harness overlay
|
|
3
|
+
//
|
|
4
|
+
// Purpose:
|
|
5
|
+
// Installs the runner delegate, action manifests, recipe snapshots and
|
|
6
|
+
// helper scripts into the target's ignored harness dir (overlay only).
|
|
7
|
+
//
|
|
8
|
+
// Inputs (flags / env):
|
|
9
|
+
// --target <metamask-extension> (default $PWD)
|
|
10
|
+
// --no-git-exclude skip .git/info/exclude updates
|
|
11
|
+
// env RECIPE_HARNESS_ROOT, METAMASK_RUNNER_DIR,
|
|
12
|
+
// METAMASK_RUNNER_PROTOCOL_ROOT > FARMSLOT_ROOT > <runner>/.farmslot-root
|
|
13
|
+
//
|
|
14
|
+
// Outputs:
|
|
15
|
+
// <harness>/{manifest.json,action-manifest.json,runner/,scripts/,
|
|
16
|
+
// installed-scripts.sha256,added-git-exclude}; pass JSON on stdout.
|
|
17
|
+
// Exit 0 — installed; 2 — bad args; non-zero — refusal (not an extension
|
|
18
|
+
// checkout, symlink path component) via thrown error.
|
|
19
|
+
//
|
|
20
|
+
// Never touches: product source files; anything outside the target's
|
|
21
|
+
// harness dir and the target's .git/info/exclude.
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import { spawnSync } from 'node:child_process';
|
|
25
|
+
import process from 'node:process';
|
|
26
|
+
import { recipeHarnessRoot } from '../lib/recipe-paths.mjs';
|
|
27
|
+
|
|
28
|
+
function usage() {
|
|
29
|
+
console.error('Usage: inject-extension-harness.mjs [--target <metamask-extension>] [--no-git-exclude]');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let target = process.cwd();
|
|
33
|
+
let gitExclude = true;
|
|
34
|
+
for (let i = 2; i < process.argv.length; i += 1) {
|
|
35
|
+
const arg = process.argv[i];
|
|
36
|
+
if (arg === '--target') {
|
|
37
|
+
if (i + 1 >= process.argv.length) { usage(); process.exit(2); }
|
|
38
|
+
target = process.argv[++i];
|
|
39
|
+
} else if (arg === '--no-git-exclude') {
|
|
40
|
+
gitExclude = false;
|
|
41
|
+
} else if (arg === '-h' || arg === '--help') {
|
|
42
|
+
usage(); process.exit(0);
|
|
43
|
+
} else {
|
|
44
|
+
console.error(`inject-extension-harness: unknown arg: ${arg}`);
|
|
45
|
+
usage(); process.exit(2);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
target = path.resolve(target);
|
|
50
|
+
const runnerRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../..');
|
|
51
|
+
const harnessRoot = harnessRootValue();
|
|
52
|
+
const harnessRel = `${harnessRoot}/extension`;
|
|
53
|
+
const harnessDir = path.join(target, harnessRel);
|
|
54
|
+
const runnerDir = process.env.METAMASK_RUNNER_DIR || runnerRoot;
|
|
55
|
+
const runnerRevision = gitHead(runnerDir);
|
|
56
|
+
const runnerPackageVersion = readPackageVersion(runnerDir);
|
|
57
|
+
const protocolRoot = process.env.METAMASK_RUNNER_PROTOCOL_ROOT || process.env.METAMASK_RUNNER_FARMSLOT_ROOT || process.env.FARMSLOT_ROOT || configuredProtocolRoot(runnerDir);
|
|
58
|
+
|
|
59
|
+
assertExtensionRoot(target);
|
|
60
|
+
for (const rel of [harnessRel, `${harnessRel}/runner`, `${harnessRel}/action-manifest.json`]) {
|
|
61
|
+
refuseSymlinkDestination(target, rel);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fs.rmSync(path.join(harnessDir, 'runner'), { recursive: true, force: true });
|
|
65
|
+
fs.mkdirSync(path.join(harnessDir, 'runner/bin'), { recursive: true });
|
|
66
|
+
fs.mkdirSync(path.join(harnessDir, 'runner/manifests'), { recursive: true });
|
|
67
|
+
fs.mkdirSync(path.join(harnessDir, 'runner/recipes'), { recursive: true });
|
|
68
|
+
fs.rmSync(path.join(harnessDir, 'scripts'), { recursive: true, force: true });
|
|
69
|
+
fs.mkdirSync(path.join(harnessDir, 'scripts/lib'), { recursive: true });
|
|
70
|
+
|
|
71
|
+
const delegate = [
|
|
72
|
+
'#!/usr/bin/env bash',
|
|
73
|
+
'set -euo pipefail',
|
|
74
|
+
protocolRoot ? `export FARMSLOT_ROOT=\${FARMSLOT_ROOT:-${shellQuote(protocolRoot)}}` : null,
|
|
75
|
+
`exec ${shellQuote(path.join(runnerDir, 'orchestration/porcelain/metamask-recipe'))} "$@"`,
|
|
76
|
+
].filter(Boolean).join('\n') + '\n';
|
|
77
|
+
fs.writeFileSync(path.join(harnessDir, 'runner/bin/metamask-recipe'), delegate, { mode: 0o755 });
|
|
78
|
+
if (protocolRoot) fs.writeFileSync(path.join(harnessDir, 'runner/.farmslot-root'), `${protocolRoot}\n`);
|
|
79
|
+
fs.writeFileSync(path.join(harnessDir, 'runner/.runner-source'), `${runnerDir}\n`);
|
|
80
|
+
copyFile(path.join(runnerDir, 'library/manifests/mobile.action-manifest.json'), path.join(harnessDir, 'runner/manifests/mobile.action-manifest.json'));
|
|
81
|
+
copyFile(path.join(runnerDir, 'library/manifests/extension.action-manifest.json'), path.join(harnessDir, 'runner/manifests/extension.action-manifest.json'));
|
|
82
|
+
copyFile(path.join(runnerDir, 'library/manifests/extension.action-manifest.json'), path.join(harnessDir, 'action-manifest.json'));
|
|
83
|
+
copyDir(path.join(runnerDir, 'library/recipes'), path.join(harnessDir, 'runner/recipes'));
|
|
84
|
+
// Canonical recipe-library snapshot: flows/ + library.json so the installed
|
|
85
|
+
// harness mirrors the runner's library layout.
|
|
86
|
+
copyFile(path.join(runnerDir, 'library/library.json'), path.join(harnessDir, 'runner/library.json'));
|
|
87
|
+
fs.mkdirSync(path.join(harnessDir, 'runner/flows'), { recursive: true });
|
|
88
|
+
copyDir(path.join(runnerDir, 'library/flows'), path.join(harnessDir, 'runner/flows'));
|
|
89
|
+
// Installed-layout back-compat (INTENTIONAL): targets keep a flat
|
|
90
|
+
// <harness>/scripts/ subtree assembled from the runner trees, so installed
|
|
91
|
+
// consumers are unaffected by runner-repo layout changes.
|
|
92
|
+
copyFile(path.join(runnerDir, 'orchestration/extension/launch-browser.cjs'), path.join(harnessDir, 'scripts/launch-browser.cjs'));
|
|
93
|
+
// installed back-compat alias: callers of the pre-rename installed name
|
|
94
|
+
copyFile(path.join(runnerDir, 'orchestration/extension/launch-browser.cjs'), path.join(harnessDir, 'scripts/launch-chrome-detached.cjs'));
|
|
95
|
+
copyFile(path.join(runnerDir, 'orchestration/extension/console-tail.mjs'), path.join(harnessDir, 'scripts/console-tail.mjs'));
|
|
96
|
+
copyFile(path.join(runnerDir, 'orchestration/extension/launch.sh'), path.join(harnessDir, 'scripts/launch.sh'));
|
|
97
|
+
copyFile(path.join(runnerDir, 'orchestration/extension/live.sh'), path.join(harnessDir, 'scripts/live.sh'));
|
|
98
|
+
copyFile(path.join(runnerDir, 'orchestration/extension/start-watch.sh'), path.join(harnessDir, 'scripts/start-watch.sh'));
|
|
99
|
+
copyFile(path.join(runnerDir, 'orchestration/extension/snapshot-dist.sh'), path.join(harnessDir, 'scripts/snapshot-dist.sh'));
|
|
100
|
+
copyFile(path.join(runnerDir, 'orchestration/extension/pin-remote-flags.cjs'), path.join(harnessDir, 'scripts/pin-remote-flags.cjs'));
|
|
101
|
+
copyFile(path.join(runnerDir, 'orchestration/extension/seed-fixture.sh'), path.join(harnessDir, 'scripts/seed-fixture.sh'));
|
|
102
|
+
copyFile(path.join(runnerDir, 'orchestration/extension/sidepanel-toggle.sh'), path.join(harnessDir, 'scripts/sidepanel-toggle.sh'));
|
|
103
|
+
copyFile(path.join(runnerDir, 'orchestration/extension/readiness.mjs'), path.join(harnessDir, 'scripts/readiness.mjs'));
|
|
104
|
+
copyFile(path.join(runnerDir, 'orchestration/extension/wallet-fixture-state.cjs'), path.join(harnessDir, 'scripts/wallet-fixture-state.cjs'));
|
|
105
|
+
copyFile(path.join(runnerDir, 'runner/extension/verify.sh'), path.join(harnessDir, 'scripts/verify.sh'));
|
|
106
|
+
copyFile(path.join(runnerDir, 'orchestration/extension/refresh-build.sh'), path.join(harnessDir, 'scripts/refresh-build.sh'));
|
|
107
|
+
copyFile(path.join(runnerDir, 'orchestration/extension/ensure-browser.sh'), path.join(harnessDir, 'scripts/reopen-browser.sh'));
|
|
108
|
+
copyFile(path.join(runnerDir, 'orchestration/lib/harness-path.sh'), path.join(harnessDir, 'scripts/lib/harness-path.sh'));
|
|
109
|
+
copyFile(path.join(runnerDir, 'orchestration/lib/activate-repo-node.sh'), path.join(harnessDir, 'scripts/lib/activate-repo-node.sh'));
|
|
110
|
+
copyFile(path.join(runnerDir, 'orchestration/lib/resolve-farmslot-ports.sh'), path.join(harnessDir, 'scripts/lib/resolve-farmslot-ports.sh'));
|
|
111
|
+
copyFile(path.join(runnerDir, 'orchestration/lib/path-defaults.json'), path.join(harnessDir, 'scripts/lib/path-defaults.json'));
|
|
112
|
+
copyFile(path.join(runnerDir, 'orchestration/lib/recipe-paths.mjs'), path.join(harnessDir, 'scripts/lib/recipe-paths.mjs'));
|
|
113
|
+
copyFile(path.join(runnerDir, 'orchestration/lib/json-field.sh'), path.join(harnessDir, 'scripts/lib/json-field.sh'));
|
|
114
|
+
copyFile(path.join(runnerDir, 'orchestration/lib/cli-color.mjs'), path.join(harnessDir, 'scripts/lib/cli-color.mjs'));
|
|
115
|
+
copyFile(path.join(runnerDir, 'orchestration/lib/log-tui.mjs'), path.join(harnessDir, 'scripts/lib/log-tui.mjs'));
|
|
116
|
+
copyFile(path.join(runnerDir, 'orchestration/lib/progress.mjs'), path.join(harnessDir, 'scripts/lib/progress.mjs'));
|
|
117
|
+
copyFile(path.join(runnerDir, 'orchestration/lib/cli-version.mjs'), path.join(harnessDir, 'scripts/lib/cli-version.mjs'));
|
|
118
|
+
makeExecutableTree(path.join(harnessDir, 'scripts'));
|
|
119
|
+
fs.writeFileSync(path.join(harnessDir, 'installed-scripts.sha256'), `${dirContentHash(path.join(harnessDir, 'scripts'))}\n`);
|
|
120
|
+
|
|
121
|
+
const cleanupScript = path.join(runnerDir, 'orchestration/extension/cleanup.mjs');
|
|
122
|
+
const cleanupCommand = `RECIPE_HARNESS_ROOT=${harnessRoot} ${shellQuote(cleanupScript)} --target ${shellQuote(target)}`;
|
|
123
|
+
const manifest = {
|
|
124
|
+
adapter: 'extension',
|
|
125
|
+
installedAt: new Date().toISOString(),
|
|
126
|
+
runnerPackageVersion,
|
|
127
|
+
source: {
|
|
128
|
+
runnerDir,
|
|
129
|
+
runnerRevision,
|
|
130
|
+
runnerSourceKind: process.env.METAMASK_RUNNER_SOURCE_KIND || 'runner-self',
|
|
131
|
+
},
|
|
132
|
+
target,
|
|
133
|
+
protocolVersion: 'v1',
|
|
134
|
+
actionManifestPath: `${harnessRel}/action-manifest.json`,
|
|
135
|
+
runnerEntrypoint: `${harnessRel}/runner/bin/metamask-recipe`,
|
|
136
|
+
installedPaths: [`${harnessRel}/scripts`, `${harnessRel}/runner`, `${harnessRel}/action-manifest.json`, `${harnessRel}/manifest.json`],
|
|
137
|
+
runtimeHelpers: {
|
|
138
|
+
live: `${harnessRel}/scripts/live.sh`,
|
|
139
|
+
refreshBuild: `${harnessRel}/scripts/refresh-build.sh`,
|
|
140
|
+
reopenBrowser: `${harnessRel}/scripts/reopen-browser.sh`,
|
|
141
|
+
sidepanelToggle: `${harnessRel}/scripts/sidepanel-toggle.sh`,
|
|
142
|
+
},
|
|
143
|
+
patchedFiles: [],
|
|
144
|
+
backupDir: null,
|
|
145
|
+
cleanupCommand,
|
|
146
|
+
productDiffExcludes: [`:(exclude)${harnessRoot}`],
|
|
147
|
+
};
|
|
148
|
+
fs.writeFileSync(path.join(harnessDir, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
149
|
+
|
|
150
|
+
if (gitExclude) addGitExclude(target, `${harnessRoot}/`, harnessDir);
|
|
151
|
+
|
|
152
|
+
const ensureDeps = path.join(runnerDir, 'orchestration/lib/ensure-runner-deps.sh');
|
|
153
|
+
if (fs.existsSync(ensureDeps)) {
|
|
154
|
+
const ensured = spawnSync('bash', [ensureDeps, runnerDir], { stdio: 'inherit' });
|
|
155
|
+
if (ensured.status !== 0) process.exit(ensured.status ?? 1);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
console.log(JSON.stringify({ status: 'pass', adapter: 'extension', harnessDir, manifestPath: path.join(harnessDir, 'manifest.json'), runnerEntrypoint: manifest.runnerEntrypoint }, null, 2));
|
|
159
|
+
|
|
160
|
+
function harnessRootValue() {
|
|
161
|
+
return recipeHarnessRoot();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function assertExtensionRoot(dir) {
|
|
165
|
+
const packageJson = path.join(dir, 'package.json');
|
|
166
|
+
if (!fs.existsSync(packageJson)) throw new Error(`Target is not a MetaMask Extension checkout: ${dir}`);
|
|
167
|
+
const text = fs.readFileSync(packageJson, 'utf8');
|
|
168
|
+
if (!text.includes('metamask-extension') && !path.basename(dir).startsWith('metamask-extension')) {
|
|
169
|
+
throw new Error(`Target is not a MetaMask Extension checkout: ${dir}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function refuseSymlinkDestination(root, rel) {
|
|
174
|
+
let current = root;
|
|
175
|
+
for (const part of rel.split('/').filter(Boolean)) {
|
|
176
|
+
current = path.join(current, part);
|
|
177
|
+
if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink()) {
|
|
178
|
+
throw new Error(`Refusing extension recipe harness install: ${rel} contains symlink component ${current}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function copyFile(src, dest) {
|
|
184
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
185
|
+
fs.copyFileSync(src, dest);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function copyDir(src, dest) {
|
|
189
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
190
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
191
|
+
if (!fs.existsSync(src)) return;
|
|
192
|
+
fs.cpSync(src, dest, { recursive: true });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function makeExecutableTree(dir) {
|
|
196
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
197
|
+
const file = path.join(dir, entry.name);
|
|
198
|
+
if (entry.isDirectory()) {
|
|
199
|
+
makeExecutableTree(file);
|
|
200
|
+
} else if (/\.(?:sh|js|cjs|mjs)$/u.test(entry.name)) {
|
|
201
|
+
fs.chmodSync(file, 0o755);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function dirContentHash(dir) {
|
|
207
|
+
const files = [];
|
|
208
|
+
collectFiles(dir, files);
|
|
209
|
+
const hashes = files.sort().map((file) => {
|
|
210
|
+
const relative = path.relative(dir, file);
|
|
211
|
+
const content = spawnSync('shasum', ['-a', '256', file], { encoding: 'utf8' });
|
|
212
|
+
if (content.status !== 0) throw new Error(`Failed to hash ${file}: ${content.stderr}`);
|
|
213
|
+
return `${content.stdout.trim()} ${relative}`;
|
|
214
|
+
}).join('\n');
|
|
215
|
+
const result = spawnSync('shasum', ['-a', '256'], { input: hashes, encoding: 'utf8' });
|
|
216
|
+
if (result.status !== 0) throw new Error(`Failed to hash extension scripts: ${result.stderr}`);
|
|
217
|
+
return result.stdout.trim().split(/\s+/u)[0];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function collectFiles(dir, files) {
|
|
221
|
+
if (!fs.existsSync(dir)) return;
|
|
222
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
223
|
+
const file = path.join(dir, entry.name);
|
|
224
|
+
if (entry.isDirectory()) collectFiles(file, files);
|
|
225
|
+
else if (entry.isFile()) files.push(file);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function gitHead(dir) {
|
|
230
|
+
const result = spawnSync('git', ['-C', dir, 'rev-parse', 'HEAD'], { encoding: 'utf8' });
|
|
231
|
+
return result.status === 0 ? result.stdout.trim() : 'unknown';
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function readPackageVersion(dir) {
|
|
235
|
+
try {
|
|
236
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
|
|
237
|
+
return pkg.version || 'unknown';
|
|
238
|
+
} catch {
|
|
239
|
+
return 'unknown';
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function configuredProtocolRoot(dir) {
|
|
244
|
+
const file = path.join(dir, '.farmslot-root');
|
|
245
|
+
return fs.existsSync(file) ? fs.readFileSync(file, 'utf8').trim() : '';
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function shellQuote(value) {
|
|
249
|
+
return `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function addGitExclude(repo, entry, harnessDir) {
|
|
253
|
+
const check = spawnSync('git', ['-C', repo, 'check-ignore', '-q', entry.replace(/\/$/u, '')]);
|
|
254
|
+
if (check.status === 0) return;
|
|
255
|
+
const gitDirResult = spawnSync('git', ['-C', repo, 'rev-parse', '--git-dir'], { encoding: 'utf8' });
|
|
256
|
+
if (gitDirResult.status !== 0) return;
|
|
257
|
+
let gitDir = gitDirResult.stdout.trim();
|
|
258
|
+
if (!path.isAbsolute(gitDir)) gitDir = path.join(repo, gitDir);
|
|
259
|
+
const excludeFile = path.join(gitDir, 'info/exclude');
|
|
260
|
+
fs.mkdirSync(path.dirname(excludeFile), { recursive: true });
|
|
261
|
+
const existing = fs.existsSync(excludeFile) ? fs.readFileSync(excludeFile, 'utf8').split('\n') : [];
|
|
262
|
+
if (!existing.includes(entry)) {
|
|
263
|
+
fs.appendFileSync(excludeFile, `${entry}\n`);
|
|
264
|
+
fs.appendFileSync(path.join(harnessDir, 'added-git-exclude'), `${entry}\n`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// launch-browser.cjs — per-instance isolated detached Chrome spawn
|
|
3
|
+
//
|
|
4
|
+
// Purpose:
|
|
5
|
+
// Spawns a detached Chrome bound to 127.0.0.1:<cdp-port> with its own
|
|
6
|
+
// profile and the runtime dist loaded as an unpacked extension.
|
|
7
|
+
//
|
|
8
|
+
// Inputs (flags, all required):
|
|
9
|
+
// --cdp-port <port> --chrome-bin <path> --profile <dir>
|
|
10
|
+
// --extension-dir <dist> --chrome-log <file> --chrome-pid <file>
|
|
11
|
+
// Env is sanitized for the child (DYLD_*, PYTHON*, BUNDLED_DEBUGPY_PATH).
|
|
12
|
+
//
|
|
13
|
+
// Outputs:
|
|
14
|
+
// <chrome-pid> pid file; <chrome-log> append-log.
|
|
15
|
+
// Exit 0 — spawned; non-zero — invalid/missing args, missing chrome binary,
|
|
16
|
+
// or missing dist manifest (thrown error message on stderr).
|
|
17
|
+
//
|
|
18
|
+
// Never touches: the extension dist (read-only), anything outside the
|
|
19
|
+
// profile/log/pid paths it was given.
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const fs = require('node:fs');
|
|
23
|
+
const os = require('node:os');
|
|
24
|
+
const path = require('node:path');
|
|
25
|
+
const { execFileSync, spawn, spawnSync } = require('node:child_process');
|
|
26
|
+
|
|
27
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
28
|
+
console.log(
|
|
29
|
+
'Usage: launch-browser.cjs --cdp-port <port> --chrome-bin <path> --profile <dir> --extension-dir <dist> --chrome-log <file> --chrome-pid <file>',
|
|
30
|
+
);
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const args = parseArgs(process.argv.slice(2));
|
|
35
|
+
const cdpPort = Number(args['cdp-port']);
|
|
36
|
+
if (!Number.isInteger(cdpPort) || cdpPort <= 0) {
|
|
37
|
+
throw new Error(`Invalid --cdp-port: ${args['cdp-port'] || ''}`);
|
|
38
|
+
}
|
|
39
|
+
for (const key of ['chrome-bin', 'profile', 'extension-dir', 'chrome-log', 'chrome-pid']) {
|
|
40
|
+
if (!args[key]) throw new Error(`Missing --${key}`);
|
|
41
|
+
}
|
|
42
|
+
if (!fs.existsSync(args['chrome-bin'])) throw new Error(`Chrome binary not found: ${args['chrome-bin']}`);
|
|
43
|
+
if (!fs.existsSync(path.join(args['extension-dir'], 'manifest.json'))) {
|
|
44
|
+
throw new Error(`Extension dist manifest not found: ${path.join(args['extension-dir'], 'manifest.json')}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Isolation guard: a standalone launch must use a per-checkout profile, never a
|
|
48
|
+
// shared (e.g. ~/.chrome-farmslot) or the OS-default browser profile. Farm dispatch
|
|
49
|
+
// injects its own already-isolated profile via runtime-context, so this only trips
|
|
50
|
+
// on a genuinely personal/shared path — protecting the user's browser data.
|
|
51
|
+
assertIsolatedProfile(args.profile);
|
|
52
|
+
|
|
53
|
+
fs.mkdirSync(args.profile, { recursive: true });
|
|
54
|
+
fs.mkdirSync(path.dirname(args['chrome-log']), { recursive: true });
|
|
55
|
+
fs.mkdirSync(path.dirname(args['chrome-pid']), { recursive: true });
|
|
56
|
+
|
|
57
|
+
// Ownership guard: only ever take over a CDP endpoint we provably launched. A pid
|
|
58
|
+
// on this port is ours iff its command line loads our --user-data-dir; anything
|
|
59
|
+
// else is a browser this harness did not launch (possibly the user's personal
|
|
60
|
+
// Chrome), so we refuse rather than kill it or attach to it.
|
|
61
|
+
const listenerPids = cdpListenerPids(cdpPort);
|
|
62
|
+
const foreignPids = listenerPids.filter((pid) => !processLoadsProfile(pid, args.profile));
|
|
63
|
+
if (foreignPids.length > 0) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Refusing to launch on CDP port ${cdpPort}: it is held by a browser this harness did not launch (pid ${foreignPids.join(', ')}). ` +
|
|
66
|
+
`Next: pick a free --cdp-port, or stop that browser.`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
const ownedPids = new Set(listenerPids);
|
|
70
|
+
const previousPid = readPidFile(args['chrome-pid']);
|
|
71
|
+
if (previousPid !== null && processLoadsProfile(previousPid, args.profile)) ownedPids.add(previousPid);
|
|
72
|
+
terminatePids([...ownedPids]);
|
|
73
|
+
removeProfileSingletonLocks(args.profile);
|
|
74
|
+
|
|
75
|
+
const logFd = fs.openSync(args['chrome-log'], 'a');
|
|
76
|
+
let child;
|
|
77
|
+
try {
|
|
78
|
+
child = spawn(args['chrome-bin'], [
|
|
79
|
+
`--user-data-dir=${args.profile}`,
|
|
80
|
+
'--remote-debugging-address=127.0.0.1',
|
|
81
|
+
`--remote-debugging-port=${cdpPort}`,
|
|
82
|
+
'--no-first-run',
|
|
83
|
+
'--disable-first-run-ui',
|
|
84
|
+
'--disable-default-apps',
|
|
85
|
+
'--disable-popup-blocking',
|
|
86
|
+
'--disable-extensions-file-access-check',
|
|
87
|
+
'--disable-extensions-content-verification',
|
|
88
|
+
'--disable-features=ExtensionContentVerification,DisableLoadExtensionCommandLineSwitch',
|
|
89
|
+
`--disable-extensions-except=${args['extension-dir']}`,
|
|
90
|
+
`--load-extension=${args['extension-dir']}`,
|
|
91
|
+
'chrome://extensions/',
|
|
92
|
+
], {
|
|
93
|
+
detached: true,
|
|
94
|
+
env: {
|
|
95
|
+
...process.env,
|
|
96
|
+
BUNDLED_DEBUGPY_PATH: undefined,
|
|
97
|
+
PYTHONHOME: undefined,
|
|
98
|
+
PYTHONPATH: undefined,
|
|
99
|
+
DYLD_LIBRARY_PATH: undefined,
|
|
100
|
+
DYLD_FALLBACK_LIBRARY_PATH: undefined,
|
|
101
|
+
DYLD_INSERT_LIBRARIES: undefined,
|
|
102
|
+
},
|
|
103
|
+
stdio: ['ignore', logFd, logFd],
|
|
104
|
+
});
|
|
105
|
+
} finally {
|
|
106
|
+
fs.closeSync(logFd);
|
|
107
|
+
}
|
|
108
|
+
child.unref();
|
|
109
|
+
fs.writeFileSync(args['chrome-pid'], `${child.pid}\n`);
|
|
110
|
+
|
|
111
|
+
function parseArgs(argv) {
|
|
112
|
+
const parsed = {};
|
|
113
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
114
|
+
const arg = argv[i];
|
|
115
|
+
if (!arg.startsWith('--')) throw new Error(`Unknown positional argument: ${arg}`);
|
|
116
|
+
if (i + 1 >= argv.length) throw new Error(`Missing value for ${arg}`);
|
|
117
|
+
parsed[arg.slice(2)] = argv[i + 1];
|
|
118
|
+
i += 1;
|
|
119
|
+
}
|
|
120
|
+
return parsed;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function assertIsolatedProfile(profile) {
|
|
124
|
+
const resolved = path.resolve(profile);
|
|
125
|
+
const home = os.homedir();
|
|
126
|
+
const osDefaults = [
|
|
127
|
+
path.join(home, 'Library/Application Support/Google/Chrome'),
|
|
128
|
+
path.join(home, 'Library/Application Support/Chromium'),
|
|
129
|
+
path.join(home, 'Library/Application Support/Microsoft Edge'),
|
|
130
|
+
path.join(home, '.config/google-chrome'),
|
|
131
|
+
path.join(home, '.config/chromium'),
|
|
132
|
+
path.join(home, '.config/microsoft-edge'),
|
|
133
|
+
];
|
|
134
|
+
const underDefault = (dir) => resolved === dir || resolved.startsWith(`${dir}${path.sep}`);
|
|
135
|
+
if (resolved === home || resolved.includes(`${path.sep}.chrome-farmslot`) || osDefaults.some(underDefault)) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`Refusing to launch: --profile ${resolved} is a shared or default browser profile; ` +
|
|
138
|
+
`standalone launches must use a per-checkout profile under the harness runtime dir.`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Proof of ownership: a process is ours iff its command line loads the exact
|
|
144
|
+
// --user-data-dir we launch with. If the process can't be inspected we treat it
|
|
145
|
+
// as NOT ours (fail safe — never assume ownership of an opaque process).
|
|
146
|
+
function processLoadsProfile(pid, profile) {
|
|
147
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
148
|
+
try {
|
|
149
|
+
const command = execFileSync('ps', ['-ww', '-o', 'command=', '-p', String(pid)], {
|
|
150
|
+
encoding: 'utf8',
|
|
151
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
152
|
+
});
|
|
153
|
+
return command.includes(`--user-data-dir=${profile}`);
|
|
154
|
+
} catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function readPidFile(file) {
|
|
160
|
+
if (!fs.existsSync(file)) return null;
|
|
161
|
+
const pid = Number.parseInt(fs.readFileSync(file, 'utf8').trim(), 10);
|
|
162
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function cdpListenerPids(port) {
|
|
166
|
+
try {
|
|
167
|
+
return execFileSync('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-t'], {
|
|
168
|
+
encoding: 'utf8',
|
|
169
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
170
|
+
})
|
|
171
|
+
.split(/\s+/u)
|
|
172
|
+
.map((value) => Number.parseInt(value, 10))
|
|
173
|
+
.filter((pid) => Number.isInteger(pid) && pid > 0);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (error.status === 1) return [];
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function terminatePids(pids) {
|
|
181
|
+
const unique = [...new Set(pids)].filter((pid) => pid !== process.pid);
|
|
182
|
+
for (const pid of unique) signalPid(pid, 'SIGTERM');
|
|
183
|
+
for (let i = 0; i < 10 && unique.some(pidAlive); i += 1) {
|
|
184
|
+
spawnSync('sleep', ['0.2']);
|
|
185
|
+
}
|
|
186
|
+
for (const pid of unique) {
|
|
187
|
+
if (pidAlive(pid)) signalPid(pid, 'SIGKILL');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function signalPid(pid, signal) {
|
|
192
|
+
try {
|
|
193
|
+
process.kill(pid, signal);
|
|
194
|
+
return true;
|
|
195
|
+
} catch (error) {
|
|
196
|
+
if (error.code === 'ESRCH') return false;
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function pidAlive(pid) {
|
|
202
|
+
try {
|
|
203
|
+
process.kill(pid, 0);
|
|
204
|
+
return true;
|
|
205
|
+
} catch (error) {
|
|
206
|
+
if (error.code === 'ESRCH') return false;
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function removeProfileSingletonLocks(profile) {
|
|
212
|
+
for (const name of ['SingletonLock', 'SingletonSocket', 'SingletonCookie']) {
|
|
213
|
+
const file = path.join(profile, name);
|
|
214
|
+
if (fs.existsSync(file)) fs.rmSync(file, { force: true });
|
|
215
|
+
}
|
|
216
|
+
}
|