@deeeed/metamask-harness 0.41.1 → 0.43.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 +35 -0
- package/README.md +12 -0
- package/adapters/manifest.json +8 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +108 -10
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +117 -16
- package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +2 -0
- package/adapters/mobile/bridge-runtime/lib/cdp-broker.cjs +921 -0
- package/adapters/mobile/bridge-runtime/lib/config.cjs +14 -4
- package/adapters/mobile/bridge-runtime/lib/devtools-proxy.cjs +177 -0
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +13 -3
- package/adapters/mobile/reload-app.mjs +67 -0
- package/adapters/mobile/start-console-forwarder.sh +22 -3
- package/adapters/mobile/start-metro.sh +6 -11
- package/adapters/mobile/stop-metro.sh +10 -1
- package/adapters/shared/open-debug.mjs +172 -2
- package/dist/adapters/extension/browser-cdp.js +174 -0
- package/dist/adapters/extension/network-observer.js +209 -0
- package/dist/adapters/extension/performance-observer.js +75 -0
- package/dist/adapters/mobile/frame-metrics.js +45 -0
- package/dist/adapters/mobile/metro-env.js +0 -5
- package/dist/adapters/mobile/performance-observer.js +43 -0
- package/dist/adapters/mobile/prepare.js +1 -3
- package/dist/adapters/mobile/runtime-decision.js +6 -9
- package/dist/adapters/performance/cdp-trace.js +342 -0
- package/dist/adapters/performance/js-task-metrics.js +35 -0
- package/dist/adapters.js +29 -1
- package/dist/artifact-files.js +92 -0
- package/dist/async.js +19 -0
- package/dist/cli-commands.js +6 -3
- package/dist/cli.js +4 -0
- package/dist/command-contract.js +3 -0
- package/dist/commands/call.js +66 -20
- package/dist/commands/reload.js +80 -0
- package/dist/commands/run-engine.js +18 -0
- package/dist/commands/run.js +68 -22
- package/dist/mm-harness-cli.js +17 -1
- package/dist/network-observation.js +283 -0
- package/dist/performance-observation.js +465 -0
- package/docs/NETWORK-CAPTURE.md +98 -0
- package/docs/PERFORMANCE-CAPTURE.md +33 -0
- package/docs/RECIPES.md +17 -0
- package/library/actions/mobile/app/network_assert.mjs +14 -0
- package/library/actions/mobile/app/network_capture.mjs +72 -0
- package/library/actions/mobile/platform/bridge.mjs +10 -2
- package/library/actions/shared/app/network-artifact.mjs +10 -0
- package/library/actions/shared/app/network-assert.mjs +154 -0
- package/library/manifests/extension.action-manifest.json +173 -0
- package/library/manifests/mobile.action-manifest.json +204 -0
- package/library/recipes/mobile/perps/performance.recipe.json +11 -11
- package/package.json +1 -1
- package/scripts/completions.sh +2 -1
- package/scripts/site-contrast.mjs +43 -27
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.43.0 - 2026-08-21
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Add platform-neutral `app.performance_capture` and `app.performance_assert` actions that attribute Mobile or Extension CDP trace samples to exact recipe-node boundaries and emit JSON plus self-contained HTML evidence.
|
|
10
|
+
- Mobile native UI FPS requires a development client that reports `unstable_frameRecordingEnabled: true`; unpatched clients report partial or unavailable native coverage. Extension uses Chromium CDP and needs no client patch.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Bound performance capture duration, discard raw samples after node attribution, correct iOS frame completion timestamps, fail closed on unresolved Extension renderers, and reset CDP collector state after trace failures.
|
|
15
|
+
- Scope Mobile Hermes broker ownership by checkout runtime and Metro port so a forwarder from a prior port cannot intercept bridge, HUD, or observation commands after a slot port change.
|
|
16
|
+
- Preserve explicit Mobile `--device` pins when slot defaults load so HUD and live-adapter child processes cannot target another device.
|
|
17
|
+
- Resume broker commands on the next Hermes session generation without losing the pinned logical device.
|
|
18
|
+
- Keep Hermes session recovery inside the caller's RPC deadline, pass only the remaining time to the recovered command, and classify broker connection or request timeouts so transient HUD updates do not abort a recipe.
|
|
19
|
+
- Accept React Native trace response chunks up to 16 MiB while keeping broker requests capped at 8 MiB, and never retry the non-idempotent `Tracing.end` command.
|
|
20
|
+
- Limit Mobile UI captures to clock and native-frame categories; keep browser timeline and JavaScript task categories on Extension.
|
|
21
|
+
|
|
22
|
+
## 0.42.0 - 2026-08-20
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- Add `app.network_capture` and `app.network_assert` for redacted Mobile HTTP request windows, indexed evidence, and assertions that preserve failed-run diagnostics.
|
|
27
|
+
- Support the same bounded network observation contract on Extension and automatically index safe request metadata with recipe-node boundaries for live Mobile and Extension runs.
|
|
28
|
+
- Add `mm-harness reload` for Metro-equivalent Mobile reloads and in-place Extension CDP refreshes.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- Route Mobile console forwarding and recipe bridge commands through the same debugger connection so Network capture can coexist with HUD and wallet/domain actions.
|
|
33
|
+
- Make the canonical Perps performance recipe close positions and orders outside timing for its market-only cohort instead of failing on inherited account state.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- Pass Expo `--clear` only for an explicit `mm-harness launch --clear-metro`, bound default Metro concurrency to four workers, and remove a stopped checkout from Watchman's root set.
|
|
38
|
+
- Route React Native DevTools through the harness CDP broker so debugger use, recipe actions, and app reloads no longer evict one another.
|
|
39
|
+
|
|
5
40
|
## 0.41.1 - 2026-08-19
|
|
6
41
|
|
|
7
42
|
### Fixed
|
package/README.md
CHANGED
|
@@ -87,6 +87,7 @@ mm-harness status
|
|
|
87
87
|
mm-harness stop
|
|
88
88
|
mm-harness logs
|
|
89
89
|
mm-harness debug
|
|
90
|
+
mm-harness reload # Mobile Metro reload / Extension CDP refresh
|
|
90
91
|
mm-harness fixtures set
|
|
91
92
|
|
|
92
93
|
# Delete the current wallet, then reapply the canonical fixture from clean state.
|
|
@@ -109,6 +110,17 @@ mm-harness logs --source app # Mobile app
|
|
|
109
110
|
mm-harness logs --source metro # Mobile bundler
|
|
110
111
|
```
|
|
111
112
|
|
|
113
|
+
Live Mobile and Extension recipes automatically index a bounded, redacted
|
|
114
|
+
`network/run-summary.json` with request timing and recipe-node boundaries. Use
|
|
115
|
+
explicit `app.network_capture` and `app.network_assert` nodes for filtered
|
|
116
|
+
windows and machine-checked request expectations. See
|
|
117
|
+
[Recipe-scoped network capture](docs/NETWORK-CAPTURE.md).
|
|
118
|
+
|
|
119
|
+
Mobile and Extension recipes can bracket explicit UI-smoothness windows with
|
|
120
|
+
`app.performance_capture` and verify their evidence with
|
|
121
|
+
`app.performance_assert`. Capture is never automatic. See
|
|
122
|
+
[UI smoothness capture](docs/PERFORMANCE-CAPTURE.md).
|
|
123
|
+
|
|
112
124
|
## Discover and prove
|
|
113
125
|
|
|
114
126
|
```bash
|
package/adapters/manifest.json
CHANGED
|
@@ -74,6 +74,14 @@
|
|
|
74
74
|
"inputs": "--target --port; env WATCHER_PORT",
|
|
75
75
|
"outputs": "progress on stderr; exit 0 stopped-or-nothing / 1 stop failed / 2 bad args"
|
|
76
76
|
},
|
|
77
|
+
{
|
|
78
|
+
"id": "mobile/reload-app",
|
|
79
|
+
"entry": "adapters/mobile/reload-app.mjs",
|
|
80
|
+
"kind": "node",
|
|
81
|
+
"purpose": "Request an in-place React Native app reload through Metro's protocol-v2 message endpoint.",
|
|
82
|
+
"inputs": "--port --json; env WATCHER_PORT, METRO_PORT",
|
|
83
|
+
"outputs": "reload result on stdout; exit 0 requested / 1 unavailable or invalid input"
|
|
84
|
+
},
|
|
77
85
|
{
|
|
78
86
|
"id": "mobile/prewarm-bundle",
|
|
79
87
|
"entry": "adapters/mobile/prewarm-bundle.sh",
|
|
@@ -19,8 +19,15 @@
|
|
|
19
19
|
const fs = require('node:fs');
|
|
20
20
|
const path = require('node:path');
|
|
21
21
|
const { loadPort } = require('./lib/config.cjs');
|
|
22
|
-
const {
|
|
22
|
+
const {
|
|
23
|
+
discoverTarget,
|
|
24
|
+
} = require('./lib/target-discovery.cjs');
|
|
23
25
|
const { createWSClient } = require('./lib/ws-client.cjs');
|
|
26
|
+
const {
|
|
27
|
+
brokerSocketPath,
|
|
28
|
+
createBrokerClient,
|
|
29
|
+
deviceIdFromUrl,
|
|
30
|
+
} = require('./lib/cdp-broker.cjs');
|
|
24
31
|
const {
|
|
25
32
|
EXIT_CODE_BY_ERROR_CODE,
|
|
26
33
|
TEACHING_BY_ERROR_CODE,
|
|
@@ -924,6 +931,22 @@ const COMMANDS = {
|
|
|
924
931
|
return result || { count: 0, entries: [] };
|
|
925
932
|
},
|
|
926
933
|
|
|
934
|
+
async 'network-capture-start'(client, args) {
|
|
935
|
+
if (typeof client.control !== 'function') {
|
|
936
|
+
throw new Error('Network capture requires the CDP broker');
|
|
937
|
+
}
|
|
938
|
+
const options = JSON.parse(args[0] || '{}');
|
|
939
|
+
return client.control('capture-start', options);
|
|
940
|
+
},
|
|
941
|
+
|
|
942
|
+
async 'network-capture-end'(client, args) {
|
|
943
|
+
if (typeof client.control !== 'function') {
|
|
944
|
+
throw new Error('Network capture requires the CDP broker');
|
|
945
|
+
}
|
|
946
|
+
const options = JSON.parse(args[0] || '{}');
|
|
947
|
+
return client.control('capture-end', options);
|
|
948
|
+
},
|
|
949
|
+
|
|
927
950
|
};
|
|
928
951
|
|
|
929
952
|
// ---------------------------------------------------------------------------
|
|
@@ -1019,6 +1042,8 @@ Commands:
|
|
|
1019
1042
|
issues-arm Install console/exception hooks that
|
|
1020
1043
|
populate globalThis.__AGENTIC_ISSUES__
|
|
1021
1044
|
issues-collect Snapshot + clear the in-app issue buffer
|
|
1045
|
+
network-capture-start <json> Start a broker-owned Network capture
|
|
1046
|
+
network-capture-end <json> End capture and return redacted summary
|
|
1022
1047
|
|
|
1023
1048
|
Environment:
|
|
1024
1049
|
WATCHER_PORT Metro port (default: 8081)
|
|
@@ -1035,24 +1060,94 @@ Environment:
|
|
|
1035
1060
|
process.exit(1);
|
|
1036
1061
|
}
|
|
1037
1062
|
|
|
1038
|
-
// Only a real command takes the debugger slot; help/unknown paths above never
|
|
1039
|
-
// suppress the console forwarder.
|
|
1040
|
-
acquireBridgeLock();
|
|
1041
|
-
|
|
1042
1063
|
const port = loadPort();
|
|
1064
|
+
const brokerSocketFile = brokerSocketPath(
|
|
1065
|
+
path.dirname(BRIDGE_LOCK_FILE),
|
|
1066
|
+
port,
|
|
1067
|
+
);
|
|
1043
1068
|
const timeout = Number.parseInt(process.env.CDP_TIMEOUT || '5000', 10);
|
|
1069
|
+
const brokerAvailable = fs.existsSync(brokerSocketFile);
|
|
1070
|
+
if (!brokerAvailable) acquireBridgeLock();
|
|
1071
|
+
|
|
1072
|
+
function selectedTargetPin() {
|
|
1073
|
+
const platform = String(process.env.MM_HARNESS_EXPLICIT_PLATFORM || '').toLowerCase();
|
|
1074
|
+
const androidPin =
|
|
1075
|
+
process.env.ANDROID_TARGET_DEVICE_NAME || process.env.ANDROID_DEVICE || '';
|
|
1076
|
+
const iosPin = process.env.IOS_SIMULATOR || '';
|
|
1077
|
+
return String(
|
|
1078
|
+
platform === 'android'
|
|
1079
|
+
? androidPin
|
|
1080
|
+
: platform === 'ios'
|
|
1081
|
+
? iosPin
|
|
1082
|
+
: androidPin || iosPin,
|
|
1083
|
+
).trim();
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
async function brokerTargets({ retainIdentity = false } = {}) {
|
|
1087
|
+
const discoveryClient = await createBrokerClient(
|
|
1088
|
+
brokerSocketFile,
|
|
1089
|
+
'',
|
|
1090
|
+
timeout,
|
|
1091
|
+
);
|
|
1092
|
+
try {
|
|
1093
|
+
const targets = await discoveryClient.control(
|
|
1094
|
+
retainIdentity ? 'resolve-targets' : 'list-targets',
|
|
1095
|
+
retainIdentity ? { nameIncludes: selectedTargetPin() } : {},
|
|
1096
|
+
timeout,
|
|
1097
|
+
);
|
|
1098
|
+
return (Array.isArray(targets) ? targets : []).flatMap((target) => {
|
|
1099
|
+
const deviceId = String(target?.deviceId || '');
|
|
1100
|
+
return deviceId
|
|
1101
|
+
? [{ wsUrl: deviceId, deviceName: String(target?.name || '') }]
|
|
1102
|
+
: [];
|
|
1103
|
+
});
|
|
1104
|
+
} finally {
|
|
1105
|
+
discoveryClient.close();
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function selectBrokerTarget(targets) {
|
|
1110
|
+
const pin = selectedTargetPin();
|
|
1111
|
+
const candidates = pin
|
|
1112
|
+
? targets.filter((target) =>
|
|
1113
|
+
target.deviceName.toLowerCase().includes(pin.toLowerCase()),
|
|
1114
|
+
)
|
|
1115
|
+
: targets;
|
|
1116
|
+
if (candidates.length !== 1) {
|
|
1117
|
+
throw new Error(
|
|
1118
|
+
`Mobile CDP broker target selection requires one target; found ${candidates.length}${pin ? ` for ${JSON.stringify(pin)}` : ''}.`,
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
return candidates[0];
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
async function clientFor(wsUrl) {
|
|
1125
|
+
if (brokerAvailable) {
|
|
1126
|
+
return createBrokerClient(
|
|
1127
|
+
brokerSocketFile,
|
|
1128
|
+
deviceIdFromUrl(wsUrl),
|
|
1129
|
+
timeout,
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
if (command.startsWith('network-capture-')) {
|
|
1133
|
+
throw new Error('Network capture requires a running CDP broker');
|
|
1134
|
+
}
|
|
1135
|
+
acquireBridgeLock();
|
|
1136
|
+
return createWSClient(wsUrl, timeout);
|
|
1137
|
+
}
|
|
1044
1138
|
|
|
1045
1139
|
// `status` probes ALL connected targets so both platforms are visible.
|
|
1046
1140
|
// `status-selected` falls through to the normal pinned discovery path for
|
|
1047
1141
|
// action code that must survive one device's Hermes runtime rotation.
|
|
1048
1142
|
if (command === 'status') {
|
|
1049
|
-
const
|
|
1050
|
-
|
|
1143
|
+
const allTargets = brokerAvailable
|
|
1144
|
+
? await brokerTargets()
|
|
1145
|
+
: await require('./lib/target-discovery.cjs').discoverAllTargets(port);
|
|
1051
1146
|
const results = [];
|
|
1052
1147
|
for (const target of allTargets) {
|
|
1053
1148
|
let client;
|
|
1054
1149
|
try {
|
|
1055
|
-
client = await
|
|
1150
|
+
client = await clientFor(target.wsUrl);
|
|
1056
1151
|
const platform = await cdpEval(client, 'globalThis.__AGENTIC__?.platform') || '';
|
|
1057
1152
|
const result = await handler(client, args.slice(1), { deviceName: target.deviceName, platform });
|
|
1058
1153
|
results.push(result);
|
|
@@ -1068,8 +1163,11 @@ Environment:
|
|
|
1068
1163
|
return;
|
|
1069
1164
|
}
|
|
1070
1165
|
|
|
1071
|
-
const { wsUrl, deviceName } =
|
|
1072
|
-
|
|
1166
|
+
const { wsUrl, deviceName } = brokerAvailable
|
|
1167
|
+
? selectBrokerTarget(await brokerTargets({ retainIdentity: true }))
|
|
1168
|
+
: await discoverTarget(port, { probe: true });
|
|
1169
|
+
if (!wsUrl) throw new Error('No broker-owned Hermes target is available');
|
|
1170
|
+
const client = await clientFor(wsUrl);
|
|
1073
1171
|
|
|
1074
1172
|
try {
|
|
1075
1173
|
// Detect platform from the running app (exposed by __AGENTIC__ bridge as Platform.OS)
|
|
@@ -29,9 +29,16 @@ const http = require('node:http');
|
|
|
29
29
|
const path = require('node:path');
|
|
30
30
|
const { rankRuntimeCandidates } = require('./lib/target-discovery.cjs');
|
|
31
31
|
const { formatArgs: formatConsoleArgs } = require('./lib/console-format.cjs');
|
|
32
|
+
const {
|
|
33
|
+
brokerSocketPath,
|
|
34
|
+
createCdpBroker,
|
|
35
|
+
deviceIdFromUrl,
|
|
36
|
+
} = require('./lib/cdp-broker.cjs');
|
|
37
|
+
const { createDevtoolsProxy } = require('./lib/devtools-proxy.cjs');
|
|
38
|
+
const { resolvePort } = require('./lib/config.cjs');
|
|
32
39
|
|
|
33
|
-
//
|
|
34
|
-
//
|
|
40
|
+
// Hermes uses Node's built-in WebSocket client. The local DevTools proxy uses
|
|
41
|
+
// the package's pinned `ws` server implementation.
|
|
35
42
|
const HANDSHAKE_TIMEOUT_MS = 3000;
|
|
36
43
|
|
|
37
44
|
const DISCOVER_ACTIVE_MS = 1000; // a device is unattached — look for it quickly
|
|
@@ -40,7 +47,7 @@ const FLUSH_MS = 100;
|
|
|
40
47
|
const MAX_LINE_CHARS = 4000;
|
|
41
48
|
|
|
42
49
|
function parseArgs(argv) {
|
|
43
|
-
const args = { port:
|
|
50
|
+
const args = { port: resolvePort(), out: null };
|
|
44
51
|
for (let i = 2; i < argv.length; i += 1) {
|
|
45
52
|
if (argv[i] === '--port') args.port = argv[++i];
|
|
46
53
|
else if (argv[i] === '--out') args.out = argv[++i];
|
|
@@ -101,9 +108,10 @@ function bridgeLockActive() {
|
|
|
101
108
|
}
|
|
102
109
|
|
|
103
110
|
function yieldSessions() {
|
|
104
|
-
for (const
|
|
111
|
+
for (const session of sessions.values()) {
|
|
112
|
+
broker?.onSessionClose(session.deviceId);
|
|
105
113
|
try {
|
|
106
|
-
ws.close();
|
|
114
|
+
session.ws.close();
|
|
107
115
|
} catch {}
|
|
108
116
|
}
|
|
109
117
|
sessions.clear();
|
|
@@ -170,8 +178,56 @@ function serializeState() {
|
|
|
170
178
|
return JSON.stringify(state);
|
|
171
179
|
}
|
|
172
180
|
|
|
173
|
-
/** deviceId -> live WebSocket session. */
|
|
181
|
+
/** deviceId -> live WebSocket session state. */
|
|
174
182
|
const sessions = new Map();
|
|
183
|
+
let broker = null;
|
|
184
|
+
let devtoolsProxy = null;
|
|
185
|
+
|
|
186
|
+
function sendCommand(session, method, params = {}, timeoutMs = 10_000) {
|
|
187
|
+
return new Promise((resolve, reject) => {
|
|
188
|
+
if (!session.opened) {
|
|
189
|
+
reject(new Error('CDP session is not open'));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const id = ++session.nextId;
|
|
193
|
+
const timer = setTimeout(() => {
|
|
194
|
+
session.pending.delete(id);
|
|
195
|
+
reject(new Error(`CDP command timed out: ${method}`));
|
|
196
|
+
}, timeoutMs);
|
|
197
|
+
session.pending.set(id, {
|
|
198
|
+
resolve: (result) => {
|
|
199
|
+
clearTimeout(timer);
|
|
200
|
+
resolve(result);
|
|
201
|
+
},
|
|
202
|
+
reject: (error) => {
|
|
203
|
+
clearTimeout(timer);
|
|
204
|
+
reject(error);
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
session.ws.send(JSON.stringify({ id, method, params }));
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
broker = createCdpBroker({
|
|
212
|
+
socketPath: brokerSocketPath(path.dirname(out), port),
|
|
213
|
+
sessions,
|
|
214
|
+
sendCommand,
|
|
215
|
+
requestDiscovery(deviceId) {
|
|
216
|
+
foreignDebuggerUntil.delete(deviceId);
|
|
217
|
+
discover();
|
|
218
|
+
},
|
|
219
|
+
onClientActivity: noteBridgeCoordination,
|
|
220
|
+
});
|
|
221
|
+
devtoolsProxy = createDevtoolsProxy({
|
|
222
|
+
descriptorPath: path.join(path.dirname(out), 'devtools-proxy.json'),
|
|
223
|
+
sessions,
|
|
224
|
+
sendCommand,
|
|
225
|
+
requestDiscovery: discover,
|
|
226
|
+
allowedOrigins: [
|
|
227
|
+
`http://127.0.0.1:${port}`,
|
|
228
|
+
`http://localhost:${port}`,
|
|
229
|
+
],
|
|
230
|
+
});
|
|
175
231
|
|
|
176
232
|
/** deviceId -> epoch ms until which a foreign debugger owns the slot. */
|
|
177
233
|
const foreignDebuggerUntil = new Map();
|
|
@@ -226,11 +282,6 @@ function deviceNameFromTitle(title) {
|
|
|
226
282
|
return m ? m[1] : (title || 'device');
|
|
227
283
|
}
|
|
228
284
|
|
|
229
|
-
function deviceIdFromUrl(wsUrl) {
|
|
230
|
-
const m = /[?&]device=([^&]+)/.exec(wsUrl || '');
|
|
231
|
-
return m ? m[1] : wsUrl;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
285
|
function connect(target) {
|
|
235
286
|
const deviceId = deviceIdFromUrl(target.webSocketDebuggerUrl);
|
|
236
287
|
const name = deviceNameFromTitle(target.title);
|
|
@@ -240,7 +291,16 @@ function connect(target) {
|
|
|
240
291
|
} catch {
|
|
241
292
|
return;
|
|
242
293
|
}
|
|
243
|
-
|
|
294
|
+
const session = {
|
|
295
|
+
deviceId,
|
|
296
|
+
name,
|
|
297
|
+
ws,
|
|
298
|
+
opened: false,
|
|
299
|
+
brokerReady: false,
|
|
300
|
+
nextId: 1,
|
|
301
|
+
pending: new Map(),
|
|
302
|
+
};
|
|
303
|
+
sessions.set(deviceId, session);
|
|
244
304
|
// The built-in WebSocket has no handshake timeout; a hung connect would hold
|
|
245
305
|
// the session slot forever and block every future re-attach for this device.
|
|
246
306
|
const handshakeTimer = setTimeout(() => {
|
|
@@ -248,10 +308,29 @@ function connect(target) {
|
|
|
248
308
|
ws.close();
|
|
249
309
|
} catch {}
|
|
250
310
|
}, HANDSHAKE_TIMEOUT_MS);
|
|
251
|
-
ws.addEventListener('open', () => {
|
|
311
|
+
ws.addEventListener('open', async () => {
|
|
252
312
|
clearTimeout(handshakeTimer);
|
|
253
|
-
|
|
254
|
-
|
|
313
|
+
session.opened = true;
|
|
314
|
+
try {
|
|
315
|
+
await sendCommand(session, 'Runtime.enable');
|
|
316
|
+
const evaluation = await sendCommand(session, 'Runtime.evaluate', {
|
|
317
|
+
expression: "typeof globalThis.__AGENTIC__ === 'object'",
|
|
318
|
+
returnByValue: true,
|
|
319
|
+
awaitPromise: false,
|
|
320
|
+
});
|
|
321
|
+
if (evaluation?.result?.value !== true) {
|
|
322
|
+
throw new Error('CDP target does not expose __AGENTIC__');
|
|
323
|
+
}
|
|
324
|
+
session.brokerReady = true;
|
|
325
|
+
broker.onSessionOpen(deviceId);
|
|
326
|
+
devtoolsProxy.onSessionOpen(deviceId, session);
|
|
327
|
+
process.stderr.write(`console-forwarder: attached ${name}\n`);
|
|
328
|
+
} catch (error) {
|
|
329
|
+
process.stderr.write(
|
|
330
|
+
`console-forwarder: rejected ${name}: ${String(error?.message || error).slice(0, 256)}\n`,
|
|
331
|
+
);
|
|
332
|
+
ws.close();
|
|
333
|
+
}
|
|
255
334
|
});
|
|
256
335
|
ws.addEventListener('message', (event) => {
|
|
257
336
|
let msg;
|
|
@@ -260,6 +339,17 @@ function connect(target) {
|
|
|
260
339
|
} catch {
|
|
261
340
|
return;
|
|
262
341
|
}
|
|
342
|
+
if (msg.id && session.pending.has(msg.id)) {
|
|
343
|
+
const entry = session.pending.get(msg.id);
|
|
344
|
+
session.pending.delete(msg.id);
|
|
345
|
+
if (msg.error) entry.reject(new Error(JSON.stringify(msg.error)));
|
|
346
|
+
else entry.resolve(msg.result);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (msg.method) {
|
|
350
|
+
broker.onCdpEvent(deviceId, msg.method, msg.params || {});
|
|
351
|
+
devtoolsProxy.onCdpEvent(deviceId, msg.method, msg.params || {});
|
|
352
|
+
}
|
|
263
353
|
if (msg.method !== 'Runtime.consoleAPICalled') return;
|
|
264
354
|
const text = formatArgs(msg.params.args);
|
|
265
355
|
// dev-middleware emits this NOTE on every debugger attach (i.e. ours). Drop it.
|
|
@@ -278,8 +368,15 @@ function connect(target) {
|
|
|
278
368
|
});
|
|
279
369
|
const drop = () => {
|
|
280
370
|
clearTimeout(handshakeTimer);
|
|
281
|
-
|
|
371
|
+
session.opened = false;
|
|
372
|
+
session.brokerReady = false;
|
|
373
|
+
for (const entry of session.pending.values()) {
|
|
374
|
+
entry.reject(new Error('CDP session closed'));
|
|
375
|
+
}
|
|
376
|
+
session.pending.clear();
|
|
377
|
+
if (sessions.get(deviceId) === session) {
|
|
282
378
|
sessions.delete(deviceId);
|
|
379
|
+
broker.onSessionClose(deviceId);
|
|
283
380
|
process.stderr.write(`console-forwarder: detached ${name}; will re-attach\n`);
|
|
284
381
|
}
|
|
285
382
|
};
|
|
@@ -351,10 +448,14 @@ function schedule(ms) {
|
|
|
351
448
|
|
|
352
449
|
process.on('SIGTERM', () => {
|
|
353
450
|
flushSync();
|
|
451
|
+
devtoolsProxy.close();
|
|
452
|
+
broker.close();
|
|
354
453
|
process.exit(0);
|
|
355
454
|
});
|
|
356
455
|
process.on('SIGINT', () => {
|
|
357
456
|
flushSync();
|
|
457
|
+
devtoolsProxy.close();
|
|
458
|
+
broker.close();
|
|
358
459
|
process.exit(0);
|
|
359
460
|
});
|
|
360
461
|
|
|
@@ -63,6 +63,8 @@ const NEEDLE_CODES = [
|
|
|
63
63
|
['websocket error', BRIDGE_ERROR_CODES.WS_CLOSED],
|
|
64
64
|
['cdp connection timeout', BRIDGE_ERROR_CODES.CDP_TIMEOUT],
|
|
65
65
|
['cdp message timeout', BRIDGE_ERROR_CODES.CDP_TIMEOUT],
|
|
66
|
+
['cdp broker connection timeout', BRIDGE_ERROR_CODES.CDP_TIMEOUT],
|
|
67
|
+
['cdp broker request timeout', BRIDGE_ERROR_CODES.CDP_TIMEOUT],
|
|
66
68
|
['evaluation timed out', BRIDGE_ERROR_CODES.CDP_TIMEOUT],
|
|
67
69
|
['timed out', BRIDGE_ERROR_CODES.CDP_TIMEOUT],
|
|
68
70
|
];
|