@deeeed/metamask-harness 0.42.0 → 0.44.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 +42 -0
- package/README.md +5 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +363 -55
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +23 -10
- package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +2 -0
- package/adapters/mobile/bridge-runtime/lib/cdp-broker.cjs +296 -25
- package/adapters/mobile/bridge-runtime/lib/config.cjs +14 -4
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +4 -2
- package/adapters/mobile/reload-app.mjs +99 -1
- 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/mobile/prepare.js +12 -0
- package/dist/adapters/performance/cdp-trace.js +342 -0
- package/dist/adapters/performance/js-task-metrics.js +35 -0
- package/dist/adapters.js +39 -5
- package/dist/artifact-files.js +92 -0
- package/dist/async.js +19 -0
- package/dist/commands/call.js +63 -4
- package/dist/commands/run-engine.js +265 -139
- package/dist/commands/run-report.js +68 -0
- package/dist/commands/run.js +67 -3
- package/dist/execution-provenance.js +342 -0
- package/dist/network-observation.js +59 -47
- package/dist/performance-observation.js +465 -0
- package/dist/run-diagnostics.js +36 -11
- package/dist/runner.js +44 -13
- package/docs/PERFORMANCE-CAPTURE.md +33 -0
- package/docs/RECIPES.md +7 -0
- package/library/actions/mobile/perps/performance-capture.mjs +570 -189
- package/library/actions/mobile/perps/perps.mjs +122 -0
- package/library/actions/mobile/platform/bridge.mjs +43 -8
- package/library/actions/mobile/platform/native-session.mjs +1 -1
- package/library/actions/mobile/wallet/lock.mjs +1 -4
- package/library/actions/mobile/wallet/select_account.mjs +129 -17
- package/library/manifests/extension.action-manifest.json +85 -0
- package/library/manifests/mobile.action-manifest.json +125 -3
- package/library/recipes/mobile/perps/performance.recipe.json +73 -47
- package/package.json +1 -1
- package/scripts/site-contrast.mjs +43 -27
|
@@ -35,10 +35,12 @@ 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.
|
|
41
42
|
const HANDSHAKE_TIMEOUT_MS = 3000;
|
|
43
|
+
const RUNTIME_ENABLE_TIMEOUT_MS = 60_000;
|
|
42
44
|
|
|
43
45
|
const DISCOVER_ACTIVE_MS = 1000; // a device is unattached — look for it quickly
|
|
44
46
|
const DISCOVER_STEADY_MS = 10000; // all known targets attached — cheap liveness tick
|
|
@@ -46,7 +48,7 @@ const FLUSH_MS = 100;
|
|
|
46
48
|
const MAX_LINE_CHARS = 4000;
|
|
47
49
|
|
|
48
50
|
function parseArgs(argv) {
|
|
49
|
-
const args = { port:
|
|
51
|
+
const args = { port: resolvePort(), out: null };
|
|
50
52
|
for (let i = 2; i < argv.length; i += 1) {
|
|
51
53
|
if (argv[i] === '--port') args.port = argv[++i];
|
|
52
54
|
else if (argv[i] === '--out') args.out = argv[++i];
|
|
@@ -208,7 +210,7 @@ function sendCommand(session, method, params = {}, timeoutMs = 10_000) {
|
|
|
208
210
|
}
|
|
209
211
|
|
|
210
212
|
broker = createCdpBroker({
|
|
211
|
-
socketPath: brokerSocketPath(path.dirname(out)),
|
|
213
|
+
socketPath: brokerSocketPath(path.dirname(out), port),
|
|
212
214
|
sessions,
|
|
213
215
|
sendCommand,
|
|
214
216
|
requestDiscovery(deviceId) {
|
|
@@ -310,25 +312,36 @@ function connect(target) {
|
|
|
310
312
|
ws.addEventListener('open', async () => {
|
|
311
313
|
clearTimeout(handshakeTimer);
|
|
312
314
|
session.opened = true;
|
|
315
|
+
// A live inspector socket is enough for broker commands. Do not keep the
|
|
316
|
+
// broker unavailable while Runtime.enable replays a large console buffer or
|
|
317
|
+
// while __AGENTIC__ is being installed after a Hermes context handoff.
|
|
318
|
+
// Commands such as status already report agenticPresent=false and their
|
|
319
|
+
// callers retry against the same bounded action deadline.
|
|
320
|
+
session.brokerReady = true;
|
|
321
|
+
broker.onSessionOpen(deviceId);
|
|
322
|
+
devtoolsProxy.onSessionOpen(deviceId, session);
|
|
323
|
+
process.stderr.write(`console-forwarder: attached ${name}\n`);
|
|
313
324
|
try {
|
|
314
|
-
await sendCommand(
|
|
325
|
+
await sendCommand(
|
|
326
|
+
session,
|
|
327
|
+
'Runtime.enable',
|
|
328
|
+
{},
|
|
329
|
+
RUNTIME_ENABLE_TIMEOUT_MS,
|
|
330
|
+
);
|
|
315
331
|
const evaluation = await sendCommand(session, 'Runtime.evaluate', {
|
|
316
332
|
expression: "typeof globalThis.__AGENTIC__ === 'object'",
|
|
317
333
|
returnByValue: true,
|
|
318
334
|
awaitPromise: false,
|
|
319
335
|
});
|
|
320
336
|
if (evaluation?.result?.value !== true) {
|
|
321
|
-
|
|
337
|
+
process.stderr.write(
|
|
338
|
+
`console-forwarder: ${name} is waiting for __AGENTIC__\n`,
|
|
339
|
+
);
|
|
322
340
|
}
|
|
323
|
-
session.brokerReady = true;
|
|
324
|
-
broker.onSessionOpen(deviceId);
|
|
325
|
-
devtoolsProxy.onSessionOpen(deviceId, session);
|
|
326
|
-
process.stderr.write(`console-forwarder: attached ${name}\n`);
|
|
327
341
|
} catch (error) {
|
|
328
342
|
process.stderr.write(
|
|
329
|
-
`console-forwarder:
|
|
343
|
+
`console-forwarder: console bootstrap pending for ${name}: ${String(error?.message || error).slice(0, 256)}\n`,
|
|
330
344
|
);
|
|
331
|
-
ws.close();
|
|
332
345
|
}
|
|
333
346
|
});
|
|
334
347
|
ws.addEventListener('message', (event) => {
|
|
@@ -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,22 +12,40 @@ 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;
|
|
17
|
+
const MAX_HUD_STEP_BYTES = 16 * 1024;
|
|
18
|
+
const MAX_HUD_UPDATES = 64;
|
|
16
19
|
const MAX_RETAINED_STRING_LENGTH = 256;
|
|
17
20
|
const MAX_BROKER_TIMEOUT_MS = 60_000;
|
|
18
21
|
const SENSITIVE_FIELD =
|
|
19
22
|
/(?:address|authorization|cookie|key|password|secret|token|user|account)/iu;
|
|
20
23
|
const SENSITIVE_VALUE =
|
|
21
24
|
/(?:0x[a-f0-9]{40,}|[a-f0-9]{64,}|eyJ[a-z0-9_-]{20,}\.[a-z0-9_-]{20,})/iu;
|
|
25
|
+
const APPLY_HUD_UPDATE_FUNCTION = `function(step) {
|
|
26
|
+
const bridge = this.__AGENTIC__;
|
|
27
|
+
if (step === null) {
|
|
28
|
+
if (typeof bridge?.hideStep !== 'function') return false;
|
|
29
|
+
setTimeout(() => bridge.hideStep(), 0);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
if (typeof bridge?.showStep !== 'function') return false;
|
|
33
|
+
setTimeout(() => bridge.showStep(step), 0);
|
|
34
|
+
return true;
|
|
35
|
+
}`;
|
|
22
36
|
|
|
23
|
-
function brokerSocketPath(runtimeDir) {
|
|
37
|
+
function brokerSocketPath(runtimeDir, endpointIdentity) {
|
|
24
38
|
const runtimePath = path.resolve(
|
|
25
39
|
runtimeDir ||
|
|
26
40
|
process.env.RECIPE_RUNTIME_DIR ||
|
|
27
41
|
path.join('temp', 'recipe', 'runtime'),
|
|
28
42
|
);
|
|
43
|
+
const socketIdentity =
|
|
44
|
+
endpointIdentity === undefined || endpointIdentity === null
|
|
45
|
+
? runtimePath
|
|
46
|
+
: `${runtimePath}\u0000${String(endpointIdentity)}`;
|
|
29
47
|
const identity = createHash('sha256')
|
|
30
|
-
.update(
|
|
48
|
+
.update(socketIdentity)
|
|
31
49
|
.digest('hex')
|
|
32
50
|
.slice(0, 20);
|
|
33
51
|
return path.join('/tmp', `mmh-cdp-${identity}.sock`);
|
|
@@ -77,6 +95,23 @@ function boundedTimeout(value, fallback = 10_000) {
|
|
|
77
95
|
: fallback;
|
|
78
96
|
}
|
|
79
97
|
|
|
98
|
+
// State recovery is event-driven. Pick a fixed bound below the caller's RPC
|
|
99
|
+
// deadline so the broker always answers before the client gives up. Returning
|
|
100
|
+
// literals also keeps the timer independent from untrusted request values.
|
|
101
|
+
function stateWaitTimeout(value) {
|
|
102
|
+
const timeout = boundedTimeout(value);
|
|
103
|
+
if (timeout <= 500) return 1;
|
|
104
|
+
if (timeout <= 1_000) return 400;
|
|
105
|
+
if (timeout <= 2_000) return 900;
|
|
106
|
+
if (timeout <= 5_000) return 1_900;
|
|
107
|
+
if (timeout <= 10_000) return 4_500;
|
|
108
|
+
if (timeout <= 15_000) return 9_500;
|
|
109
|
+
if (timeout <= 30_000) return 14_500;
|
|
110
|
+
if (timeout <= 45_000) return 29_500;
|
|
111
|
+
if (timeout <= 55_000) return 44_500;
|
|
112
|
+
return 54_500;
|
|
113
|
+
}
|
|
114
|
+
|
|
80
115
|
function readField(value, field) {
|
|
81
116
|
let current = value;
|
|
82
117
|
for (const segment of String(field).split('.')) {
|
|
@@ -331,8 +366,30 @@ function createCdpBroker({
|
|
|
331
366
|
}) {
|
|
332
367
|
const captures = new Map();
|
|
333
368
|
const completedCaptures = new Map();
|
|
369
|
+
const hudUpdates = new Map();
|
|
370
|
+
const appliedHudVersions = new Map();
|
|
371
|
+
const hudApplyChains = new Map();
|
|
334
372
|
const clients = new Set();
|
|
335
373
|
const subscriptions = new Map();
|
|
374
|
+
const knownTargets = new Map(
|
|
375
|
+
[...sessions.entries()].flatMap(([deviceId, session]) =>
|
|
376
|
+
session?.brokerReady
|
|
377
|
+
? [[deviceId, {
|
|
378
|
+
deviceId,
|
|
379
|
+
generation: 1,
|
|
380
|
+
name: String(session.name || ''),
|
|
381
|
+
ready: true,
|
|
382
|
+
}]]
|
|
383
|
+
: [],
|
|
384
|
+
),
|
|
385
|
+
);
|
|
386
|
+
const sessionWaiters = new Map();
|
|
387
|
+
const targetWaiters = new Set();
|
|
388
|
+
const observedSessions = new Map(
|
|
389
|
+
[...sessions.entries()].flatMap(([deviceId, session]) =>
|
|
390
|
+
session?.brokerReady ? [[deviceId, session]] : [],
|
|
391
|
+
),
|
|
392
|
+
);
|
|
336
393
|
|
|
337
394
|
const capturesFor = (deviceId) => {
|
|
338
395
|
if (!captures.has(deviceId)) captures.set(deviceId, new Map());
|
|
@@ -346,23 +403,83 @@ function createCdpBroker({
|
|
|
346
403
|
return completedCaptures.get(deviceId);
|
|
347
404
|
};
|
|
348
405
|
|
|
349
|
-
|
|
406
|
+
function targetList({ nameIncludes = '', readyOnly = false } = {}) {
|
|
407
|
+
const pin = String(nameIncludes).trim().toLowerCase();
|
|
408
|
+
return [...knownTargets.values()]
|
|
409
|
+
.filter((target) => !readyOnly || target.ready)
|
|
410
|
+
.filter((target) => !pin || target.name.toLowerCase().includes(pin))
|
|
411
|
+
.map((target) => ({ ...target }))
|
|
412
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function settleTargetWaiters() {
|
|
416
|
+
for (const waiter of [...targetWaiters]) {
|
|
417
|
+
const targets = targetList({ nameIncludes: waiter.nameIncludes });
|
|
418
|
+
if (targets.length === 0) continue;
|
|
419
|
+
targetWaiters.delete(waiter);
|
|
420
|
+
clearTimeout(waiter.timer);
|
|
421
|
+
waiter.resolve(targets);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function resolveTargets(params, timeoutMs, socket) {
|
|
426
|
+
const existing = targetList({ nameIncludes: params.nameIncludes });
|
|
427
|
+
if (existing.length > 0) return Promise.resolve(existing);
|
|
428
|
+
requestDiscovery?.('');
|
|
429
|
+
return new Promise((resolve, reject) => {
|
|
430
|
+
const waiter = {
|
|
431
|
+
nameIncludes: String(params.nameIncludes || ''),
|
|
432
|
+
resolve,
|
|
433
|
+
reject,
|
|
434
|
+
socket,
|
|
435
|
+
timer: null,
|
|
436
|
+
};
|
|
437
|
+
waiter.timer = setTimeout(() => {
|
|
438
|
+
targetWaiters.delete(waiter);
|
|
439
|
+
reject(new Error('CDP broker never observed the requested target'));
|
|
440
|
+
}, stateWaitTimeout(timeoutMs));
|
|
441
|
+
targetWaiters.add(waiter);
|
|
442
|
+
settleTargetWaiters();
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function waitForSession(deviceId, timeoutMs, socket) {
|
|
350
447
|
if (sessions.get(deviceId)?.brokerReady) return sessions.get(deviceId);
|
|
351
448
|
requestDiscovery?.(deviceId);
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
449
|
+
return new Promise((resolve, reject) => {
|
|
450
|
+
const waiters = sessionWaiters.get(deviceId) || new Set();
|
|
451
|
+
const waiter = {
|
|
452
|
+
resolve,
|
|
453
|
+
reject,
|
|
454
|
+
socket,
|
|
455
|
+
timer: setTimeout(() => {
|
|
456
|
+
waiters.delete(waiter);
|
|
457
|
+
if (waiters.size === 0) sessionWaiters.delete(deviceId);
|
|
458
|
+
reject(new Error(`CDP broker target unavailable: ${deviceId}`));
|
|
459
|
+
}, stateWaitTimeout(timeoutMs)),
|
|
460
|
+
};
|
|
461
|
+
waiters.add(waiter);
|
|
462
|
+
sessionWaiters.set(deviceId, waiters);
|
|
355
463
|
const session = sessions.get(deviceId);
|
|
356
|
-
if (session?.brokerReady)
|
|
357
|
-
|
|
358
|
-
|
|
464
|
+
if (session?.brokerReady) {
|
|
465
|
+
waiters.delete(waiter);
|
|
466
|
+
if (waiters.size === 0) sessionWaiters.delete(deviceId);
|
|
467
|
+
clearTimeout(waiter.timer);
|
|
468
|
+
resolve(session);
|
|
469
|
+
}
|
|
470
|
+
});
|
|
359
471
|
}
|
|
360
472
|
|
|
361
|
-
async function enableNetwork(
|
|
473
|
+
async function enableNetwork(
|
|
474
|
+
deviceId,
|
|
475
|
+
capture,
|
|
476
|
+
timeoutMs = 10_000,
|
|
477
|
+
socket,
|
|
478
|
+
) {
|
|
362
479
|
try {
|
|
363
480
|
const budgetMs = boundedTimeout(timeoutMs);
|
|
364
481
|
const deadline = Date.now() + budgetMs;
|
|
365
|
-
const session = await waitForSession(deviceId, budgetMs);
|
|
482
|
+
const session = await waitForSession(deviceId, budgetMs, socket);
|
|
366
483
|
const remainingMs = Math.max(1, deadline - Date.now());
|
|
367
484
|
await sendCommand(session, 'Network.enable', {}, remainingMs);
|
|
368
485
|
if (capture.enableErrors.length > 0) capture.partial = true;
|
|
@@ -381,6 +498,67 @@ function createCdpBroker({
|
|
|
381
498
|
capturesFor(deviceId).delete(capture.id);
|
|
382
499
|
}
|
|
383
500
|
|
|
501
|
+
async function applyHudUpdate(deviceId, timeoutMs, socket) {
|
|
502
|
+
if (!hudUpdates.has(deviceId)) return { status: 'none' };
|
|
503
|
+
const budgetMs = boundedTimeout(timeoutMs);
|
|
504
|
+
const deadline = Date.now() + budgetMs;
|
|
505
|
+
const session = await waitForSession(deviceId, budgetMs, socket);
|
|
506
|
+
const update = hudUpdates.get(deviceId);
|
|
507
|
+
if (!update) return { status: 'none' };
|
|
508
|
+
if (appliedHudVersions.get(deviceId) === update.version) {
|
|
509
|
+
return { status: 'applied', version: update.version };
|
|
510
|
+
}
|
|
511
|
+
let applied = false;
|
|
512
|
+
while (Date.now() < deadline) {
|
|
513
|
+
const globalObject = await sendCommand(
|
|
514
|
+
session,
|
|
515
|
+
'Runtime.evaluate',
|
|
516
|
+
{ expression: 'globalThis', returnByValue: false, awaitPromise: false },
|
|
517
|
+
Math.max(1, deadline - Date.now()),
|
|
518
|
+
);
|
|
519
|
+
const objectId = globalObject?.result?.objectId;
|
|
520
|
+
if (!objectId) {
|
|
521
|
+
throw new Error('Mobile HUD runtime did not expose globalThis');
|
|
522
|
+
}
|
|
523
|
+
const evaluation = await sendCommand(
|
|
524
|
+
session,
|
|
525
|
+
'Runtime.callFunctionOn',
|
|
526
|
+
{
|
|
527
|
+
objectId,
|
|
528
|
+
functionDeclaration: APPLY_HUD_UPDATE_FUNCTION,
|
|
529
|
+
arguments: [{ value: update.step }],
|
|
530
|
+
returnByValue: true,
|
|
531
|
+
awaitPromise: false,
|
|
532
|
+
},
|
|
533
|
+
Math.max(1, deadline - Date.now()),
|
|
534
|
+
);
|
|
535
|
+
if (evaluation?.result?.value === true) {
|
|
536
|
+
applied = true;
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
540
|
+
}
|
|
541
|
+
if (!applied) {
|
|
542
|
+
throw new Error('Mobile HUD bridge was not installed before the update deadline');
|
|
543
|
+
}
|
|
544
|
+
if (hudUpdates.get(deviceId)?.version === update.version) {
|
|
545
|
+
appliedHudVersions.set(deviceId, update.version);
|
|
546
|
+
}
|
|
547
|
+
return { status: 'applied', version: update.version };
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function scheduleHudUpdate(deviceId, timeoutMs, socket) {
|
|
551
|
+
const previous = hudApplyChains.get(deviceId) || Promise.resolve();
|
|
552
|
+
const next = previous
|
|
553
|
+
.catch(() => undefined)
|
|
554
|
+
.then(() => applyHudUpdate(deviceId, timeoutMs, socket));
|
|
555
|
+
hudApplyChains.set(deviceId, next);
|
|
556
|
+
void next.finally(() => {
|
|
557
|
+
if (hudApplyChains.get(deviceId) === next) hudApplyChains.delete(deviceId);
|
|
558
|
+
}).catch(() => undefined);
|
|
559
|
+
return next;
|
|
560
|
+
}
|
|
561
|
+
|
|
384
562
|
function expireCapture(deviceId, capture) {
|
|
385
563
|
if (capturesFor(deviceId).get(capture.id) !== capture) return;
|
|
386
564
|
finishCapture(deviceId, capture, true);
|
|
@@ -422,15 +600,44 @@ function createCdpBroker({
|
|
|
422
600
|
};
|
|
423
601
|
}
|
|
424
602
|
|
|
425
|
-
async function control(
|
|
603
|
+
async function control(
|
|
604
|
+
deviceId,
|
|
605
|
+
action,
|
|
606
|
+
params,
|
|
607
|
+
timeoutMs = 10_000,
|
|
608
|
+
socket,
|
|
609
|
+
) {
|
|
426
610
|
if (action === 'list-targets') {
|
|
427
|
-
return
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
611
|
+
return targetList({ readyOnly: true }).map(({ deviceId, name }) => ({
|
|
612
|
+
deviceId,
|
|
613
|
+
name,
|
|
614
|
+
}));
|
|
615
|
+
}
|
|
616
|
+
if (action === 'resolve-targets') {
|
|
617
|
+
return resolveTargets(params, timeoutMs, socket);
|
|
618
|
+
}
|
|
619
|
+
if (action === 'hud-update') {
|
|
620
|
+
const step = params.step ?? null;
|
|
621
|
+
if (step !== null && (typeof step !== 'object' || Array.isArray(step))) {
|
|
622
|
+
throw new Error('HUD update requires an object step or null');
|
|
623
|
+
}
|
|
624
|
+
if (Buffer.byteLength(JSON.stringify(step)) > MAX_HUD_STEP_BYTES) {
|
|
625
|
+
throw new Error('HUD update exceeds its bound');
|
|
626
|
+
}
|
|
627
|
+
const previousVersion = hudUpdates.get(deviceId)?.version || 0;
|
|
628
|
+
if (hudUpdates.has(deviceId)) {
|
|
629
|
+
hudUpdates.delete(deviceId);
|
|
630
|
+
}
|
|
631
|
+
while (hudUpdates.size >= MAX_HUD_UPDATES) {
|
|
632
|
+
const oldestDeviceId = hudUpdates.keys().next().value;
|
|
633
|
+
hudUpdates.delete(oldestDeviceId);
|
|
634
|
+
appliedHudVersions.delete(oldestDeviceId);
|
|
635
|
+
}
|
|
636
|
+
const version = previousVersion + 1;
|
|
637
|
+
hudUpdates.set(deviceId, { step, version });
|
|
638
|
+
requestDiscovery?.(deviceId);
|
|
639
|
+
void scheduleHudUpdate(deviceId, 10_000).catch(() => undefined);
|
|
640
|
+
return { status: 'queued', version };
|
|
434
641
|
}
|
|
435
642
|
if (action === 'capture-start') {
|
|
436
643
|
const capture = normalizeCapture(params);
|
|
@@ -443,7 +650,12 @@ function createCdpBroker({
|
|
|
443
650
|
}
|
|
444
651
|
completedCapturesFor(deviceId).delete(capture.id);
|
|
445
652
|
deviceCaptures.set(capture.id, capture);
|
|
446
|
-
await enableNetwork(
|
|
653
|
+
await enableNetwork(
|
|
654
|
+
deviceId,
|
|
655
|
+
capture,
|
|
656
|
+
Math.max(1, timeoutMs - 500),
|
|
657
|
+
socket,
|
|
658
|
+
);
|
|
447
659
|
return { id: capture.id, status: 'started' };
|
|
448
660
|
}
|
|
449
661
|
if (action === 'capture-end') {
|
|
@@ -467,15 +679,18 @@ function createCdpBroker({
|
|
|
467
679
|
try {
|
|
468
680
|
onClientActivity?.();
|
|
469
681
|
if (request.type === 'command') {
|
|
682
|
+
const budgetMs = boundedTimeout(request.timeoutMs);
|
|
683
|
+
const deadline = Date.now() + budgetMs;
|
|
470
684
|
const session = await waitForSession(
|
|
471
685
|
request.deviceId,
|
|
472
|
-
|
|
686
|
+
budgetMs,
|
|
687
|
+
socket,
|
|
473
688
|
);
|
|
474
689
|
const result = await sendCommand(
|
|
475
690
|
session,
|
|
476
691
|
request.method,
|
|
477
692
|
request.params || {},
|
|
478
|
-
|
|
693
|
+
Math.max(1, deadline - Date.now()),
|
|
479
694
|
);
|
|
480
695
|
writeMessage(socket, { id: request.id, result });
|
|
481
696
|
return;
|
|
@@ -486,6 +701,7 @@ function createCdpBroker({
|
|
|
486
701
|
request.action,
|
|
487
702
|
request.params || {},
|
|
488
703
|
boundedTimeout(request.timeoutMs),
|
|
704
|
+
socket,
|
|
489
705
|
);
|
|
490
706
|
writeMessage(socket, { id: request.id, result });
|
|
491
707
|
return;
|
|
@@ -532,7 +748,7 @@ function createCdpBroker({
|
|
|
532
748
|
socket.setEncoding('utf8');
|
|
533
749
|
socket.on('data', (chunk) => {
|
|
534
750
|
buffer += chunk;
|
|
535
|
-
if (Buffer.byteLength(buffer) >
|
|
751
|
+
if (Buffer.byteLength(buffer) > MAX_REQUEST_FRAME_BYTES) {
|
|
536
752
|
writeMessage(socket, { error: 'CDP broker request exceeds its bound' });
|
|
537
753
|
socket.destroy();
|
|
538
754
|
return;
|
|
@@ -556,6 +772,21 @@ function createCdpBroker({
|
|
|
556
772
|
const drop = () => {
|
|
557
773
|
clients.delete(socket);
|
|
558
774
|
subscriptions.delete(socket);
|
|
775
|
+
for (const waiter of [...targetWaiters]) {
|
|
776
|
+
if (waiter.socket !== socket) continue;
|
|
777
|
+
targetWaiters.delete(waiter);
|
|
778
|
+
clearTimeout(waiter.timer);
|
|
779
|
+
waiter.reject(new Error('CDP broker client closed'));
|
|
780
|
+
}
|
|
781
|
+
for (const [deviceId, waiters] of sessionWaiters) {
|
|
782
|
+
for (const waiter of [...waiters]) {
|
|
783
|
+
if (waiter.socket !== socket) continue;
|
|
784
|
+
waiters.delete(waiter);
|
|
785
|
+
clearTimeout(waiter.timer);
|
|
786
|
+
waiter.reject(new Error('CDP broker client closed'));
|
|
787
|
+
}
|
|
788
|
+
if (waiters.size === 0) sessionWaiters.delete(deviceId);
|
|
789
|
+
}
|
|
559
790
|
};
|
|
560
791
|
socket.on('close', drop);
|
|
561
792
|
socket.on('error', drop);
|
|
@@ -568,12 +799,40 @@ function createCdpBroker({
|
|
|
568
799
|
|
|
569
800
|
return {
|
|
570
801
|
onSessionOpen(deviceId) {
|
|
802
|
+
const session = sessions.get(deviceId);
|
|
803
|
+
if (!session?.brokerReady) return;
|
|
804
|
+
const previous = knownTargets.get(deviceId);
|
|
805
|
+
const sessionChanged = observedSessions.get(deviceId) !== session;
|
|
806
|
+
knownTargets.set(deviceId, {
|
|
807
|
+
deviceId,
|
|
808
|
+
generation:
|
|
809
|
+
previous?.generation === undefined
|
|
810
|
+
? 1
|
|
811
|
+
: previous.generation + (!previous.ready || sessionChanged ? 1 : 0),
|
|
812
|
+
name: String(session.name || previous?.name || ''),
|
|
813
|
+
ready: true,
|
|
814
|
+
});
|
|
815
|
+
observedSessions.set(deviceId, session);
|
|
816
|
+
const waiters = sessionWaiters.get(deviceId);
|
|
817
|
+
if (waiters) {
|
|
818
|
+
sessionWaiters.delete(deviceId);
|
|
819
|
+
for (const waiter of waiters) {
|
|
820
|
+
clearTimeout(waiter.timer);
|
|
821
|
+
waiter.resolve(session);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
settleTargetWaiters();
|
|
571
825
|
for (const capture of capturesFor(deviceId).values()) {
|
|
572
826
|
if (capture.reconnects > 0) capture.partial = true;
|
|
573
827
|
void enableNetwork(deviceId, capture);
|
|
574
828
|
}
|
|
829
|
+
void scheduleHudUpdate(deviceId, 10_000).catch(() => undefined);
|
|
575
830
|
},
|
|
576
831
|
onSessionClose(deviceId) {
|
|
832
|
+
const previous = knownTargets.get(deviceId);
|
|
833
|
+
if (previous) knownTargets.set(deviceId, { ...previous, ready: false });
|
|
834
|
+
observedSessions.delete(deviceId);
|
|
835
|
+
appliedHudVersions.delete(deviceId);
|
|
577
836
|
for (const capture of capturesFor(deviceId).values()) {
|
|
578
837
|
capture.reconnects += 1;
|
|
579
838
|
capture.partial = true;
|
|
@@ -621,6 +880,18 @@ function createCdpBroker({
|
|
|
621
880
|
},
|
|
622
881
|
close() {
|
|
623
882
|
clearInterval(expirySweep);
|
|
883
|
+
for (const waiters of sessionWaiters.values()) {
|
|
884
|
+
for (const waiter of waiters) {
|
|
885
|
+
clearTimeout(waiter.timer);
|
|
886
|
+
waiter.reject(new Error('CDP broker closed'));
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
sessionWaiters.clear();
|
|
890
|
+
for (const waiter of targetWaiters) {
|
|
891
|
+
clearTimeout(waiter.timer);
|
|
892
|
+
waiter.reject(new Error('CDP broker closed'));
|
|
893
|
+
}
|
|
894
|
+
targetWaiters.clear();
|
|
624
895
|
for (const socket of clients) socket.destroy();
|
|
625
896
|
try {
|
|
626
897
|
server.close();
|
|
@@ -696,7 +967,7 @@ function createBrokerClient(socketPath, deviceId, timeout) {
|
|
|
696
967
|
});
|
|
697
968
|
socket.on('data', (chunk) => {
|
|
698
969
|
buffer += chunk;
|
|
699
|
-
if (Buffer.byteLength(buffer) >
|
|
970
|
+
if (Buffer.byteLength(buffer) > MAX_RESPONSE_FRAME_BYTES) {
|
|
700
971
|
socket.destroy();
|
|
701
972
|
for (const entry of pending.values()) {
|
|
702
973
|
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 };
|
|
@@ -300,6 +300,7 @@ async function discoverTarget(port, { probe = true } = {}) {
|
|
|
300
300
|
for (const candidate of candidates) {
|
|
301
301
|
if (candidate.webSocketDebuggerUrl === acceptedPinnedCandidate.webSocketDebuggerUrl) {
|
|
302
302
|
return {
|
|
303
|
+
id: acceptedPinnedCandidate.id || '',
|
|
303
304
|
wsUrl: acceptedPinnedCandidate.webSocketDebuggerUrl,
|
|
304
305
|
deviceName: acceptedPinnedCandidate.deviceName || '',
|
|
305
306
|
};
|
|
@@ -312,6 +313,7 @@ async function discoverTarget(port, { probe = true } = {}) {
|
|
|
312
313
|
// platform/device filtering and select the highest-ranked runtime instead.
|
|
313
314
|
if (!probe) {
|
|
314
315
|
return {
|
|
316
|
+
id: candidates[0].id || '',
|
|
315
317
|
wsUrl: candidates[0].webSocketDebuggerUrl,
|
|
316
318
|
deviceName: candidates[0].deviceName || '',
|
|
317
319
|
};
|
|
@@ -331,7 +333,7 @@ async function discoverTarget(port, { probe = true } = {}) {
|
|
|
331
333
|
for (const candidate of candidates) {
|
|
332
334
|
const hasAgentic = await probeTarget(candidate.webSocketDebuggerUrl);
|
|
333
335
|
if (hasAgentic) {
|
|
334
|
-
return { wsUrl: candidate.webSocketDebuggerUrl, deviceName: candidate.deviceName || '' };
|
|
336
|
+
return { id: candidate.id || '', wsUrl: candidate.webSocketDebuggerUrl, deviceName: candidate.deviceName || '' };
|
|
335
337
|
}
|
|
336
338
|
}
|
|
337
339
|
|
|
@@ -348,7 +350,7 @@ async function discoverTarget(port, { probe = true } = {}) {
|
|
|
348
350
|
}
|
|
349
351
|
|
|
350
352
|
// Fallback: return highest page number (most likely the JS runtime)
|
|
351
|
-
return { wsUrl: candidates[0].webSocketDebuggerUrl, deviceName: candidates[0].deviceName || '' };
|
|
353
|
+
return { id: candidates[0].id || '', wsUrl: candidates[0].webSocketDebuggerUrl, deviceName: candidates[0].deviceName || '' };
|
|
352
354
|
}
|
|
353
355
|
|
|
354
356
|
/**
|