execbro 1.10.23 → 1.10.25
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/README.md +2 -1
- package/build/__tests__/helpers/fake-cdp-server.d.ts +9 -0
- package/build/__tests__/helpers/fake-cdp-server.d.ts.map +1 -1
- package/build/__tests__/helpers/fake-cdp-server.js +20 -2
- package/build/__tests__/helpers/fake-cdp-server.js.map +1 -1
- package/build/__tests__/integration/connection-health.test.js +8 -1
- package/build/__tests__/integration/connection-health.test.js.map +1 -1
- package/build/__tests__/integration/socket-close-midflight.test.d.ts +2 -0
- package/build/__tests__/integration/socket-close-midflight.test.d.ts.map +1 -0
- package/build/__tests__/integration/socket-close-midflight.test.js +106 -0
- package/build/__tests__/integration/socket-close-midflight.test.js.map +1 -0
- package/build/__tests__/integration/stale-target-probe.test.d.ts +2 -0
- package/build/__tests__/integration/stale-target-probe.test.d.ts.map +1 -0
- package/build/__tests__/integration/stale-target-probe.test.js +85 -0
- package/build/__tests__/integration/stale-target-probe.test.js.map +1 -0
- package/build/__tests__/unit/componentSource.test.d.ts +2 -0
- package/build/__tests__/unit/componentSource.test.d.ts.map +1 -0
- package/build/__tests__/unit/componentSource.test.js +67 -0
- package/build/__tests__/unit/componentSource.test.js.map +1 -0
- package/build/__tests__/unit/componentSourceResolve.test.d.ts +2 -0
- package/build/__tests__/unit/componentSourceResolve.test.d.ts.map +1 -0
- package/build/__tests__/unit/componentSourceResolve.test.js +91 -0
- package/build/__tests__/unit/componentSourceResolve.test.js.map +1 -0
- package/build/__tests__/unit/deviceResolution.test.d.ts +2 -0
- package/build/__tests__/unit/deviceResolution.test.d.ts.map +1 -0
- package/build/__tests__/unit/deviceResolution.test.js +108 -0
- package/build/__tests__/unit/deviceResolution.test.js.map +1 -0
- package/build/__tests__/unit/executor.test.js +48 -12
- package/build/__tests__/unit/executor.test.js.map +1 -1
- package/build/__tests__/unit/getLogsEmptyContract.test.d.ts +2 -0
- package/build/__tests__/unit/getLogsEmptyContract.test.d.ts.map +1 -0
- package/build/__tests__/unit/getLogsEmptyContract.test.js +99 -0
- package/build/__tests__/unit/getLogsEmptyContract.test.js.map +1 -0
- package/build/__tests__/unit/inspectorHarvest.test.d.ts +2 -0
- package/build/__tests__/unit/inspectorHarvest.test.d.ts.map +1 -0
- package/build/__tests__/unit/inspectorHarvest.test.js +61 -0
- package/build/__tests__/unit/inspectorHarvest.test.js.map +1 -0
- package/build/__tests__/unit/inspectorSource.test.d.ts +2 -0
- package/build/__tests__/unit/inspectorSource.test.d.ts.map +1 -0
- package/build/__tests__/unit/inspectorSource.test.js +74 -0
- package/build/__tests__/unit/inspectorSource.test.js.map +1 -0
- package/build/__tests__/unit/logDiagnosis.test.d.ts +2 -0
- package/build/__tests__/unit/logDiagnosis.test.d.ts.map +1 -0
- package/build/__tests__/unit/logDiagnosis.test.js +81 -0
- package/build/__tests__/unit/logDiagnosis.test.js.map +1 -0
- package/build/__tests__/unit/pendingExecutions.test.d.ts +2 -0
- package/build/__tests__/unit/pendingExecutions.test.d.ts.map +1 -0
- package/build/__tests__/unit/pendingExecutions.test.js +70 -0
- package/build/__tests__/unit/pendingExecutions.test.js.map +1 -0
- package/build/__tests__/unit/reloadAppAutoConnect.test.d.ts +2 -0
- package/build/__tests__/unit/reloadAppAutoConnect.test.d.ts.map +1 -0
- package/build/__tests__/unit/reloadAppAutoConnect.test.js +63 -0
- package/build/__tests__/unit/reloadAppAutoConnect.test.js.map +1 -0
- package/build/__tests__/unit/selectionBuffer.test.d.ts +2 -0
- package/build/__tests__/unit/selectionBuffer.test.d.ts.map +1 -0
- package/build/__tests__/unit/selectionBuffer.test.js +86 -0
- package/build/__tests__/unit/selectionBuffer.test.js.map +1 -0
- package/build/__tests__/unit/selectionPoller.test.d.ts +2 -0
- package/build/__tests__/unit/selectionPoller.test.d.ts.map +1 -0
- package/build/__tests__/unit/selectionPoller.test.js +109 -0
- package/build/__tests__/unit/selectionPoller.test.js.map +1 -0
- package/build/__tests__/unit/symbolicate.test.d.ts +2 -0
- package/build/__tests__/unit/symbolicate.test.d.ts.map +1 -0
- package/build/__tests__/unit/symbolicate.test.js +154 -0
- package/build/__tests__/unit/symbolicate.test.js.map +1 -0
- package/build/__tests__/unit/transportClassify.test.js +108 -7
- package/build/__tests__/unit/transportClassify.test.js.map +1 -1
- package/build/core/componentSource.d.ts +62 -0
- package/build/core/componentSource.d.ts.map +1 -0
- package/build/core/componentSource.js +268 -0
- package/build/core/componentSource.js.map +1 -0
- package/build/core/connection.d.ts +30 -0
- package/build/core/connection.d.ts.map +1 -1
- package/build/core/connection.js +101 -28
- package/build/core/connection.js.map +1 -1
- package/build/core/debugGlobals.d.ts.map +1 -1
- package/build/core/debugGlobals.js +89 -32
- package/build/core/debugGlobals.js.map +1 -1
- package/build/core/errors.d.ts +7 -1
- package/build/core/errors.d.ts.map +1 -1
- package/build/core/errors.js +8 -1
- package/build/core/errors.js.map +1 -1
- package/build/core/guides.js +1 -1
- package/build/core/index.d.ts +4 -1
- package/build/core/index.d.ts.map +1 -1
- package/build/core/index.js +4 -1
- package/build/core/index.js.map +1 -1
- package/build/core/inspector.d.ts +24 -0
- package/build/core/inspector.d.ts.map +1 -1
- package/build/core/inspector.js +58 -0
- package/build/core/inspector.js.map +1 -1
- package/build/core/jsExecute.d.ts +76 -1
- package/build/core/jsExecute.d.ts.map +1 -1
- package/build/core/jsExecute.js +193 -32
- package/build/core/jsExecute.js.map +1 -1
- package/build/core/logDiagnosis.d.ts +65 -0
- package/build/core/logDiagnosis.d.ts.map +1 -0
- package/build/core/logDiagnosis.js +51 -0
- package/build/core/logDiagnosis.js.map +1 -0
- package/build/core/register.d.ts.map +1 -1
- package/build/core/register.js +22 -6
- package/build/core/register.js.map +1 -1
- package/build/core/selectionBuffer.d.ts +42 -0
- package/build/core/selectionBuffer.d.ts.map +1 -0
- package/build/core/selectionBuffer.js +51 -0
- package/build/core/selectionBuffer.js.map +1 -0
- package/build/core/selectionPoller.d.ts +13 -0
- package/build/core/selectionPoller.d.ts.map +1 -0
- package/build/core/selectionPoller.js +102 -0
- package/build/core/selectionPoller.js.map +1 -0
- package/build/core/state.d.ts +10 -0
- package/build/core/state.d.ts.map +1 -1
- package/build/core/state.js +21 -0
- package/build/core/state.js.map +1 -1
- package/build/core/symbolicate.d.ts +18 -0
- package/build/core/symbolicate.d.ts.map +1 -0
- package/build/core/symbolicate.js +119 -0
- package/build/core/symbolicate.js.map +1 -0
- package/build/core/types.d.ts +8 -0
- package/build/core/types.d.ts.map +1 -1
- package/build/tools/componentTools.d.ts.map +1 -1
- package/build/tools/componentTools.js +78 -9
- package/build/tools/componentTools.js.map +1 -1
- package/build/tools/connectionTools.d.ts.map +1 -1
- package/build/tools/connectionTools.js +10 -4
- package/build/tools/connectionTools.js.map +1 -1
- package/build/tools/executionTools.js +7 -7
- package/build/tools/executionTools.js.map +1 -1
- package/build/tools/interactionTools.d.ts.map +1 -1
- package/build/tools/interactionTools.js +1 -0
- package/build/tools/interactionTools.js.map +1 -1
- package/build/tools/logTools.d.ts.map +1 -1
- package/build/tools/logTools.js +77 -69
- package/build/tools/logTools.js.map +1 -1
- package/build/tools/networkTools.d.ts.map +1 -1
- package/build/tools/networkTools.js +63 -13
- package/build/tools/networkTools.js.map +1 -1
- package/build/tools/screenshotTools.d.ts.map +1 -1
- package/build/tools/screenshotTools.js +3 -0
- package/build/tools/screenshotTools.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { parseStackString, symbolicateFrames } from "./symbolicate.js";
|
|
2
|
+
/** Shared fiber-root lookup. Hermes-safe: var + function only. */
|
|
3
|
+
const ROOTS_SNIPPET = `
|
|
4
|
+
var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
5
|
+
var roots = [];
|
|
6
|
+
if (hook && hook.getFiberRoots) {
|
|
7
|
+
if (hook.renderers) {
|
|
8
|
+
for (var entry of hook.renderers) {
|
|
9
|
+
try { roots = roots.concat(Array.from(hook.getFiberRoots(entry[0]) || [])); } catch (e) {}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
if (roots.length === 0) {
|
|
13
|
+
try { roots = Array.from(hook.getFiberRoots(1) || []); } catch (e) {}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
`;
|
|
17
|
+
/**
|
|
18
|
+
* Cheap per-tick probe. Deliberately does NO measurement — it runs every
|
|
19
|
+
* 500-2000ms and must stay inexpensive. Reports whether RN's inspector is
|
|
20
|
+
* mounted and whether its selection changed since the previous tick.
|
|
21
|
+
*/
|
|
22
|
+
export function buildSelectionProbeExpression() {
|
|
23
|
+
return `
|
|
24
|
+
(function() {
|
|
25
|
+
${ROOTS_SNIPPET}
|
|
26
|
+
if (roots.length === 0) return { active: false, selected: false, changed: false };
|
|
27
|
+
|
|
28
|
+
var panels = [];
|
|
29
|
+
function walk(fiber, depth) {
|
|
30
|
+
if (!fiber || depth > 3000) return;
|
|
31
|
+
if (fiber.type && typeof fiber.type !== 'string' &&
|
|
32
|
+
(fiber.type.displayName || fiber.type.name) === 'InspectorPanel') {
|
|
33
|
+
panels.push(fiber);
|
|
34
|
+
}
|
|
35
|
+
var child = fiber.child;
|
|
36
|
+
while (child) { walk(child, depth + 1); child = child.sibling; }
|
|
37
|
+
}
|
|
38
|
+
for (var r = 0; r < roots.length; r++) { walk(roots[r].current, 0); }
|
|
39
|
+
|
|
40
|
+
if (panels.length === 0) {
|
|
41
|
+
globalThis.__execbro_lastInspected = undefined;
|
|
42
|
+
return { active: false, selected: false, changed: false };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
var panel = null;
|
|
46
|
+
for (var p = 0; p < panels.length; p++) {
|
|
47
|
+
var h = panels[p].memoizedProps && panels[p].memoizedProps.hierarchy;
|
|
48
|
+
if (h && h.length > 0) { panel = panels[p]; break; }
|
|
49
|
+
}
|
|
50
|
+
if (!panel) return { active: true, selected: false, changed: false };
|
|
51
|
+
|
|
52
|
+
var props = panel.memoizedProps;
|
|
53
|
+
var inspected = props.inspected;
|
|
54
|
+
|
|
55
|
+
// RN allocates a fresh object per tap, so identity - not value -
|
|
56
|
+
// distinguishes two consecutive taps on the same element.
|
|
57
|
+
if (inspected === globalThis.__execbro_lastInspected) {
|
|
58
|
+
return { active: true, selected: true, changed: false };
|
|
59
|
+
}
|
|
60
|
+
globalThis.__execbro_lastInspected = inspected;
|
|
61
|
+
|
|
62
|
+
var names = [];
|
|
63
|
+
for (var i = 0; i < props.hierarchy.length; i++) {
|
|
64
|
+
names.push(props.hierarchy[i].name);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
var style = {};
|
|
68
|
+
if (inspected && inspected.style) {
|
|
69
|
+
var list = Array.isArray(inspected.style) ? inspected.style : [inspected.style];
|
|
70
|
+
for (var s = 0; s < list.length; s++) {
|
|
71
|
+
if (list[s] && typeof list[s] === 'object') {
|
|
72
|
+
for (var k in list[s]) { style[k] = list[s][k]; }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
active: true,
|
|
79
|
+
selected: true,
|
|
80
|
+
changed: true,
|
|
81
|
+
element: names.length > 0 ? names[names.length - 1] : 'Unknown',
|
|
82
|
+
path: names.join(' > '),
|
|
83
|
+
hierarchy: names,
|
|
84
|
+
frame: inspected && inspected.frame ? inspected.frame : null,
|
|
85
|
+
style: Object.keys(style).length > 0 ? style : null
|
|
86
|
+
};
|
|
87
|
+
})()
|
|
88
|
+
`;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Hit-tests at (x, y) and harvests raw _debugStack strings up the return chain.
|
|
92
|
+
* Measures host fibers, so this runs only when the probe reports a change -
|
|
93
|
+
* once per tap, never per tick.
|
|
94
|
+
*
|
|
95
|
+
* Prefers _debugSource when present (React < 19, already source coordinates);
|
|
96
|
+
* otherwise emits the raw _debugStack string for host-side symbolication.
|
|
97
|
+
*
|
|
98
|
+
* The cap is deliberately generous. Real trees bury the nearest user component
|
|
99
|
+
* deep behind library wrappers - measured on a production app, a tapped SearchBar
|
|
100
|
+
* sat 12 ancestors above the hit fiber (SVG icon, Pressable and View wrappers in
|
|
101
|
+
* between) and HomeScreen sat at 22. Collapsed library frames are dropped
|
|
102
|
+
* host-side, so a larger walk improves recall without bloating the output; the
|
|
103
|
+
* extra frames cost nothing beyond one batched, cached symbolicate request.
|
|
104
|
+
*/
|
|
105
|
+
export function buildDebugStackHarvestExpression(x, y, maxAncestors = 30) {
|
|
106
|
+
return `
|
|
107
|
+
new Promise(function(resolve) {
|
|
108
|
+
var maxAncestors = ${maxAncestors};
|
|
109
|
+
${ROOTS_SNIPPET}
|
|
110
|
+
if (roots.length === 0) return resolve({ stacks: [], error: 'no-fiber-roots' });
|
|
111
|
+
|
|
112
|
+
function getMeasurable(fiber) {
|
|
113
|
+
var sn = fiber.stateNode;
|
|
114
|
+
if (!sn) return null;
|
|
115
|
+
if (typeof sn.measureInWindow === 'function') return sn;
|
|
116
|
+
if (sn.canonical && sn.canonical.publicInstance &&
|
|
117
|
+
typeof sn.canonical.publicInstance.measureInWindow === 'function') {
|
|
118
|
+
return sn.canonical.publicInstance;
|
|
119
|
+
}
|
|
120
|
+
if (sn.node && globalThis.nativeFabricUIManager &&
|
|
121
|
+
typeof globalThis.nativeFabricUIManager.measureInWindow === 'function') {
|
|
122
|
+
var node = sn.node;
|
|
123
|
+
return {
|
|
124
|
+
measureInWindow: function(cb) {
|
|
125
|
+
try { globalThis.nativeFabricUIManager.measureInWindow(node, cb); } catch (e) {}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
var hostFibers = [];
|
|
133
|
+
function walkFibers(fiber, depth) {
|
|
134
|
+
var cur = fiber;
|
|
135
|
+
while (cur) {
|
|
136
|
+
if (hostFibers.length >= 500) return;
|
|
137
|
+
if (typeof cur.type === 'string' && getMeasurable(cur)) hostFibers.push(cur);
|
|
138
|
+
if (cur.child && depth < 250) walkFibers(cur.child, depth + 1);
|
|
139
|
+
cur = cur.sibling;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
for (var r = 0; r < roots.length; r++) { walkFibers(roots[r].current, 0); }
|
|
143
|
+
if (hostFibers.length === 0) return resolve({ stacks: [], error: 'no-host-fibers' });
|
|
144
|
+
|
|
145
|
+
var measurements = new Array(hostFibers.length).fill(null);
|
|
146
|
+
var pending = hostFibers.length;
|
|
147
|
+
var settled = false;
|
|
148
|
+
|
|
149
|
+
function collect() {
|
|
150
|
+
if (settled) return;
|
|
151
|
+
settled = true;
|
|
152
|
+
|
|
153
|
+
var targetX = ${x};
|
|
154
|
+
var targetY = ${y};
|
|
155
|
+
var hits = [];
|
|
156
|
+
for (var i = 0; i < measurements.length; i++) {
|
|
157
|
+
var m = measurements[i];
|
|
158
|
+
if (m && m.width > 0 && m.height > 0 &&
|
|
159
|
+
targetX >= m.x && targetX <= m.x + m.width &&
|
|
160
|
+
targetY >= m.y && targetY <= m.y + m.height) {
|
|
161
|
+
hits.push({ fiber: hostFibers[i], area: m.width * m.height });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (hits.length === 0) return resolve({ stacks: [], error: 'no-hit' });
|
|
165
|
+
hits.sort(function(a, b) { return a.area - b.area; });
|
|
166
|
+
|
|
167
|
+
var stacks = [];
|
|
168
|
+
var node = hits[0].fiber;
|
|
169
|
+
while (node && stacks.length < maxAncestors) {
|
|
170
|
+
var name = null;
|
|
171
|
+
if (node.type) {
|
|
172
|
+
name = typeof node.type === 'string'
|
|
173
|
+
? node.type
|
|
174
|
+
: (node.type.displayName || node.type.name);
|
|
175
|
+
}
|
|
176
|
+
if (name) {
|
|
177
|
+
var src = node._debugSource;
|
|
178
|
+
if (src && src.fileName) {
|
|
179
|
+
stacks.push({
|
|
180
|
+
component: name,
|
|
181
|
+
stack: '',
|
|
182
|
+
file: src.fileName,
|
|
183
|
+
lineNumber: src.lineNumber,
|
|
184
|
+
column: src.columnNumber
|
|
185
|
+
});
|
|
186
|
+
} else if (node._debugStack && node._debugStack.stack) {
|
|
187
|
+
stacks.push({ component: name, stack: String(node._debugStack.stack) });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
node = node.return;
|
|
191
|
+
}
|
|
192
|
+
resolve({ stacks: stacks });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
hostFibers.forEach(function(fiber, i) {
|
|
196
|
+
try {
|
|
197
|
+
getMeasurable(fiber).measureInWindow(function(fx, fy, fw, fh) {
|
|
198
|
+
measurements[i] = { x: fx, y: fy, width: fw, height: fh };
|
|
199
|
+
if (--pending === 0) collect();
|
|
200
|
+
});
|
|
201
|
+
} catch (e) {
|
|
202
|
+
if (--pending === 0) collect();
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
setTimeout(collect, 300);
|
|
207
|
+
})
|
|
208
|
+
`;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Frame index 1 of a parsed _debugStack is the component's render site - the
|
|
212
|
+
* JSX call site in its parent, which is the line a developer edits. Index 0 is
|
|
213
|
+
* the Error construction inside React's jsx runtime.
|
|
214
|
+
*/
|
|
215
|
+
const RENDER_SITE_FRAME_INDEX = 1;
|
|
216
|
+
export async function resolveStacksToSource(stacks) {
|
|
217
|
+
if (stacks.length === 0) {
|
|
218
|
+
return { source: null, ancestors: [], sourceUnavailable: "no-debug-stack" };
|
|
219
|
+
}
|
|
220
|
+
// React < 19 path: _debugSource already carries source coordinates.
|
|
221
|
+
const direct = [];
|
|
222
|
+
const needsSymbolication = [];
|
|
223
|
+
for (const entry of stacks) {
|
|
224
|
+
if (entry.file && entry.lineNumber !== undefined) {
|
|
225
|
+
direct.push({ component: entry.component, file: entry.file, line: entry.lineNumber });
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const frames = parseStackString(entry.stack, RENDER_SITE_FRAME_INDEX + 1);
|
|
229
|
+
const renderSite = frames[RENDER_SITE_FRAME_INDEX];
|
|
230
|
+
if (renderSite) {
|
|
231
|
+
needsSymbolication.push({ component: entry.component, frame: renderSite });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (needsSymbolication.length === 0) {
|
|
235
|
+
if (direct.length === 0) {
|
|
236
|
+
return { source: null, ancestors: [], sourceUnavailable: "no-debug-stack" };
|
|
237
|
+
}
|
|
238
|
+
const first = direct[0];
|
|
239
|
+
const firstEntry = stacks.find((s) => s.component === first.component);
|
|
240
|
+
return {
|
|
241
|
+
source: { file: first.file, line: first.line, column: firstEntry?.column ?? 0 },
|
|
242
|
+
ancestors: direct,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
const resolved = await symbolicateFrames(needsSymbolication.map((n) => n.frame));
|
|
246
|
+
if (resolved === null) {
|
|
247
|
+
return { source: null, ancestors: [], sourceUnavailable: "symbolicate-unreachable" };
|
|
248
|
+
}
|
|
249
|
+
const ancestors = [...direct];
|
|
250
|
+
let best = null;
|
|
251
|
+
resolved.forEach((frame, i) => {
|
|
252
|
+
const component = needsSymbolication[i]?.component;
|
|
253
|
+
if (!component || frame.collapse)
|
|
254
|
+
return;
|
|
255
|
+
ancestors.push({ component, file: frame.file, line: frame.lineNumber });
|
|
256
|
+
if (!best) {
|
|
257
|
+
best = { file: frame.file, line: frame.lineNumber, column: frame.column };
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
if (!best && direct.length > 0) {
|
|
261
|
+
best = { file: direct[0].file, line: direct[0].line, column: 0 };
|
|
262
|
+
}
|
|
263
|
+
if (!best) {
|
|
264
|
+
return { source: null, ancestors: [], sourceUnavailable: "library-only" };
|
|
265
|
+
}
|
|
266
|
+
return { source: best, ancestors };
|
|
267
|
+
}
|
|
268
|
+
//# sourceMappingURL=componentSource.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"componentSource.js","sourceRoot":"","sources":["../../src/core/componentSource.ts"],"names":[],"mappings":"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"}
|
|
@@ -55,6 +55,36 @@ export declare function getConnectedAppBySimulatorUdid(udid: string): ConnectedA
|
|
|
55
55
|
* multiple Android apps are connected.
|
|
56
56
|
*/
|
|
57
57
|
export declare function getConnectedAppByAndroidDeviceId(deviceId?: string): ConnectedApp | null;
|
|
58
|
+
/**
|
|
59
|
+
* Outcome of matching a caller-supplied `device` string against the currently
|
|
60
|
+
* connected apps. Split out of getConnectedAppByDevice so callers that can
|
|
61
|
+
* *recover* from a miss (reload_app auto-connects, then re-resolves) can
|
|
62
|
+
* inspect the failure instead of catching a thrown UserInputError — a throw
|
|
63
|
+
* on the first resolve is what made reload_app skip its own auto-connect path
|
|
64
|
+
* whenever a device argument was supplied.
|
|
65
|
+
*/
|
|
66
|
+
export type DeviceResolution = {
|
|
67
|
+
kind: "ok";
|
|
68
|
+
app: ConnectedApp;
|
|
69
|
+
} | {
|
|
70
|
+
kind: "ambiguous";
|
|
71
|
+
device: string;
|
|
72
|
+
matches: ConnectedApp[];
|
|
73
|
+
} | {
|
|
74
|
+
kind: "none";
|
|
75
|
+
device?: string;
|
|
76
|
+
connected: ConnectedApp[];
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Human-facing explanation for a `kind: "none"` / `kind: "ambiguous"` resolution.
|
|
80
|
+
* Kept next to the matcher so the message always reflects the matching rules.
|
|
81
|
+
*/
|
|
82
|
+
export declare function describeDeviceResolution(resolution: DeviceResolution): string;
|
|
83
|
+
/**
|
|
84
|
+
* Non-throwing device matcher. Prefer this in call sites that can retry
|
|
85
|
+
* (auto-connect, rescan); use getConnectedAppByDevice when a miss is terminal.
|
|
86
|
+
*/
|
|
87
|
+
export declare function resolveConnectedAppByDevice(device?: string): DeviceResolution;
|
|
58
88
|
export declare function getConnectedAppByDevice(device?: string): ConnectedApp | null;
|
|
59
89
|
export declare function hasConnectedApp(): boolean;
|
|
60
90
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/core/connection.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,IAAI,CAAC;AAC3B,OAAO,EAAE,UAAU,EAAE,YAAY,EAAoB,YAAY,EAAkB,cAAc,EAAsB,sBAAsB,EAAmB,qBAAqB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/core/connection.ts"],"names":[],"mappings":"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"}
|
package/build/core/connection.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import WebSocket from "ws";
|
|
2
|
-
import { connectedApps, pendingExecutions, getNextMessageId, getLogBuffer, getNetworkBuffer, setActiveSimulatorUdid, clearActiveSimulatorIfSource, updateLastCDPMessageTime, getLastCDPMessageTime, clearLastCDPMessageTime } from "./state.js";
|
|
2
|
+
import { connectedApps, pendingExecutions, failPendingExecutionsForSocket, getNextMessageId, getLogBuffer, getNetworkBuffer, setActiveSimulatorUdid, clearActiveSimulatorIfSource, updateLastCDPMessageTime, getLastCDPMessageTime, clearLastCDPMessageTime } from "./state.js";
|
|
3
3
|
import { mapConsoleType } from "./logs.js";
|
|
4
4
|
import { injectNetworkInterceptor, sendNetworkEnable, isInterceptorEvent, applyInterceptedEvent } from "./networkInterceptor.js";
|
|
5
5
|
import { findSimulatorByName } from "./ios.js";
|
|
@@ -9,6 +9,7 @@ import { probeCdpAlive } from "./probe.js";
|
|
|
9
9
|
import { UserInputError } from "./errors.js";
|
|
10
10
|
import { scheduleAppDetection } from "./appDetection.js";
|
|
11
11
|
import { markConnectionEstablished } from "./jsExecute.js";
|
|
12
|
+
import { startSelectionPoller } from "./selectionPoller.js";
|
|
12
13
|
import { DEFAULT_RECONNECTION_CONFIG, MIN_STABLE_CONNECTION_MS, initConnectionState, updateConnectionState, getConnectionState, recordConnectionGap, closeConnectionGap, saveConnectionMetadata, clearConnectionMetadata, getConnectionMetadata, saveReconnectionTimer, cancelReconnectionTimer, calculateBackoffDelay, initContextHealth, markContextHealthy, markContextStale, getContextHealth, updateContextHealth, formatDuration, recordConnectionEvent, } from "./connectionState.js";
|
|
13
14
|
// Connection locks to prevent concurrent connection attempts to the same device
|
|
14
15
|
const connectionLocks = new Set();
|
|
@@ -877,6 +878,14 @@ export async function connectToDevice(device, port, options = {}) {
|
|
|
877
878
|
clearInterval(pingInterval);
|
|
878
879
|
pingInterval = null;
|
|
879
880
|
}
|
|
881
|
+
// Fail anything still in flight on this socket. Otherwise each
|
|
882
|
+
// call parks until its own timeoutMs and then reports a generic
|
|
883
|
+
// "Expression took too long", which classifies as logical and
|
|
884
|
+
// never triggers auto-reconnect.
|
|
885
|
+
const failedCount = failPendingExecutionsForSocket(ws, "WebSocket connection is not open.");
|
|
886
|
+
if (failedCount > 0) {
|
|
887
|
+
console.error(`[execbro] Failed ${failedCount} in-flight call(s) on closed socket for ${device.title}`);
|
|
888
|
+
}
|
|
880
889
|
// Release connection lock if still held
|
|
881
890
|
connectionLocks.delete(appKey);
|
|
882
891
|
connectedApps.delete(appKey);
|
|
@@ -954,6 +963,9 @@ export async function connectToDevice(device, port, options = {}) {
|
|
|
954
963
|
connectionLocks.delete(appKey);
|
|
955
964
|
connectedApps.set(appKey, { ws, deviceInfo: device, port, platform: "android" });
|
|
956
965
|
markConnectionEstablished();
|
|
966
|
+
// Watch RN's Element Inspector for selections made outside an agent
|
|
967
|
+
// request. Idempotent, and a no-op when EXECBRO_DISABLE_SELECTION_POLL=1.
|
|
968
|
+
startSelectionPoller(device.deviceName);
|
|
957
969
|
// Initialize or update connection state
|
|
958
970
|
// Note: We do NOT reset reconnectionAttempts here - that happens
|
|
959
971
|
// only when connection has been stable for MIN_STABLE_CONNECTION_MS
|
|
@@ -1227,27 +1239,85 @@ function normalizeDeviceId(value) {
|
|
|
1227
1239
|
return "";
|
|
1228
1240
|
return value.toLowerCase().replace(/[\s_\-]+/g, "");
|
|
1229
1241
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1242
|
+
/** Guess which platform a caller-supplied device string refers to. */
|
|
1243
|
+
function guessRequestedPlatform(device) {
|
|
1244
|
+
const lower = device.toLowerCase();
|
|
1245
|
+
// Simulator UDIDs are uppercase UUIDs; adb serials are not.
|
|
1246
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(lower))
|
|
1247
|
+
return "ios";
|
|
1248
|
+
if (/iphone|ipad|ipod|\bios\b|simulator/.test(lower))
|
|
1249
|
+
return "ios";
|
|
1250
|
+
if (/emulator-|sdk_gphone|pixel|galaxy|nexus|\bsm-|moto|xiaomi|redmi|oneplus|android|api \d+/.test(lower))
|
|
1251
|
+
return "android";
|
|
1252
|
+
return null;
|
|
1253
|
+
}
|
|
1254
|
+
function deviceLabel(app) {
|
|
1255
|
+
return app.deviceInfo.deviceName || app.deviceInfo.title || "";
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* Human-facing explanation for a `kind: "none"` / `kind: "ambiguous"` resolution.
|
|
1259
|
+
* Kept next to the matcher so the message always reflects the matching rules.
|
|
1260
|
+
*/
|
|
1261
|
+
export function describeDeviceResolution(resolution) {
|
|
1262
|
+
if (resolution.kind === "ok")
|
|
1263
|
+
return "";
|
|
1264
|
+
if (resolution.kind === "ambiguous") {
|
|
1265
|
+
const names = resolution.matches.map(deviceLabel).join(", ");
|
|
1266
|
+
return `Multiple devices match "${resolution.device}": ${names}. Be more specific (use the full device name for an exact match).`;
|
|
1233
1267
|
}
|
|
1234
|
-
const
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1268
|
+
const { device, connected } = resolution;
|
|
1269
|
+
if (!device) {
|
|
1270
|
+
return "No apps connected. Run scan_metro to discover and connect to Metro servers.";
|
|
1271
|
+
}
|
|
1272
|
+
if (connected.length === 0) {
|
|
1273
|
+
return `No connected device matches "${device}". No devices are currently connected — run scan_metro to discover and connect to Metro servers.`;
|
|
1274
|
+
}
|
|
1275
|
+
const quoted = connected.map(a => `"${deviceLabel(a)}"`).join(", ");
|
|
1276
|
+
// Cross-platform mismatch is the dominant shape of this failure: the agent
|
|
1277
|
+
// passes a name from list_ios_simulators / adb devices while only the other
|
|
1278
|
+
// platform is attached to Metro. Saying so beats "retry with one of these".
|
|
1279
|
+
const requested = guessRequestedPlatform(device);
|
|
1280
|
+
const attachedPlatforms = new Set(connected.map(a => a.platform));
|
|
1281
|
+
if (requested && !attachedPlatforms.has(requested)) {
|
|
1282
|
+
const other = requested === "ios" ? "Android" : "iOS";
|
|
1283
|
+
const launchHint = requested === "ios"
|
|
1284
|
+
? "ios_launch_app (boot the simulator first if needed)"
|
|
1285
|
+
: "android_launch_app";
|
|
1286
|
+
return [
|
|
1287
|
+
`No connected device matches "${device}". Only ${other} device(s) are attached to Metro: ${quoted}.`,
|
|
1288
|
+
`The ${requested === "ios" ? "iOS" : "Android"} app is not connected — start it with ${launchHint}, then run scan_metro.`,
|
|
1289
|
+
`Or omit the device argument / use get_apps to target an already-connected device.`
|
|
1290
|
+
].join(" ");
|
|
1291
|
+
}
|
|
1292
|
+
return `No connected device matches "${device}". Connected devices: ${quoted}. Retry with one of these names (substring match, case-insensitive), or run get_apps to list them.`;
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Non-throwing device matcher. Prefer this in call sites that can retry
|
|
1296
|
+
* (auto-connect, rescan); use getConnectedAppByDevice when a miss is terminal.
|
|
1297
|
+
*/
|
|
1298
|
+
export function resolveConnectedAppByDevice(device) {
|
|
1299
|
+
const openApps = [];
|
|
1238
1300
|
for (const [key, app] of connectedApps.entries()) {
|
|
1239
1301
|
if (app.ws.readyState !== WebSocket.OPEN) {
|
|
1240
1302
|
connectedApps.delete(key);
|
|
1241
1303
|
continue;
|
|
1242
1304
|
}
|
|
1243
|
-
|
|
1244
|
-
|
|
1305
|
+
openApps.push(app);
|
|
1306
|
+
}
|
|
1307
|
+
if (!device) {
|
|
1308
|
+
const app = getFirstConnectedApp();
|
|
1309
|
+
return app ? { kind: "ok", app } : { kind: "none", connected: openApps };
|
|
1310
|
+
}
|
|
1311
|
+
const lowerDevice = device.toLowerCase();
|
|
1312
|
+
const normDevice = normalizeDeviceId(device);
|
|
1313
|
+
const matches = [];
|
|
1314
|
+
for (const app of openApps) {
|
|
1245
1315
|
// Match against deviceName + the underlying hardware identifiers, with
|
|
1246
1316
|
// separator-insensitive normalization so "SM_A356N" finds
|
|
1247
1317
|
// "SM-A356N - 15 - API 35" and "emulator-5554" finds the Android app
|
|
1248
1318
|
// attached to that serial.
|
|
1249
1319
|
const haystacks = [
|
|
1250
|
-
normalizeDeviceId(
|
|
1320
|
+
normalizeDeviceId(deviceLabel(app)),
|
|
1251
1321
|
normalizeDeviceId(app.simulatorUdid),
|
|
1252
1322
|
normalizeDeviceId(app.adbSerial)
|
|
1253
1323
|
].filter((s) => s.length > 0);
|
|
@@ -1257,24 +1327,27 @@ export function getConnectedAppByDevice(device) {
|
|
|
1257
1327
|
}
|
|
1258
1328
|
// Prefer exact (case-insensitive) match when present — disambiguates
|
|
1259
1329
|
// "iPhone 17 Pro" from "iPhone 17 Pro Max" when both are connected.
|
|
1260
|
-
const exact = matches.find(a =>
|
|
1261
|
-
const name = a.deviceInfo.deviceName || a.deviceInfo.title || "";
|
|
1262
|
-
return name.toLowerCase() === lowerDevice;
|
|
1263
|
-
});
|
|
1330
|
+
const exact = matches.find(a => deviceLabel(a).toLowerCase() === lowerDevice);
|
|
1264
1331
|
if (exact)
|
|
1265
|
-
return exact;
|
|
1266
|
-
if (matches.length === 1)
|
|
1267
|
-
return matches[0];
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1332
|
+
return { kind: "ok", app: exact };
|
|
1333
|
+
if (matches.length === 1)
|
|
1334
|
+
return { kind: "ok", app: matches[0] };
|
|
1335
|
+
if (matches.length > 1)
|
|
1336
|
+
return { kind: "ambiguous", device, matches };
|
|
1337
|
+
return { kind: "none", device, connected: openApps };
|
|
1338
|
+
}
|
|
1339
|
+
export function getConnectedAppByDevice(device) {
|
|
1340
|
+
const resolution = resolveConnectedAppByDevice(device);
|
|
1341
|
+
if (resolution.kind === "ok")
|
|
1342
|
+
return resolution.app;
|
|
1343
|
+
// No device argument: a miss just means "nothing connected" — callers
|
|
1344
|
+
// handle null themselves (historically getFirstConnectedApp's contract).
|
|
1345
|
+
if (!device)
|
|
1346
|
+
return null;
|
|
1347
|
+
const context = resolution.kind === "ambiguous"
|
|
1348
|
+
? "ambiguous_device"
|
|
1349
|
+
: resolution.connected.length > 0 ? "device_mismatch" : "no_devices_connected";
|
|
1350
|
+
throw new UserInputError(describeDeviceResolution(resolution), context);
|
|
1278
1351
|
}
|
|
1279
1352
|
// Check if any app is connected with an OPEN WebSocket
|
|
1280
1353
|
export function hasConnectedApp() {
|