@houwert/conductor 0.27.2 → 0.29.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.
@@ -0,0 +1,348 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.measureKeyLatency = measureKeyLatency;
4
+ exports.printLatencyReport = printLatencyReport;
5
+ /**
6
+ * Input-to-response latency for `press-key --measure`.
7
+ *
8
+ * Three things had to be got right here, all of them learned by measuring on
9
+ * real Android TV hardware over networked adb rather than assumed:
10
+ *
11
+ * - **A repeat is not a repeated measurement.** Pressing Right twenty times
12
+ * walks focus across twenty *different* transitions, some cheap, some
13
+ * crossing into a lazily-mounted row. Samples are therefore grouped by the
14
+ * transition they actually performed, and the aggregate is reported
15
+ * alongside — never instead of — the per-transition breakdown.
16
+ *
17
+ * - **Focus refusing to move is not a hang.** At the end of a rail the app is
18
+ * correctly declining to move focus. Reporting that as a timeout would be
19
+ * evidence of the sluggishness we are hunting, invented out of correct
20
+ * behaviour. `outcome` distinguishes `moved` / `unchanged` / `query-failed`.
21
+ *
22
+ * - **Over networked adb, host-side polling cannot measure this at all.** One
23
+ * hierarchy dump costs a full round trip (~400ms on a LAN); focus on a
24
+ * healthy TV app moves in 50-150ms. The polling result is then a measurement
25
+ * of the network with a deceptively tight variance, so when `pollCost`
26
+ * dominates we say so in a structured note. The measurement that does work
27
+ * is `pressToFrame`, which is computed entirely from device-side clocks.
28
+ */
29
+ const runner_js_1 = require("../runner.js");
30
+ const bootstrap_js_1 = require("../drivers/bootstrap.js");
31
+ const focused_js_1 = require("./focused.js");
32
+ const press_key_js_1 = require("./press-key.js");
33
+ const stats_js_1 = require("../stats.js");
34
+ const device_js_1 = require("../android/device.js");
35
+ const profile_frames_js_1 = require("./profile-frames.js");
36
+ async function resolveDeviceId(sessionName) {
37
+ if (sessionName !== 'default')
38
+ return sessionName;
39
+ const detected = await (0, runner_js_1.detectFirstDevice)().catch(() => undefined);
40
+ if (!detected)
41
+ throw new Error('no device found; pass --device');
42
+ return detected;
43
+ }
44
+ /**
45
+ * Reset, inject, timestamp, settle and dump — all inside one device-side shell.
46
+ *
47
+ * The timestamp is taken *after* the injection command returns rather than
48
+ * before it. `adb shell input keyevent` costs ~713ms on the device because it
49
+ * spawns an `app_process` JVM per invocation, and that cost lands entirely
50
+ * before the event is dispatched (`input` injects with WAIT_FOR_FINISH and then
51
+ * exits). Reading the clock afterwards therefore puts the press timestamp
52
+ * within a few ms of the actual injection instead of 713ms early, which is the
53
+ * difference between reporting the app's latency and reporting JVM startup.
54
+ *
55
+ * What this does *not* fix is contention: spawning and tearing down a JVM
56
+ * beside the frames being measured is load, and on a 1.7GB device it competes
57
+ * for exactly the resources whose scarcity we are looking for. That residual is
58
+ * reported as a `driver-perturbation` note rather than hidden.
59
+ */
60
+ async function measurePressResponse(deviceId, appId, keycode, settleMs) {
61
+ const bracket = await (0, device_js_1.shellBracketed)(deviceId, `dumpsys gfxinfo ${appId} reset; cat /proc/uptime; input keyevent ${keycode}; ` +
62
+ `cat /proc/uptime; sleep ${(settleMs / 1000).toFixed(2)}; ` +
63
+ `dumpsys gfxinfo ${appId} framestats`);
64
+ if (!bracket)
65
+ return undefined;
66
+ const stamps = [...bracket.stdout.matchAll(/^\s*(\d+\.\d+)\s+\d+\.\d+\s*$/gm)].map((m) => Number(m[1]) * 1000);
67
+ if (stamps.length < 2)
68
+ return undefined;
69
+ const [beforeInject, afterInject] = stamps;
70
+ const frames = (0, profile_frames_js_1.parseFramestats)(bracket.stdout)
71
+ .map((r) => (0, profile_frames_js_1.toFrameSample)(r))
72
+ .filter((f) => f !== null)
73
+ .sort((a, b) => a.vsyncNs - b.vsyncNs);
74
+ // A frame that *started* after the injection is a response to it. Frames
75
+ // drawn during JVM startup are in the buffer too and must not be counted.
76
+ const responding = frames.filter((f) => f.vsyncNs / 1e6 >= afterInject);
77
+ if (responding.length === 0)
78
+ return undefined;
79
+ const first = responding[0];
80
+ const last = responding[responding.length - 1];
81
+ return {
82
+ injectionCostMs: (0, stats_js_1.round)(afterInject - beforeInject),
83
+ response: {
84
+ pressToFrameMs: (0, stats_js_1.round)(first.completedNs / 1e6 - afterInject),
85
+ pressToFrameUpperMs: (0, stats_js_1.round)(first.completedNs / 1e6 - beforeInject),
86
+ dispatchWindowMs: (0, stats_js_1.round)(afterInject - beforeInject),
87
+ renderBurstMs: (0, stats_js_1.round)((last.completedNs - first.vsyncNs) / 1e6),
88
+ framesInBurst: responding.length,
89
+ jankyInBurst: responding.filter((f) => f.totalMs > 16.67).length,
90
+ },
91
+ };
92
+ }
93
+ /** One sample: press, then poll focus until its identity changes or we give up. */
94
+ async function sampleOnce(driver, key, index, opts, pollCosts, androidTarget) {
95
+ let before;
96
+ try {
97
+ before = await (0, focused_js_1.queryFocused)(driver);
98
+ }
99
+ catch {
100
+ return {
101
+ index,
102
+ key,
103
+ outcome: 'query-failed',
104
+ elapsedMs: null,
105
+ dispatchMs: 0,
106
+ polls: 0,
107
+ from: '(unknown)',
108
+ to: '(unknown)',
109
+ };
110
+ }
111
+ const beforeKey = (0, focused_js_1.focusKey)(before);
112
+ let response;
113
+ let injectionCostMs;
114
+ let dispatchMs = 0;
115
+ const t0 = performance.now();
116
+ const keycode = androidTarget ? press_key_js_1.ANDROID_KEYCODE[key] : undefined;
117
+ if (androidTarget && keycode !== undefined) {
118
+ // Injection happens inside the device-side bracket, so the press is timed
119
+ // in the device's own clock domain with no host or adb time in the window.
120
+ const measured = await measurePressResponse(androidTarget.deviceId, androidTarget.appId, keycode, opts.settleMs);
121
+ dispatchMs = (0, stats_js_1.round)(performance.now() - t0);
122
+ response = measured?.response;
123
+ injectionCostMs = measured?.injectionCostMs;
124
+ // The bracket already waited out the settle, so focus has landed.
125
+ }
126
+ else {
127
+ const t = performance.now();
128
+ await (0, press_key_js_1.dispatchKey)(driver, key, opts.holdSeconds);
129
+ dispatchMs = (0, stats_js_1.round)(performance.now() - t);
130
+ }
131
+ const deadline = performance.now() + opts.timeoutMs;
132
+ let polls = 0;
133
+ let queryFailed = false;
134
+ let lastKey = beforeKey;
135
+ while (performance.now() < deadline) {
136
+ const pollStart = performance.now();
137
+ let current;
138
+ try {
139
+ current = await (0, focused_js_1.queryFocused)(driver);
140
+ }
141
+ catch {
142
+ queryFailed = true;
143
+ break;
144
+ }
145
+ const pollEnd = performance.now();
146
+ pollCosts.push((0, stats_js_1.round)(pollEnd - pollStart));
147
+ polls++;
148
+ lastKey = (0, focused_js_1.focusKey)(current);
149
+ if (lastKey !== beforeKey) {
150
+ return {
151
+ index,
152
+ key,
153
+ outcome: 'moved',
154
+ // pollEnd is an upper bound: the change could have landed any time
155
+ // during this dump. pollCost is the error bar.
156
+ elapsedMs: (0, stats_js_1.round)(pollEnd - t0),
157
+ dispatchMs,
158
+ polls,
159
+ from: beforeKey,
160
+ to: lastKey,
161
+ response,
162
+ injectionCostMs,
163
+ };
164
+ }
165
+ if (opts.pollIntervalMs > 0) {
166
+ await new Promise((r) => setTimeout(r, opts.pollIntervalMs));
167
+ }
168
+ }
169
+ return {
170
+ index,
171
+ key,
172
+ // Focus queries kept succeeding and the identity never changed: the app is
173
+ // responding, it simply did not move focus. That is a boundary, not a hang.
174
+ outcome: queryFailed ? 'query-failed' : 'unchanged',
175
+ elapsedMs: null,
176
+ dispatchMs,
177
+ polls,
178
+ from: beforeKey,
179
+ to: lastKey,
180
+ response,
181
+ injectionCostMs,
182
+ };
183
+ }
184
+ function groupByTransition(samples) {
185
+ const groups = new Map();
186
+ for (const s of samples) {
187
+ if (s.outcome !== 'moved')
188
+ continue;
189
+ const id = `${s.key}${s.from}${s.to}`;
190
+ groups.set(id, [...(groups.get(id) ?? []), s]);
191
+ }
192
+ return [...groups.entries()]
193
+ .map(([id, group]) => {
194
+ const [key, from, to] = id.split('');
195
+ return {
196
+ key,
197
+ from,
198
+ to,
199
+ count: group.length,
200
+ focusChange: (0, stats_js_1.describe)(group.map((s) => s.elapsedMs).filter((v) => v !== null)),
201
+ pressToFrame: (0, stats_js_1.describe)(group.map((s) => s.response?.pressToFrameMs).filter((v) => v !== undefined)),
202
+ };
203
+ })
204
+ .sort((a, b) => (b.pressToFrame.p50Ms ?? b.focusChange.p50Ms ?? 0) -
205
+ (a.pressToFrame.p50Ms ?? a.focusChange.p50Ms ?? 0));
206
+ }
207
+ async function measureKeyLatency(sessionName, keys, opts) {
208
+ const deviceId = await resolveDeviceId(sessionName);
209
+ const platform = await (0, bootstrap_js_1.detectPlatform)(deviceId).catch(() => 'unknown');
210
+ const driver = await (0, runner_js_1.getDriver)(sessionName);
211
+ const notes = [];
212
+ let androidTarget;
213
+ let appId;
214
+ if (platform === 'android') {
215
+ appId = opts.appId ?? (await (0, device_js_1.resolveAndroidForegroundApp)(deviceId));
216
+ if (appId) {
217
+ const probe = await (0, device_js_1.adbShell)(deviceId, ['dumpsys', 'gfxinfo', appId]);
218
+ if (probe.success)
219
+ androidTarget = { deviceId, appId };
220
+ }
221
+ }
222
+ const pollCosts = [];
223
+ const samples = [];
224
+ for (let i = 0; i < opts.repeat; i++) {
225
+ samples.push(await sampleOnce(driver, keys[i % keys.length], i, opts, pollCosts, androidTarget));
226
+ }
227
+ const outcomes = { moved: 0, unchanged: 0, 'query-failed': 0 };
228
+ for (const s of samples)
229
+ outcomes[s.outcome]++;
230
+ const moved = samples.filter((s) => s.elapsedMs !== null).map((s) => s.elapsedMs);
231
+ const responses = samples
232
+ .map((s) => s.response?.pressToFrameMs)
233
+ .filter((v) => v !== undefined);
234
+ const focusChange = moved.length > 0 ? (0, stats_js_1.describe)(moved) : undefined;
235
+ const pollCost = pollCosts.length > 0 ? (0, stats_js_1.describe)(pollCosts) : undefined;
236
+ const pressToFrame = responses.length > 0 ? (0, stats_js_1.describe)(responses) : undefined;
237
+ // A poll that costs more than the thing it is timing is measuring itself.
238
+ if ((0, stats_js_1.hasSamples)(focusChange) && (0, stats_js_1.hasSamples)(pollCost)) {
239
+ const ratio = (0, stats_js_1.round)(pollCost.p50Ms / focusChange.p50Ms);
240
+ if (ratio > 0.5) {
241
+ notes.push({
242
+ code: 'round-trip-bound',
243
+ message: `One focus query costs ${pollCost.p50Ms}ms against a focusChange of ` +
244
+ `${focusChange.p50Ms}ms, so this result is bounded by transport, not by the app. ` +
245
+ `Its small variance reflects a stable connection rather than a precise measurement. ` +
246
+ (pressToFrame
247
+ ? 'Use pressToFrame, which is computed from device-side clocks.'
248
+ : 'Connect over USB rather than networked adb, or profile with `profile frames`.'),
249
+ pollCostP50Ms: pollCost.p50Ms,
250
+ focusChangeP50Ms: focusChange.p50Ms,
251
+ ratio,
252
+ });
253
+ }
254
+ }
255
+ // The harness spawns a JVM per keypress. On a memory-constrained TV that is
256
+ // load landing on exactly the resources whose scarcity we are measuring, so
257
+ // it is declared rather than left for the reader to infer.
258
+ const injectionCosts = samples
259
+ .map((s) => s.injectionCostMs)
260
+ .filter((v) => v !== undefined);
261
+ if (injectionCosts.length > 0) {
262
+ const cost = (0, stats_js_1.describe)(injectionCosts);
263
+ if ((0, stats_js_1.hasSamples)(cost) && cost.p50Ms > 100) {
264
+ notes.push({
265
+ code: 'driver-perturbation',
266
+ message: `Each press is injected with \`adb shell input keyevent\`, which spawns an ` +
267
+ `app_process JVM on the device and cost ${cost.p50Ms}ms here. That process starts and ` +
268
+ `tears down alongside the frames being measured, so on a memory-constrained device ` +
269
+ `some of the jank in this window is the harness. pressToFrame excludes the startup ` +
270
+ `time but cannot exclude the contention.`,
271
+ injectionCostMs: cost.p50Ms,
272
+ });
273
+ }
274
+ }
275
+ if (androidTarget && !pressToFrame) {
276
+ notes.push({
277
+ code: 'no-press-to-frame',
278
+ message: 'No frames were attributable to any press, so the device-side measurement is absent. ' +
279
+ 'The app may not be redrawing in response, or --settle is too short.',
280
+ });
281
+ }
282
+ if (outcomes.unchanged > 0) {
283
+ notes.push({
284
+ code: 'boundary-refusals',
285
+ message: `${outcomes.unchanged}/${samples.length} press(es) left focus where it was while focus ` +
286
+ `queries kept succeeding. That is the app declining to move — a rail edge, or a key this ` +
287
+ `screen ignores — not a hang, and it is excluded from the latency figures.`,
288
+ });
289
+ }
290
+ if (outcomes['query-failed'] > 0) {
291
+ notes.push({
292
+ code: 'query-failures',
293
+ message: `${outcomes['query-failed']}/${samples.length} sample(s) could not read the focused ` +
294
+ `element at all. Unlike a boundary refusal this does suggest the app or driver is wedged.`,
295
+ });
296
+ }
297
+ return {
298
+ keys: [...new Set(keys)],
299
+ deviceId,
300
+ platform,
301
+ appId,
302
+ samples,
303
+ outcomes,
304
+ focusChange,
305
+ pressToFrame,
306
+ pollCost,
307
+ byTransition: groupByTransition(samples),
308
+ notes,
309
+ };
310
+ }
311
+ function line(label, d) {
312
+ if (!(0, stats_js_1.hasSamples)(d))
313
+ return ` ${label.padEnd(14)} n/a`;
314
+ return (` ${label.padEnd(14)} p50 ${(0, stats_js_1.fmt)(d.p50Ms)} p90 ${(0, stats_js_1.fmt)(d.p90Ms)} p99 ${(0, stats_js_1.fmt)(d.p99Ms)} ` +
315
+ `max ${(0, stats_js_1.fmt)(d.maxMs)} (n=${d.count}, σ ${(0, stats_js_1.fmt)(d.stddevMs)})`);
316
+ }
317
+ function shortId(key) {
318
+ const [id, , bounds] = key.split('|');
319
+ return id || bounds || key;
320
+ }
321
+ function printLatencyReport(r) {
322
+ console.log(`press-key ${r.keys.join(',')} --measure — ${r.samples.length} sample(s) on ${r.deviceId}`);
323
+ console.log(` outcomes: moved=${r.outcomes.moved} unchanged=${r.outcomes.unchanged} ` +
324
+ `queryFailed=${r.outcomes['query-failed']}`);
325
+ if (r.pressToFrame)
326
+ console.log(line('press→frame', r.pressToFrame));
327
+ console.log(line('focus→move', r.focusChange));
328
+ console.log(line('poll cost', r.pollCost));
329
+ if (r.byTransition.length > 0) {
330
+ console.log('\n by transition (aggregates above mix these together)');
331
+ for (const t of r.byTransition) {
332
+ const primary = (0, stats_js_1.hasSamples)(t.pressToFrame) ? t.pressToFrame : t.focusChange;
333
+ const which = (0, stats_js_1.hasSamples)(t.pressToFrame) ? 'press→frame' : 'focus→move';
334
+ console.log(` ${t.key} ${shortId(t.from)} → ${shortId(t.to)} ` +
335
+ `${which} p50 ${(0, stats_js_1.fmt)(primary.p50Ms)} max ${(0, stats_js_1.fmt)(primary.maxMs)} (n=${t.count})`);
336
+ }
337
+ }
338
+ const withBurst = r.samples.filter((s) => s.response);
339
+ if (withBurst.length > 0) {
340
+ const burst = (0, stats_js_1.describe)(withBurst.map((s) => s.response.renderBurstMs));
341
+ console.log(line('render burst', burst));
342
+ const window = (0, stats_js_1.describe)(withBurst.map((s) => s.response.dispatchWindowMs));
343
+ console.log(` dispatch window p50 ${(0, stats_js_1.fmt)(window.p50Ms)} — press→frame is a lower bound; ` +
344
+ `its upper bound is that much higher.`);
345
+ }
346
+ for (const note of r.notes)
347
+ console.log(`\n note [${note.code}]: ${note.message}`);
348
+ }
@@ -23,6 +23,8 @@ async function listApps(opts = {}, sessionName = 'default') {
23
23
  }
24
24
  const platform = await (0, bootstrap_js_1.detectPlatform)(deviceId);
25
25
  let appIds;
26
+ // Display names, where the platform gives them up cheaply (iOS/tvOS only).
27
+ const appNames = {};
26
28
  if (platform === 'web') {
27
29
  (0, output_js_1.printError)('list-apps is not supported on web. Use foreground-app to get the current URL.', opts);
28
30
  return 1;
@@ -43,6 +45,11 @@ async function listApps(opts = {}, sessionName = 'default') {
43
45
  try {
44
46
  const parsed = JSON.parse(result.stdout);
45
47
  appIds = Object.keys(parsed).sort();
48
+ for (const id of appIds) {
49
+ const name = parsed[id]?.CFBundleDisplayName ?? parsed[id]?.CFBundleName;
50
+ if (typeof name === 'string' && name)
51
+ appNames[id] = name;
52
+ }
46
53
  }
47
54
  catch {
48
55
  (0, output_js_1.printError)('Failed to parse app list from simctl', opts);
@@ -62,7 +69,7 @@ async function listApps(opts = {}, sessionName = 'default') {
62
69
  .sort();
63
70
  }
64
71
  if (opts.json) {
65
- (0, output_js_1.printData)({ status: 'ok', apps: appIds }, opts);
72
+ (0, output_js_1.printData)({ status: 'ok', apps: appIds, ...(Object.keys(appNames).length ? { appNames } : {}) }, opts);
66
73
  }
67
74
  else {
68
75
  for (const id of appIds)
@@ -21,8 +21,25 @@ const cdp_discovery_js_1 = require("../drivers/cdp-discovery.js");
21
21
  // Module-scoped because the discover function returns Device[]; bolting an
22
22
  // extra return field onto the public type would ripple beyond this fix.
23
23
  let listAvdsError;
24
+ /**
25
+ * Android TV identifies itself through `ro.build.characteristics` (or the
26
+ * leanback feature). One cheap getprop per device beats guessing from a model
27
+ * name like `sdk_google_atv_x86` or `AFTKA`.
28
+ */
29
+ async function annotateAndroidFormFactors(devices, ids) {
30
+ await Promise.all(ids.map(async (id) => {
31
+ const device = devices.find((d) => d.id === id);
32
+ if (!device)
33
+ return;
34
+ const res = await (0, runner_js_1.spawnCommand)((0, sdk_js_1.resolveAndroidTool)('adb'), ['-s', id, 'shell', 'getprop', 'ro.build.characteristics'], { env: (0, sdk_js_1.androidSpawnEnv)() });
35
+ if (!res.success)
36
+ return;
37
+ device.formFactor = /\btv\b/i.test(res.stdout) ? 'tv' : 'handset';
38
+ }));
39
+ }
24
40
  async function discoverBootedDevices() {
25
41
  const devices = [];
42
+ const androidIds = [];
26
43
  // Try adb devices (Android)
27
44
  const adb = await (0, runner_js_1.spawnCommand)((0, sdk_js_1.resolveAndroidTool)('adb'), ['devices', '-l'], {
28
45
  env: (0, sdk_js_1.androidSpawnEnv)(),
@@ -40,9 +57,11 @@ async function discoverBootedDevices() {
40
57
  const modelMatch = trimmed.match(/model:(\S+)/);
41
58
  const name = modelMatch ? modelMatch[1].replace(/_/g, ' ') : id;
42
59
  devices.push({ id, name, platform: 'android', status });
60
+ androidIds.push(id);
43
61
  }
44
62
  }
45
63
  }
64
+ await annotateAndroidFormFactors(devices, androidIds);
46
65
  // Try xcrun simctl list (iOS simulators)
47
66
  const xcrun = await (0, runner_js_1.spawnCommand)('xcrun', ['simctl', 'list', 'devices', 'booted', '--json']);
48
67
  if (xcrun.success) {
@@ -143,7 +162,17 @@ async function discoverAvailableDevices() {
143
162
  for (const line of emu.stdout.split('\n')) {
144
163
  const name = line.trim();
145
164
  if (name) {
146
- devices.push({ id: name, name, platform: 'android', status: 'available' });
165
+ // A shut-down AVD can't be probed, so fall back to its name: the AVD
166
+ // wizard calls TV images "Television"/"Android TV" or `_atv_`.
167
+ devices.push({
168
+ id: name,
169
+ name,
170
+ platform: 'android',
171
+ status: 'available',
172
+ formFactor: /(^|[_\s-])(tv|television|atv|leanback)([_\s-]|$)/i.test(name)
173
+ ? 'tv'
174
+ : 'handset',
175
+ });
147
176
  }
148
177
  }
149
178
  }
@@ -29,6 +29,8 @@ async function pidsOnPort(port) {
29
29
  });
30
30
  }
31
31
  async function metroStop(opts, metroOpts) {
32
+ // Deliberately not auto-discovered: this kills whatever listens on the port,
33
+ // and guessing one risks killing a Metro the user did not mean to stop.
32
34
  const port = metroOpts.port ?? 8081;
33
35
  const pids = await pidsOnPort(port);
34
36
  if (pids.length === 0) {
@@ -65,13 +67,13 @@ async function metroStop(opts, metroOpts) {
65
67
  return 0;
66
68
  }
67
69
  async function metroReload(opts, sessionName, metroOpts) {
68
- const port = metroOpts.port ?? 8081;
69
70
  let deviceId;
70
71
  let platform;
71
72
  if (sessionName && sessionName !== 'default') {
72
73
  deviceId = sessionName;
73
74
  platform = await (0, bootstrap_js_1.detectPlatform)(deviceId).catch(() => undefined);
74
75
  }
76
+ const port = await (0, metro_cdp_js_1.resolveMetroPort)({ port: metroOpts.port, deviceId, platform });
75
77
  // Try CDP Page.reload first (works on Hermes/Fusebox).
76
78
  try {
77
79
  await (0, metro_cdp_js_1.cdpCall)('Page.reload', undefined, {
@@ -55,11 +55,11 @@ async function nativeRnSet(args, opts, sessionName, rnOpts) {
55
55
  }
56
56
  const path = args.path.split('.').filter((s) => s.length > 0);
57
57
  const { json } = parseValue(args.value);
58
- const port = rnOpts.port ?? 8081;
59
58
  const { deviceId, platformPromise } = resolveSession(sessionName);
60
59
  const client = new metro_cdp_js_1.MetroCdpClient();
61
60
  try {
62
61
  const platform = await platformPromise;
62
+ const port = await (0, metro_cdp_js_1.resolveMetroPort)({ port: rnOpts.port, deviceId, platform });
63
63
  await client.connect({ port, deviceId, platform, targetIndex: rnOpts.targetIndex });
64
64
  const raw = await client.evaluate((0, metro_scripts_js_1.makeOverridePropsScript)(tag, path, json), true);
65
65
  const res = JSON.parse(raw);
@@ -86,11 +86,11 @@ async function nativeRnProps(args, opts, sessionName, rnOpts) {
86
86
  (0, output_js_1.printError)('native-rn-props needs --react-tag <n> (from native-inspect rn.reactTag)', opts);
87
87
  return 1;
88
88
  }
89
- const port = rnOpts.port ?? 8081;
90
89
  const { deviceId, platformPromise } = resolveSession(sessionName);
91
90
  const client = new metro_cdp_js_1.MetroCdpClient();
92
91
  try {
93
92
  const platform = await platformPromise;
93
+ const port = await (0, metro_cdp_js_1.resolveMetroPort)({ port: rnOpts.port, deviceId, platform });
94
94
  await client.connect({ port, deviceId, platform, targetIndex: rnOpts.targetIndex });
95
95
  const raw = await client.evaluate((0, metro_scripts_js_1.makeRnPropsScript)(tag), true);
96
96
  const res = JSON.parse(raw);
@@ -140,10 +140,10 @@ async function networkLogs(opts, sessionName, netOpts) {
140
140
  return 1;
141
141
  }
142
142
  }
143
- const port = netOpts.port ?? 8081;
144
143
  const { deviceId, platformPromise } = resolveSession(sessionName);
145
144
  try {
146
145
  const platform = await platformPromise;
146
+ const port = await (0, metro_cdp_js_1.resolveMetroPort)({ port: netOpts.port, deviceId, platform });
147
147
  const client = new metro_cdp_js_1.MetroCdpClient();
148
148
  await client.connect({ port, deviceId, platform, targetIndex: netOpts.targetIndex });
149
149
  await client.evaluate(INSTALL_SHIM_SCRIPT);
@@ -227,10 +227,10 @@ async function networkRequest(url, opts, sessionName, reqOpts) {
227
227
  }
228
228
  })()
229
229
  `;
230
- const port = reqOpts.port ?? 8081;
231
230
  const { deviceId, platformPromise } = resolveSession(sessionName);
232
231
  try {
233
232
  const platform = await platformPromise;
233
+ const port = await (0, metro_cdp_js_1.resolveMetroPort)({ port: reqOpts.port, deviceId, platform });
234
234
  const client = new metro_cdp_js_1.MetroCdpClient();
235
235
  await client.connect({ port, deviceId, platform, targetIndex: reqOpts.targetIndex });
236
236
  const result = await client.evaluate(script);