@deeeed/metamask-harness 0.42.0 → 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 +17 -0
- package/README.md +5 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +59 -21
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +3 -2
- package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +2 -0
- package/adapters/mobile/bridge-runtime/lib/cdp-broker.cjs +194 -25
- package/adapters/mobile/bridge-runtime/lib/config.cjs +14 -4
- package/adapters/mobile/start-console-forwarder.sh +5 -1
- package/dist/adapters/extension/browser-cdp.js +174 -0
- package/dist/adapters/extension/network-observer.js +19 -110
- package/dist/adapters/extension/performance-observer.js +75 -0
- package/dist/adapters/mobile/frame-metrics.js +45 -0
- package/dist/adapters/mobile/performance-observer.js +43 -0
- package/dist/adapters/performance/cdp-trace.js +342 -0
- package/dist/adapters/performance/js-task-metrics.js +35 -0
- package/dist/adapters.js +17 -2
- package/dist/artifact-files.js +92 -0
- package/dist/async.js +19 -0
- package/dist/commands/call.js +23 -2
- package/dist/commands/run-engine.js +18 -0
- package/dist/commands/run.js +20 -1
- package/dist/network-observation.js +59 -47
- package/dist/performance-observation.js +465 -0
- package/docs/PERFORMANCE-CAPTURE.md +33 -0
- package/docs/RECIPES.md +7 -0
- package/library/actions/mobile/platform/bridge.mjs +3 -0
- package/library/manifests/extension.action-manifest.json +85 -0
- package/library/manifests/mobile.action-manifest.json +97 -0
- package/package.json +1 -1
- package/scripts/site-contrast.mjs +43 -27
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
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
|
+
|
|
5
22
|
## 0.42.0 - 2026-08-20
|
|
6
23
|
|
|
7
24
|
### Added
|
package/README.md
CHANGED
|
@@ -116,6 +116,11 @@ explicit `app.network_capture` and `app.network_assert` nodes for filtered
|
|
|
116
116
|
windows and machine-checked request expectations. See
|
|
117
117
|
[Recipe-scoped network capture](docs/NETWORK-CAPTURE.md).
|
|
118
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
|
+
|
|
119
124
|
## Discover and prove
|
|
120
125
|
|
|
121
126
|
```bash
|
|
@@ -21,7 +21,6 @@ const path = require('node:path');
|
|
|
21
21
|
const { loadPort } = require('./lib/config.cjs');
|
|
22
22
|
const {
|
|
23
23
|
discoverTarget,
|
|
24
|
-
rankRuntimeCandidates,
|
|
25
24
|
} = require('./lib/target-discovery.cjs');
|
|
26
25
|
const { createWSClient } = require('./lib/ws-client.cjs');
|
|
27
26
|
const {
|
|
@@ -961,7 +960,6 @@ const BRIDGE_LOCK_FILE = path.join(
|
|
|
961
960
|
process.env.RECIPE_RUNTIME_DIR || path.join('temp', 'recipe', 'runtime'),
|
|
962
961
|
'cdp-bridge.lock',
|
|
963
962
|
);
|
|
964
|
-
const BROKER_SOCKET_FILE = brokerSocketPath(path.dirname(BRIDGE_LOCK_FILE));
|
|
965
963
|
const BRIDGE_LOCK_OWNER_PID = /^\d+$/.test(
|
|
966
964
|
process.env.CDP_BRIDGE_LOCK_OWNER_PID || '',
|
|
967
965
|
)
|
|
@@ -1063,30 +1061,70 @@ Environment:
|
|
|
1063
1061
|
}
|
|
1064
1062
|
|
|
1065
1063
|
const port = loadPort();
|
|
1064
|
+
const brokerSocketFile = brokerSocketPath(
|
|
1065
|
+
path.dirname(BRIDGE_LOCK_FILE),
|
|
1066
|
+
port,
|
|
1067
|
+
);
|
|
1066
1068
|
const timeout = Number.parseInt(process.env.CDP_TIMEOUT || '5000', 10);
|
|
1067
|
-
const brokerAvailable = fs.existsSync(
|
|
1069
|
+
const brokerAvailable = fs.existsSync(brokerSocketFile);
|
|
1068
1070
|
if (!brokerAvailable) acquireBridgeLock();
|
|
1069
1071
|
|
|
1070
|
-
|
|
1071
|
-
const
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
const
|
|
1075
|
-
return
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
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];
|
|
1084
1122
|
}
|
|
1085
1123
|
|
|
1086
1124
|
async function clientFor(wsUrl) {
|
|
1087
1125
|
if (brokerAvailable) {
|
|
1088
1126
|
return createBrokerClient(
|
|
1089
|
-
|
|
1127
|
+
brokerSocketFile,
|
|
1090
1128
|
deviceIdFromUrl(wsUrl),
|
|
1091
1129
|
timeout,
|
|
1092
1130
|
);
|
|
@@ -1125,9 +1163,9 @@ Environment:
|
|
|
1125
1163
|
return;
|
|
1126
1164
|
}
|
|
1127
1165
|
|
|
1128
|
-
const { wsUrl, deviceName } =
|
|
1129
|
-
|
|
1130
|
-
|
|
1166
|
+
const { wsUrl, deviceName } = brokerAvailable
|
|
1167
|
+
? selectBrokerTarget(await brokerTargets({ retainIdentity: true }))
|
|
1168
|
+
: await discoverTarget(port, { probe: true });
|
|
1131
1169
|
if (!wsUrl) throw new Error('No broker-owned Hermes target is available');
|
|
1132
1170
|
const client = await clientFor(wsUrl);
|
|
1133
1171
|
|
|
@@ -35,6 +35,7 @@ const {
|
|
|
35
35
|
deviceIdFromUrl,
|
|
36
36
|
} = require('./lib/cdp-broker.cjs');
|
|
37
37
|
const { createDevtoolsProxy } = require('./lib/devtools-proxy.cjs');
|
|
38
|
+
const { resolvePort } = require('./lib/config.cjs');
|
|
38
39
|
|
|
39
40
|
// Hermes uses Node's built-in WebSocket client. The local DevTools proxy uses
|
|
40
41
|
// the package's pinned `ws` server implementation.
|
|
@@ -46,7 +47,7 @@ const FLUSH_MS = 100;
|
|
|
46
47
|
const MAX_LINE_CHARS = 4000;
|
|
47
48
|
|
|
48
49
|
function parseArgs(argv) {
|
|
49
|
-
const args = { port:
|
|
50
|
+
const args = { port: resolvePort(), out: null };
|
|
50
51
|
for (let i = 2; i < argv.length; i += 1) {
|
|
51
52
|
if (argv[i] === '--port') args.port = argv[++i];
|
|
52
53
|
else if (argv[i] === '--out') args.out = argv[++i];
|
|
@@ -208,7 +209,7 @@ function sendCommand(session, method, params = {}, timeoutMs = 10_000) {
|
|
|
208
209
|
}
|
|
209
210
|
|
|
210
211
|
broker = createCdpBroker({
|
|
211
|
-
socketPath: brokerSocketPath(path.dirname(out)),
|
|
212
|
+
socketPath: brokerSocketPath(path.dirname(out), port),
|
|
212
213
|
sessions,
|
|
213
214
|
sendCommand,
|
|
214
215
|
requestDiscovery(deviceId) {
|
|
@@ -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
|
];
|
|
@@ -12,7 +12,8 @@ const MAX_ACTIVE_CAPTURES = 16;
|
|
|
12
12
|
const MAX_COMPLETED_CAPTURES = 16;
|
|
13
13
|
const MAX_CAPTURE_BYTES = 4 * 1024 * 1024;
|
|
14
14
|
const MAX_POST_DATA_BYTES = 64 * 1024;
|
|
15
|
-
const
|
|
15
|
+
const MAX_REQUEST_FRAME_BYTES = 8 * 1024 * 1024;
|
|
16
|
+
const MAX_RESPONSE_FRAME_BYTES = 16 * 1024 * 1024;
|
|
16
17
|
const MAX_RETAINED_STRING_LENGTH = 256;
|
|
17
18
|
const MAX_BROKER_TIMEOUT_MS = 60_000;
|
|
18
19
|
const SENSITIVE_FIELD =
|
|
@@ -20,14 +21,18 @@ const SENSITIVE_FIELD =
|
|
|
20
21
|
const SENSITIVE_VALUE =
|
|
21
22
|
/(?:0x[a-f0-9]{40,}|[a-f0-9]{64,}|eyJ[a-z0-9_-]{20,}\.[a-z0-9_-]{20,})/iu;
|
|
22
23
|
|
|
23
|
-
function brokerSocketPath(runtimeDir) {
|
|
24
|
+
function brokerSocketPath(runtimeDir, endpointIdentity) {
|
|
24
25
|
const runtimePath = path.resolve(
|
|
25
26
|
runtimeDir ||
|
|
26
27
|
process.env.RECIPE_RUNTIME_DIR ||
|
|
27
28
|
path.join('temp', 'recipe', 'runtime'),
|
|
28
29
|
);
|
|
30
|
+
const socketIdentity =
|
|
31
|
+
endpointIdentity === undefined || endpointIdentity === null
|
|
32
|
+
? runtimePath
|
|
33
|
+
: `${runtimePath}\u0000${String(endpointIdentity)}`;
|
|
29
34
|
const identity = createHash('sha256')
|
|
30
|
-
.update(
|
|
35
|
+
.update(socketIdentity)
|
|
31
36
|
.digest('hex')
|
|
32
37
|
.slice(0, 20);
|
|
33
38
|
return path.join('/tmp', `mmh-cdp-${identity}.sock`);
|
|
@@ -77,6 +82,23 @@ function boundedTimeout(value, fallback = 10_000) {
|
|
|
77
82
|
: fallback;
|
|
78
83
|
}
|
|
79
84
|
|
|
85
|
+
// State recovery is event-driven. Pick a fixed bound below the caller's RPC
|
|
86
|
+
// deadline so the broker always answers before the client gives up. Returning
|
|
87
|
+
// literals also keeps the timer independent from untrusted request values.
|
|
88
|
+
function stateWaitTimeout(value) {
|
|
89
|
+
const timeout = boundedTimeout(value);
|
|
90
|
+
if (timeout <= 500) return 1;
|
|
91
|
+
if (timeout <= 1_000) return 400;
|
|
92
|
+
if (timeout <= 2_000) return 900;
|
|
93
|
+
if (timeout <= 5_000) return 1_900;
|
|
94
|
+
if (timeout <= 10_000) return 4_500;
|
|
95
|
+
if (timeout <= 15_000) return 9_500;
|
|
96
|
+
if (timeout <= 30_000) return 14_500;
|
|
97
|
+
if (timeout <= 45_000) return 29_500;
|
|
98
|
+
if (timeout <= 55_000) return 44_500;
|
|
99
|
+
return 54_500;
|
|
100
|
+
}
|
|
101
|
+
|
|
80
102
|
function readField(value, field) {
|
|
81
103
|
let current = value;
|
|
82
104
|
for (const segment of String(field).split('.')) {
|
|
@@ -333,6 +355,25 @@ function createCdpBroker({
|
|
|
333
355
|
const completedCaptures = new Map();
|
|
334
356
|
const clients = new Set();
|
|
335
357
|
const subscriptions = new Map();
|
|
358
|
+
const knownTargets = new Map(
|
|
359
|
+
[...sessions.entries()].flatMap(([deviceId, session]) =>
|
|
360
|
+
session?.brokerReady
|
|
361
|
+
? [[deviceId, {
|
|
362
|
+
deviceId,
|
|
363
|
+
generation: 1,
|
|
364
|
+
name: String(session.name || ''),
|
|
365
|
+
ready: true,
|
|
366
|
+
}]]
|
|
367
|
+
: [],
|
|
368
|
+
),
|
|
369
|
+
);
|
|
370
|
+
const sessionWaiters = new Map();
|
|
371
|
+
const targetWaiters = new Set();
|
|
372
|
+
const observedSessions = new Map(
|
|
373
|
+
[...sessions.entries()].flatMap(([deviceId, session]) =>
|
|
374
|
+
session?.brokerReady ? [[deviceId, session]] : [],
|
|
375
|
+
),
|
|
376
|
+
);
|
|
336
377
|
|
|
337
378
|
const capturesFor = (deviceId) => {
|
|
338
379
|
if (!captures.has(deviceId)) captures.set(deviceId, new Map());
|
|
@@ -346,23 +387,83 @@ function createCdpBroker({
|
|
|
346
387
|
return completedCaptures.get(deviceId);
|
|
347
388
|
};
|
|
348
389
|
|
|
349
|
-
|
|
390
|
+
function targetList({ nameIncludes = '', readyOnly = false } = {}) {
|
|
391
|
+
const pin = String(nameIncludes).trim().toLowerCase();
|
|
392
|
+
return [...knownTargets.values()]
|
|
393
|
+
.filter((target) => !readyOnly || target.ready)
|
|
394
|
+
.filter((target) => !pin || target.name.toLowerCase().includes(pin))
|
|
395
|
+
.map((target) => ({ ...target }))
|
|
396
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function settleTargetWaiters() {
|
|
400
|
+
for (const waiter of [...targetWaiters]) {
|
|
401
|
+
const targets = targetList({ nameIncludes: waiter.nameIncludes });
|
|
402
|
+
if (targets.length === 0) continue;
|
|
403
|
+
targetWaiters.delete(waiter);
|
|
404
|
+
clearTimeout(waiter.timer);
|
|
405
|
+
waiter.resolve(targets);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function resolveTargets(params, timeoutMs, socket) {
|
|
410
|
+
const existing = targetList({ nameIncludes: params.nameIncludes });
|
|
411
|
+
if (existing.length > 0) return Promise.resolve(existing);
|
|
412
|
+
requestDiscovery?.('');
|
|
413
|
+
return new Promise((resolve, reject) => {
|
|
414
|
+
const waiter = {
|
|
415
|
+
nameIncludes: String(params.nameIncludes || ''),
|
|
416
|
+
resolve,
|
|
417
|
+
reject,
|
|
418
|
+
socket,
|
|
419
|
+
timer: null,
|
|
420
|
+
};
|
|
421
|
+
waiter.timer = setTimeout(() => {
|
|
422
|
+
targetWaiters.delete(waiter);
|
|
423
|
+
reject(new Error('CDP broker never observed the requested target'));
|
|
424
|
+
}, stateWaitTimeout(timeoutMs));
|
|
425
|
+
targetWaiters.add(waiter);
|
|
426
|
+
settleTargetWaiters();
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function waitForSession(deviceId, timeoutMs, socket) {
|
|
350
431
|
if (sessions.get(deviceId)?.brokerReady) return sessions.get(deviceId);
|
|
351
432
|
requestDiscovery?.(deviceId);
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
433
|
+
return new Promise((resolve, reject) => {
|
|
434
|
+
const waiters = sessionWaiters.get(deviceId) || new Set();
|
|
435
|
+
const waiter = {
|
|
436
|
+
resolve,
|
|
437
|
+
reject,
|
|
438
|
+
socket,
|
|
439
|
+
timer: setTimeout(() => {
|
|
440
|
+
waiters.delete(waiter);
|
|
441
|
+
if (waiters.size === 0) sessionWaiters.delete(deviceId);
|
|
442
|
+
reject(new Error(`CDP broker target unavailable: ${deviceId}`));
|
|
443
|
+
}, stateWaitTimeout(timeoutMs)),
|
|
444
|
+
};
|
|
445
|
+
waiters.add(waiter);
|
|
446
|
+
sessionWaiters.set(deviceId, waiters);
|
|
355
447
|
const session = sessions.get(deviceId);
|
|
356
|
-
if (session?.brokerReady)
|
|
357
|
-
|
|
358
|
-
|
|
448
|
+
if (session?.brokerReady) {
|
|
449
|
+
waiters.delete(waiter);
|
|
450
|
+
if (waiters.size === 0) sessionWaiters.delete(deviceId);
|
|
451
|
+
clearTimeout(waiter.timer);
|
|
452
|
+
resolve(session);
|
|
453
|
+
}
|
|
454
|
+
});
|
|
359
455
|
}
|
|
360
456
|
|
|
361
|
-
async function enableNetwork(
|
|
457
|
+
async function enableNetwork(
|
|
458
|
+
deviceId,
|
|
459
|
+
capture,
|
|
460
|
+
timeoutMs = 10_000,
|
|
461
|
+
socket,
|
|
462
|
+
) {
|
|
362
463
|
try {
|
|
363
464
|
const budgetMs = boundedTimeout(timeoutMs);
|
|
364
465
|
const deadline = Date.now() + budgetMs;
|
|
365
|
-
const session = await waitForSession(deviceId, budgetMs);
|
|
466
|
+
const session = await waitForSession(deviceId, budgetMs, socket);
|
|
366
467
|
const remainingMs = Math.max(1, deadline - Date.now());
|
|
367
468
|
await sendCommand(session, 'Network.enable', {}, remainingMs);
|
|
368
469
|
if (capture.enableErrors.length > 0) capture.partial = true;
|
|
@@ -422,15 +523,21 @@ function createCdpBroker({
|
|
|
422
523
|
};
|
|
423
524
|
}
|
|
424
525
|
|
|
425
|
-
async function control(
|
|
526
|
+
async function control(
|
|
527
|
+
deviceId,
|
|
528
|
+
action,
|
|
529
|
+
params,
|
|
530
|
+
timeoutMs = 10_000,
|
|
531
|
+
socket,
|
|
532
|
+
) {
|
|
426
533
|
if (action === 'list-targets') {
|
|
427
|
-
return
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
534
|
+
return targetList({ readyOnly: true }).map(({ deviceId, name }) => ({
|
|
535
|
+
deviceId,
|
|
536
|
+
name,
|
|
537
|
+
}));
|
|
538
|
+
}
|
|
539
|
+
if (action === 'resolve-targets') {
|
|
540
|
+
return resolveTargets(params, timeoutMs, socket);
|
|
434
541
|
}
|
|
435
542
|
if (action === 'capture-start') {
|
|
436
543
|
const capture = normalizeCapture(params);
|
|
@@ -443,7 +550,12 @@ function createCdpBroker({
|
|
|
443
550
|
}
|
|
444
551
|
completedCapturesFor(deviceId).delete(capture.id);
|
|
445
552
|
deviceCaptures.set(capture.id, capture);
|
|
446
|
-
await enableNetwork(
|
|
553
|
+
await enableNetwork(
|
|
554
|
+
deviceId,
|
|
555
|
+
capture,
|
|
556
|
+
Math.max(1, timeoutMs - 500),
|
|
557
|
+
socket,
|
|
558
|
+
);
|
|
447
559
|
return { id: capture.id, status: 'started' };
|
|
448
560
|
}
|
|
449
561
|
if (action === 'capture-end') {
|
|
@@ -467,15 +579,18 @@ function createCdpBroker({
|
|
|
467
579
|
try {
|
|
468
580
|
onClientActivity?.();
|
|
469
581
|
if (request.type === 'command') {
|
|
582
|
+
const budgetMs = boundedTimeout(request.timeoutMs);
|
|
583
|
+
const deadline = Date.now() + budgetMs;
|
|
470
584
|
const session = await waitForSession(
|
|
471
585
|
request.deviceId,
|
|
472
|
-
|
|
586
|
+
budgetMs,
|
|
587
|
+
socket,
|
|
473
588
|
);
|
|
474
589
|
const result = await sendCommand(
|
|
475
590
|
session,
|
|
476
591
|
request.method,
|
|
477
592
|
request.params || {},
|
|
478
|
-
|
|
593
|
+
Math.max(1, deadline - Date.now()),
|
|
479
594
|
);
|
|
480
595
|
writeMessage(socket, { id: request.id, result });
|
|
481
596
|
return;
|
|
@@ -486,6 +601,7 @@ function createCdpBroker({
|
|
|
486
601
|
request.action,
|
|
487
602
|
request.params || {},
|
|
488
603
|
boundedTimeout(request.timeoutMs),
|
|
604
|
+
socket,
|
|
489
605
|
);
|
|
490
606
|
writeMessage(socket, { id: request.id, result });
|
|
491
607
|
return;
|
|
@@ -532,7 +648,7 @@ function createCdpBroker({
|
|
|
532
648
|
socket.setEncoding('utf8');
|
|
533
649
|
socket.on('data', (chunk) => {
|
|
534
650
|
buffer += chunk;
|
|
535
|
-
if (Buffer.byteLength(buffer) >
|
|
651
|
+
if (Buffer.byteLength(buffer) > MAX_REQUEST_FRAME_BYTES) {
|
|
536
652
|
writeMessage(socket, { error: 'CDP broker request exceeds its bound' });
|
|
537
653
|
socket.destroy();
|
|
538
654
|
return;
|
|
@@ -556,6 +672,21 @@ function createCdpBroker({
|
|
|
556
672
|
const drop = () => {
|
|
557
673
|
clients.delete(socket);
|
|
558
674
|
subscriptions.delete(socket);
|
|
675
|
+
for (const waiter of [...targetWaiters]) {
|
|
676
|
+
if (waiter.socket !== socket) continue;
|
|
677
|
+
targetWaiters.delete(waiter);
|
|
678
|
+
clearTimeout(waiter.timer);
|
|
679
|
+
waiter.reject(new Error('CDP broker client closed'));
|
|
680
|
+
}
|
|
681
|
+
for (const [deviceId, waiters] of sessionWaiters) {
|
|
682
|
+
for (const waiter of [...waiters]) {
|
|
683
|
+
if (waiter.socket !== socket) continue;
|
|
684
|
+
waiters.delete(waiter);
|
|
685
|
+
clearTimeout(waiter.timer);
|
|
686
|
+
waiter.reject(new Error('CDP broker client closed'));
|
|
687
|
+
}
|
|
688
|
+
if (waiters.size === 0) sessionWaiters.delete(deviceId);
|
|
689
|
+
}
|
|
559
690
|
};
|
|
560
691
|
socket.on('close', drop);
|
|
561
692
|
socket.on('error', drop);
|
|
@@ -568,12 +699,38 @@ function createCdpBroker({
|
|
|
568
699
|
|
|
569
700
|
return {
|
|
570
701
|
onSessionOpen(deviceId) {
|
|
702
|
+
const session = sessions.get(deviceId);
|
|
703
|
+
if (!session?.brokerReady) return;
|
|
704
|
+
const previous = knownTargets.get(deviceId);
|
|
705
|
+
const sessionChanged = observedSessions.get(deviceId) !== session;
|
|
706
|
+
knownTargets.set(deviceId, {
|
|
707
|
+
deviceId,
|
|
708
|
+
generation:
|
|
709
|
+
previous?.generation === undefined
|
|
710
|
+
? 1
|
|
711
|
+
: previous.generation + (!previous.ready || sessionChanged ? 1 : 0),
|
|
712
|
+
name: String(session.name || previous?.name || ''),
|
|
713
|
+
ready: true,
|
|
714
|
+
});
|
|
715
|
+
observedSessions.set(deviceId, session);
|
|
716
|
+
const waiters = sessionWaiters.get(deviceId);
|
|
717
|
+
if (waiters) {
|
|
718
|
+
sessionWaiters.delete(deviceId);
|
|
719
|
+
for (const waiter of waiters) {
|
|
720
|
+
clearTimeout(waiter.timer);
|
|
721
|
+
waiter.resolve(session);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
settleTargetWaiters();
|
|
571
725
|
for (const capture of capturesFor(deviceId).values()) {
|
|
572
726
|
if (capture.reconnects > 0) capture.partial = true;
|
|
573
727
|
void enableNetwork(deviceId, capture);
|
|
574
728
|
}
|
|
575
729
|
},
|
|
576
730
|
onSessionClose(deviceId) {
|
|
731
|
+
const previous = knownTargets.get(deviceId);
|
|
732
|
+
if (previous) knownTargets.set(deviceId, { ...previous, ready: false });
|
|
733
|
+
observedSessions.delete(deviceId);
|
|
577
734
|
for (const capture of capturesFor(deviceId).values()) {
|
|
578
735
|
capture.reconnects += 1;
|
|
579
736
|
capture.partial = true;
|
|
@@ -621,6 +778,18 @@ function createCdpBroker({
|
|
|
621
778
|
},
|
|
622
779
|
close() {
|
|
623
780
|
clearInterval(expirySweep);
|
|
781
|
+
for (const waiters of sessionWaiters.values()) {
|
|
782
|
+
for (const waiter of waiters) {
|
|
783
|
+
clearTimeout(waiter.timer);
|
|
784
|
+
waiter.reject(new Error('CDP broker closed'));
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
sessionWaiters.clear();
|
|
788
|
+
for (const waiter of targetWaiters) {
|
|
789
|
+
clearTimeout(waiter.timer);
|
|
790
|
+
waiter.reject(new Error('CDP broker closed'));
|
|
791
|
+
}
|
|
792
|
+
targetWaiters.clear();
|
|
624
793
|
for (const socket of clients) socket.destroy();
|
|
625
794
|
try {
|
|
626
795
|
server.close();
|
|
@@ -696,7 +865,7 @@ function createBrokerClient(socketPath, deviceId, timeout) {
|
|
|
696
865
|
});
|
|
697
866
|
socket.on('data', (chunk) => {
|
|
698
867
|
buffer += chunk;
|
|
699
|
-
if (Buffer.byteLength(buffer) >
|
|
868
|
+
if (Buffer.byteLength(buffer) > MAX_RESPONSE_FRAME_BYTES) {
|
|
700
869
|
socket.destroy();
|
|
701
870
|
for (const entry of pending.values()) {
|
|
702
871
|
entry.reject(new Error('CDP broker response exceeds its bound'));
|
|
@@ -4,9 +4,9 @@ const fs = require('node:fs');
|
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
|
|
6
6
|
/** Read a value from .js.env */
|
|
7
|
-
function loadEnvValue(key) {
|
|
7
|
+
function loadEnvValue(key, appRoot = process.env.APP_ROOT || process.cwd()) {
|
|
8
8
|
try {
|
|
9
|
-
const envPath = path.resolve(
|
|
9
|
+
const envPath = path.resolve(appRoot, '.js.env');
|
|
10
10
|
const content = fs.readFileSync(envPath, 'utf8');
|
|
11
11
|
// .js.env uses `export KEY="value"` (shell-sourceable format),
|
|
12
12
|
// so we handle the optional `export` prefix and strip surrounding quotes.
|
|
@@ -18,9 +18,19 @@ function loadEnvValue(key) {
|
|
|
18
18
|
return undefined;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
function resolvePort(env = process.env, appRoot = env.APP_ROOT || process.cwd()) {
|
|
22
|
+
return (
|
|
23
|
+
env.WATCHER_PORT ||
|
|
24
|
+
env.METRO_PORT ||
|
|
25
|
+
loadEnvValue('WATCHER_PORT', appRoot) ||
|
|
26
|
+
loadEnvValue('METRO_PORT', appRoot) ||
|
|
27
|
+
'8081'
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
21
31
|
/** Read WATCHER_PORT from .js.env or env (default: 8081) */
|
|
22
32
|
function loadPort() {
|
|
23
|
-
return Number.parseInt(
|
|
33
|
+
return Number.parseInt(resolvePort(), 10);
|
|
24
34
|
}
|
|
25
35
|
|
|
26
36
|
/** Read IOS_SIMULATOR name from .js.env or env (default: none — accept any device) */
|
|
@@ -48,4 +58,4 @@ function loadAndroidTargetDeviceName() {
|
|
|
48
58
|
return process.env.ANDROID_TARGET_DEVICE_NAME || '';
|
|
49
59
|
}
|
|
50
60
|
|
|
51
|
-
module.exports = { loadEnvValue, loadPort, loadSimulatorName, loadAndroidDevice, loadAndroidTargetDeviceName };
|
|
61
|
+
module.exports = { loadEnvValue, loadPort, resolvePort, loadSimulatorName, loadAndroidDevice, loadAndroidTargetDeviceName };
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
set -uo pipefail
|
|
4
4
|
|
|
5
5
|
TARGET="$PWD"
|
|
6
|
-
PORT="${WATCHER_PORT:-${METRO_PORT:-
|
|
6
|
+
PORT="${WATCHER_PORT:-${METRO_PORT:-}}"
|
|
7
7
|
QUIET=false
|
|
8
8
|
|
|
9
9
|
while [ "$#" -gt 0 ]; do
|
|
@@ -22,6 +22,10 @@ done
|
|
|
22
22
|
[ "${METAMASK_RECIPE_CONSOLE_FORWARD:-1}" != "0" ] || exit 0
|
|
23
23
|
TARGET="$(cd "$TARGET" && pwd -P)"
|
|
24
24
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
25
|
+
if [ -z "$PORT" ] && command -v node >/dev/null 2>&1; then
|
|
26
|
+
PORT="$(APP_ROOT="$TARGET" CONFIG_PATH="$SCRIPT_DIR/bridge-runtime/lib/config.cjs" node -e "process.stdout.write(String(require(process.env.CONFIG_PATH).resolvePort()))")"
|
|
27
|
+
fi
|
|
28
|
+
PORT="${PORT:-8081}"
|
|
25
29
|
# shellcheck disable=SC1091
|
|
26
30
|
. "$SCRIPT_DIR/../shared/harness-path.sh"
|
|
27
31
|
LOG_DIR="$TARGET/$(recipe_runtime_dir)" || exit 1
|