@houwert/conductor 0.5.0 → 0.6.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/README.md +12 -9
- package/dist/commands/assert-not-visible.js +4 -0
- package/dist/commands/assert-visible.js +4 -0
- package/dist/commands/back.js +4 -0
- package/dist/commands/cheat-sheet.js +11 -8
- package/dist/commands/delete-device.js +222 -0
- package/dist/commands/device-pool.js +33 -22
- package/dist/commands/download-app.js +87 -0
- package/dist/commands/erase-text.js +4 -0
- package/dist/commands/focused.js +39 -0
- package/dist/commands/foreground-app.js +4 -0
- package/dist/commands/hide-keyboard.js +4 -0
- package/dist/commands/inspect.js +8 -0
- package/dist/commands/install.js +103 -27
- package/dist/commands/launch-app.js +6 -0
- package/dist/commands/list-devices.js +39 -2
- package/dist/commands/logs.js +193 -0
- package/dist/commands/press-key.js +16 -0
- package/dist/commands/screenshot.js +1 -1
- package/dist/commands/scroll-until-visible.js +11 -0
- package/dist/commands/scroll.js +6 -0
- package/dist/commands/start-device.js +38 -4
- package/dist/commands/stop-app.js +4 -0
- package/dist/commands/swipe.js +22 -0
- package/dist/commands/tap.js +10 -3
- package/dist/commands/type.js +2 -2
- package/dist/commands/uninstall-app.js +4 -0
- package/dist/daemon/client.js +72 -9
- package/dist/daemon/log-collector.js +408 -0
- package/dist/daemon/server.js +110 -32
- package/dist/daemon/web-server.js +812 -0
- package/dist/device-picker.js +7 -2
- package/dist/drivers/bootstrap.js +124 -1
- package/dist/drivers/element-resolver.js +241 -30
- package/dist/drivers/flow-runner.js +63 -21
- package/dist/drivers/log-sources/android.js +156 -0
- package/dist/drivers/log-sources/daemon.js +112 -0
- package/dist/drivers/log-sources/ios.js +106 -0
- package/dist/drivers/log-sources/metro.js +252 -0
- package/dist/drivers/log-sources/types.js +13 -0
- package/dist/drivers/log-sources/web.js +96 -0
- package/dist/drivers/wait.js +57 -0
- package/dist/drivers/web.js +173 -0
- package/dist/index.js +62 -12
- package/dist/runner.js +32 -2
- package/drivers/ios/conductor-driver-ios.zip +0 -0
- package/drivers/ios/conductor-driver-iosUITests-Runner.zip +0 -0
- package/drivers/tvos/conductor-driver-tvos.zip +0 -0
- package/drivers/tvos/conductor-driver-tvosUITests-Runner.zip +0 -0
- package/package.json +5 -2
- package/skills/conductor/SKILL.md +72 -41
- package/skills/skills.yaml +1 -1
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.LogCollector = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* Daemon-side log collector — manages a platform-specific log source and
|
|
9
|
+
* buffers entries in a circular buffer for HTTP query access.
|
|
10
|
+
*
|
|
11
|
+
* Reuses the existing IOSLogSource / AndroidLogSource classes (which spawn
|
|
12
|
+
* simctl / adb logcat). For web, polls the co-located web-server's
|
|
13
|
+
* /consoleLogs endpoint.
|
|
14
|
+
*
|
|
15
|
+
* Optionally discovers and connects to a Metro dev server for React Native
|
|
16
|
+
* JS-level console logs. This is opt-in: call enableMetro(port) to start
|
|
17
|
+
* background discovery. Metro entries are merged into the same buffer with
|
|
18
|
+
* source='metro'.
|
|
19
|
+
*/
|
|
20
|
+
const http_1 = __importDefault(require("http"));
|
|
21
|
+
const child_process_1 = require("child_process");
|
|
22
|
+
const types_js_1 = require("../drivers/log-sources/types.js");
|
|
23
|
+
const ios_js_1 = require("../drivers/log-sources/ios.js");
|
|
24
|
+
const android_js_1 = require("../drivers/log-sources/android.js");
|
|
25
|
+
const metro_js_1 = require("../drivers/log-sources/metro.js");
|
|
26
|
+
const MAX_BUFFER = 5000;
|
|
27
|
+
const RESTART_DELAY_MS = 2000;
|
|
28
|
+
const WEB_POLL_INTERVAL_MS = 500;
|
|
29
|
+
const METRO_DISCOVERY_INTERVAL_MS = 3000;
|
|
30
|
+
const METRO_AUTO_DISCOVERY_MAX_ATTEMPTS = 10;
|
|
31
|
+
/** Known Metro dev server port ranges. */
|
|
32
|
+
const METRO_PORT_RANGES = [
|
|
33
|
+
[8080, 8099], // Metro default range
|
|
34
|
+
[19000, 19002], // Expo
|
|
35
|
+
];
|
|
36
|
+
function isMetroPort(port) {
|
|
37
|
+
return METRO_PORT_RANGES.some(([lo, hi]) => port >= lo && port <= hi);
|
|
38
|
+
}
|
|
39
|
+
function spawnCapture(cmd, args) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const proc = (0, child_process_1.spawn)(cmd, args, { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
42
|
+
let out = '';
|
|
43
|
+
proc.stdout?.on('data', (chunk) => {
|
|
44
|
+
out += chunk.toString();
|
|
45
|
+
});
|
|
46
|
+
proc.on('close', (code) => code === 0 ? resolve(out) : reject(new Error(`${cmd} failed (${code})`)));
|
|
47
|
+
proc.on('error', reject);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
class LogCollector {
|
|
51
|
+
constructor(platform, deviceId, driverPort, appId, dlog) {
|
|
52
|
+
this.platform = platform;
|
|
53
|
+
this.deviceId = deviceId;
|
|
54
|
+
this.driverPort = driverPort;
|
|
55
|
+
this.appId = appId;
|
|
56
|
+
this.dlog = dlog;
|
|
57
|
+
this.buffer = [];
|
|
58
|
+
this.source = null;
|
|
59
|
+
this.stopped = false;
|
|
60
|
+
this.restartTimer = null;
|
|
61
|
+
// Web polling state
|
|
62
|
+
this.webPollTimer = null;
|
|
63
|
+
this.webSince = new Date().toISOString();
|
|
64
|
+
// Metro auto-discovery state
|
|
65
|
+
this.metroSource = null;
|
|
66
|
+
this.metroDiscoveryTimer = null;
|
|
67
|
+
this.metroPort = null;
|
|
68
|
+
this.metroConnected = false;
|
|
69
|
+
this.metroAutoDiscovery = false;
|
|
70
|
+
this.metroAutoDiscoveryAttempts = 0;
|
|
71
|
+
}
|
|
72
|
+
async start() {
|
|
73
|
+
this.stopped = false;
|
|
74
|
+
if (this.platform === 'web') {
|
|
75
|
+
this.startWebPolling();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
await this.startSource();
|
|
79
|
+
}
|
|
80
|
+
stop() {
|
|
81
|
+
this.stopped = true;
|
|
82
|
+
if (this.restartTimer) {
|
|
83
|
+
clearTimeout(this.restartTimer);
|
|
84
|
+
this.restartTimer = null;
|
|
85
|
+
}
|
|
86
|
+
if (this.webPollTimer) {
|
|
87
|
+
clearTimeout(this.webPollTimer);
|
|
88
|
+
this.webPollTimer = null;
|
|
89
|
+
}
|
|
90
|
+
if (this.metroDiscoveryTimer) {
|
|
91
|
+
clearTimeout(this.metroDiscoveryTimer);
|
|
92
|
+
this.metroDiscoveryTimer = null;
|
|
93
|
+
}
|
|
94
|
+
if (this.source) {
|
|
95
|
+
this.source.disconnect();
|
|
96
|
+
this.source = null;
|
|
97
|
+
}
|
|
98
|
+
if (this.metroSource) {
|
|
99
|
+
this.metroSource.disconnect();
|
|
100
|
+
this.metroSource = null;
|
|
101
|
+
this.metroConnected = false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Enable Metro log collection for React Native apps.
|
|
106
|
+
*
|
|
107
|
+
* When `port` is given, connects directly to that Metro port.
|
|
108
|
+
* When omitted, auto-discovers the Metro port by probing the device:
|
|
109
|
+
* - Android: parses `adb reverse --list` for forwarded Metro ports
|
|
110
|
+
* - iOS/tvOS: scans `lsof` for node listeners in Metro port ranges,
|
|
111
|
+
* then probes `/json` and matches by deviceId (strict — no appId fallback)
|
|
112
|
+
*
|
|
113
|
+
* This is opt-in — only call this for React Native apps.
|
|
114
|
+
*/
|
|
115
|
+
enableMetro(port) {
|
|
116
|
+
if (this.platform === 'web')
|
|
117
|
+
return; // Web already has console logs
|
|
118
|
+
if (port !== undefined) {
|
|
119
|
+
// Explicit port — same as before
|
|
120
|
+
if (this.metroPort === port)
|
|
121
|
+
return;
|
|
122
|
+
this.teardownMetro();
|
|
123
|
+
this.metroAutoDiscovery = false;
|
|
124
|
+
this.metroPort = port;
|
|
125
|
+
this.startMetroDiscovery();
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
// Auto-discover
|
|
129
|
+
if (this.metroAutoDiscovery || this.metroConnected)
|
|
130
|
+
return;
|
|
131
|
+
this.teardownMetro();
|
|
132
|
+
this.metroAutoDiscovery = true;
|
|
133
|
+
this.metroAutoDiscoveryAttempts = 0;
|
|
134
|
+
this.startAutoDiscovery();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
teardownMetro() {
|
|
138
|
+
if (this.metroSource) {
|
|
139
|
+
this.metroSource.disconnect();
|
|
140
|
+
this.metroSource = null;
|
|
141
|
+
this.metroConnected = false;
|
|
142
|
+
}
|
|
143
|
+
if (this.metroDiscoveryTimer) {
|
|
144
|
+
clearTimeout(this.metroDiscoveryTimer);
|
|
145
|
+
this.metroDiscoveryTimer = null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
query(opts = {}) {
|
|
149
|
+
let entries = this.buffer;
|
|
150
|
+
if (opts.since) {
|
|
151
|
+
const since = opts.since;
|
|
152
|
+
entries = entries.filter((e) => e.timestamp > since);
|
|
153
|
+
}
|
|
154
|
+
if (opts.level) {
|
|
155
|
+
const minSeverity = types_js_1.LEVEL_SEVERITY[opts.level] ?? 0;
|
|
156
|
+
entries = entries.filter((e) => (types_js_1.LEVEL_SEVERITY[e.level] ?? 0) >= minSeverity);
|
|
157
|
+
}
|
|
158
|
+
if (opts.limit && opts.limit > 0) {
|
|
159
|
+
// Return the most recent N entries
|
|
160
|
+
entries = entries.slice(-opts.limit);
|
|
161
|
+
}
|
|
162
|
+
return entries;
|
|
163
|
+
}
|
|
164
|
+
pushEntry(entry) {
|
|
165
|
+
this.buffer.push(entry);
|
|
166
|
+
if (this.buffer.length > MAX_BUFFER) {
|
|
167
|
+
this.buffer.splice(0, this.buffer.length - MAX_BUFFER);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async startSource() {
|
|
171
|
+
if (this.stopped)
|
|
172
|
+
return;
|
|
173
|
+
try {
|
|
174
|
+
if (this.platform === 'ios' || this.platform === 'tvos') {
|
|
175
|
+
this.source = new ios_js_1.IOSLogSource(this.deviceId, this.appId);
|
|
176
|
+
}
|
|
177
|
+
else if (this.platform === 'android') {
|
|
178
|
+
this.source = new android_js_1.AndroidLogSource(this.deviceId, this.appId);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
return; // Unsupported platform for device log collection
|
|
182
|
+
}
|
|
183
|
+
this.source.onEntry((entry) => this.pushEntry(entry));
|
|
184
|
+
await this.source.connect();
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// Source failed to start — schedule a retry
|
|
188
|
+
this.scheduleRestart();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
scheduleRestart() {
|
|
192
|
+
if (this.stopped)
|
|
193
|
+
return;
|
|
194
|
+
this.restartTimer = setTimeout(() => {
|
|
195
|
+
this.restartTimer = null;
|
|
196
|
+
this.startSource().catch(() => this.scheduleRestart());
|
|
197
|
+
}, RESTART_DELAY_MS);
|
|
198
|
+
}
|
|
199
|
+
// ── Web polling ──────────────────────────────────────────────────────────
|
|
200
|
+
startWebPolling() {
|
|
201
|
+
const poll = async () => {
|
|
202
|
+
if (this.stopped)
|
|
203
|
+
return;
|
|
204
|
+
try {
|
|
205
|
+
const entries = await this.fetchWebLogs();
|
|
206
|
+
for (const entry of entries) {
|
|
207
|
+
this.pushEntry(entry);
|
|
208
|
+
}
|
|
209
|
+
if (entries.length > 0) {
|
|
210
|
+
this.webSince = entries[entries.length - 1].timestamp;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// Web driver may be restarting — keep polling
|
|
215
|
+
}
|
|
216
|
+
if (!this.stopped) {
|
|
217
|
+
this.webPollTimer = setTimeout(poll, WEB_POLL_INTERVAL_MS);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
poll();
|
|
221
|
+
}
|
|
222
|
+
fetchWebLogs() {
|
|
223
|
+
return new Promise((resolve, reject) => {
|
|
224
|
+
const req = http_1.default.get(`http://127.0.0.1:${this.driverPort}/consoleLogs?since=${encodeURIComponent(this.webSince)}`, (res) => {
|
|
225
|
+
const chunks = [];
|
|
226
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
227
|
+
res.on('end', () => {
|
|
228
|
+
try {
|
|
229
|
+
const data = JSON.parse(Buffer.concat(chunks).toString('utf-8'));
|
|
230
|
+
resolve(data.entries ?? []);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
resolve([]);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
req.setTimeout(5000, () => {
|
|
238
|
+
req.destroy();
|
|
239
|
+
reject(new Error('Timeout polling web console logs'));
|
|
240
|
+
});
|
|
241
|
+
req.on('error', reject);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
// ── Metro auto-discovery ─────────────────────────────────────────────────
|
|
245
|
+
async startAutoDiscovery() {
|
|
246
|
+
if (this.stopped || this.metroConnected)
|
|
247
|
+
return;
|
|
248
|
+
this.metroAutoDiscoveryAttempts++;
|
|
249
|
+
try {
|
|
250
|
+
const port = await this.discoverMetroPort();
|
|
251
|
+
if (port !== null) {
|
|
252
|
+
this.dlog?.(`Metro auto-discovered on port ${port} for device ${this.deviceId}`);
|
|
253
|
+
this.metroPort = port;
|
|
254
|
+
this.startMetroDiscovery();
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
// Discovery failed — retry
|
|
260
|
+
}
|
|
261
|
+
if (this.metroAutoDiscoveryAttempts >= METRO_AUTO_DISCOVERY_MAX_ATTEMPTS) {
|
|
262
|
+
this.dlog?.(`Metro auto-discovery: no Metro instance found for device ${this.deviceId} after ${this.metroAutoDiscoveryAttempts} attempts. Use --metro-port to specify.`);
|
|
263
|
+
this.metroAutoDiscovery = false;
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
// Retry — app may not have started yet
|
|
267
|
+
if (!this.stopped) {
|
|
268
|
+
this.metroDiscoveryTimer = setTimeout(() => {
|
|
269
|
+
this.metroDiscoveryTimer = null;
|
|
270
|
+
this.startAutoDiscovery();
|
|
271
|
+
}, METRO_DISCOVERY_INTERVAL_MS);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Discover the Metro dev server port by probing the device.
|
|
276
|
+
*
|
|
277
|
+
* Android: parse `adb reverse --list` for forwarded ports in Metro ranges.
|
|
278
|
+
* iOS/tvOS: scan `lsof` for node listeners in Metro ranges, probe `/json`,
|
|
279
|
+
* and strictly match by deviceId (no appId/single-target fallback).
|
|
280
|
+
*/
|
|
281
|
+
async discoverMetroPort() {
|
|
282
|
+
if (this.platform === 'android') {
|
|
283
|
+
return this.discoverMetroPortAndroid();
|
|
284
|
+
}
|
|
285
|
+
if (this.platform === 'ios' || this.platform === 'tvos') {
|
|
286
|
+
return this.discoverMetroPortIOS();
|
|
287
|
+
}
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
async discoverMetroPortAndroid() {
|
|
291
|
+
try {
|
|
292
|
+
const output = await spawnCapture('adb', ['-s', this.deviceId, 'reverse', '--list']);
|
|
293
|
+
// Lines look like: host-13 tcp:8082 tcp:8082
|
|
294
|
+
for (const line of output.split('\n')) {
|
|
295
|
+
const match = line.match(/tcp:(\d+)\s+tcp:(\d+)/);
|
|
296
|
+
if (!match)
|
|
297
|
+
continue;
|
|
298
|
+
const hostPort = parseInt(match[2], 10);
|
|
299
|
+
if (isMetroPort(hostPort))
|
|
300
|
+
return hostPort;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
// adb not available or device not connected
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
async discoverMetroPortIOS() {
|
|
309
|
+
try {
|
|
310
|
+
const output = await spawnCapture('lsof', ['-iTCP', '-sTCP:LISTEN', '-n', '-P']);
|
|
311
|
+
const ports = new Set();
|
|
312
|
+
for (const line of output.split('\n')) {
|
|
313
|
+
if (!line.startsWith('node'))
|
|
314
|
+
continue;
|
|
315
|
+
// Column 9 is NAME, e.g. "*:8082" or "[::1]:8082" or "127.0.0.1:8082"
|
|
316
|
+
const match = line.match(/:(\d+)\s/);
|
|
317
|
+
if (!match)
|
|
318
|
+
continue;
|
|
319
|
+
const port = parseInt(match[1], 10);
|
|
320
|
+
if (isMetroPort(port))
|
|
321
|
+
ports.add(port);
|
|
322
|
+
}
|
|
323
|
+
if (ports.size === 0)
|
|
324
|
+
return null;
|
|
325
|
+
// Probe all candidate ports in parallel
|
|
326
|
+
const results = await Promise.all([...ports].map(async (port) => {
|
|
327
|
+
try {
|
|
328
|
+
const targets = await (0, metro_js_1.fetchTargets)(port, 'localhost');
|
|
329
|
+
const withWs = targets.filter((t) => t.webSocketDebuggerUrl);
|
|
330
|
+
// Strict deviceId match only — no appId or single-target fallback
|
|
331
|
+
const match = withWs.find((t) => t.deviceId === this.deviceId || t.reactNative?.logicalDeviceId === this.deviceId);
|
|
332
|
+
return match ? port : null;
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
}));
|
|
338
|
+
return results.find((p) => p !== null) ?? null;
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
// lsof not available
|
|
342
|
+
}
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
startMetroDiscovery() {
|
|
346
|
+
if (this.stopped || this.metroPort === null)
|
|
347
|
+
return;
|
|
348
|
+
this.tryConnectMetro();
|
|
349
|
+
}
|
|
350
|
+
async tryConnectMetro() {
|
|
351
|
+
if (this.stopped || this.metroPort === null || this.metroConnected)
|
|
352
|
+
return;
|
|
353
|
+
try {
|
|
354
|
+
const targets = await (0, metro_js_1.fetchTargets)(this.metroPort, 'localhost');
|
|
355
|
+
const target = this.findTargetForDevice(targets);
|
|
356
|
+
if (!target || !target.webSocketDebuggerUrl) {
|
|
357
|
+
// Target not found yet — app may still be starting. Retry later.
|
|
358
|
+
this.scheduleMetroDiscovery();
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
// Found a matching target — connect
|
|
362
|
+
const targetIndex = targets.filter((t) => t.webSocketDebuggerUrl).indexOf(target);
|
|
363
|
+
this.metroSource = new metro_js_1.MetroLogSource(this.metroPort, 'localhost', targetIndex >= 0 ? targetIndex : undefined);
|
|
364
|
+
this.metroSource.onEntry((entry) => this.pushEntry(entry));
|
|
365
|
+
await this.metroSource.connect();
|
|
366
|
+
this.metroConnected = true;
|
|
367
|
+
this.dlog?.(`Metro connected for device ${this.deviceId} on port ${this.metroPort}`);
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
// Metro not running or connection failed — retry
|
|
371
|
+
this.scheduleMetroDiscovery();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Find a Metro debugger target that matches this daemon's device.
|
|
376
|
+
* Checks deviceId (simulator UDID / emulator serial) first,
|
|
377
|
+
* then falls back to matching by appId if available.
|
|
378
|
+
*/
|
|
379
|
+
findTargetForDevice(targets) {
|
|
380
|
+
const withWs = targets.filter((t) => t.webSocketDebuggerUrl);
|
|
381
|
+
if (withWs.length === 0)
|
|
382
|
+
return undefined;
|
|
383
|
+
// Prefer exact deviceId match (simulator UDID / emulator serial)
|
|
384
|
+
const byDevice = withWs.find((t) => t.deviceId === this.deviceId || t.reactNative?.logicalDeviceId === this.deviceId);
|
|
385
|
+
if (byDevice)
|
|
386
|
+
return byDevice;
|
|
387
|
+
// Fall back to appId match if we know the app
|
|
388
|
+
if (this.appId) {
|
|
389
|
+
const byApp = withWs.find((t) => t.appId === this.appId);
|
|
390
|
+
if (byApp)
|
|
391
|
+
return byApp;
|
|
392
|
+
}
|
|
393
|
+
// Single target — safe to use without matching
|
|
394
|
+
if (withWs.length === 1)
|
|
395
|
+
return withWs[0];
|
|
396
|
+
// Multiple targets, no match — don't guess
|
|
397
|
+
return undefined;
|
|
398
|
+
}
|
|
399
|
+
scheduleMetroDiscovery() {
|
|
400
|
+
if (this.stopped || this.metroConnected)
|
|
401
|
+
return;
|
|
402
|
+
this.metroDiscoveryTimer = setTimeout(() => {
|
|
403
|
+
this.metroDiscoveryTimer = null;
|
|
404
|
+
this.tryConnectMetro();
|
|
405
|
+
}, METRO_DISCOVERY_INTERVAL_MS);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
exports.LogCollector = LogCollector;
|
package/dist/daemon/server.js
CHANGED
|
@@ -14,12 +14,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
14
14
|
*
|
|
15
15
|
* Spawned by: node dist/daemon/server.js [sessionName]
|
|
16
16
|
*/
|
|
17
|
-
const
|
|
17
|
+
const http_1 = __importDefault(require("http"));
|
|
18
|
+
const url_1 = __importDefault(require("url"));
|
|
18
19
|
const fs_1 = __importDefault(require("fs"));
|
|
19
20
|
const path_1 = __importDefault(require("path"));
|
|
20
21
|
const protocol_js_1 = require("./protocol.js");
|
|
21
22
|
const bootstrap_js_1 = require("../drivers/bootstrap.js");
|
|
22
23
|
const android_js_1 = require("../drivers/android.js");
|
|
24
|
+
const web_server_js_1 = require("./web-server.js");
|
|
25
|
+
const log_collector_js_1 = require("./log-collector.js");
|
|
26
|
+
const session_js_1 = require("../session.js");
|
|
23
27
|
const sessionName = process.argv[2] ?? 'default';
|
|
24
28
|
const SOCKET_PATH = (0, protocol_js_1.socketPath)(sessionName);
|
|
25
29
|
const PID_FILE = (0, protocol_js_1.pidFile)(sessionName);
|
|
@@ -36,6 +40,7 @@ function dlog(msg) {
|
|
|
36
40
|
// ── Driver lifecycle ──────────────────────────────────────────────────────────
|
|
37
41
|
let driverPort = 1075;
|
|
38
42
|
let driverPlatform = 'ios';
|
|
43
|
+
let logCollector = null;
|
|
39
44
|
const DRIVER_HEALTH_INTERVAL_MS = 10000; // Check driver health every 10s
|
|
40
45
|
let _restartInProgress = false;
|
|
41
46
|
let _driverStarted = false;
|
|
@@ -50,7 +55,7 @@ async function ensureDriverRunning() {
|
|
|
50
55
|
probe.close();
|
|
51
56
|
}
|
|
52
57
|
else {
|
|
53
|
-
//
|
|
58
|
+
// 'ios', 'tvos', and 'web' all use an HTTP server — port open = alive
|
|
54
59
|
alive = await (0, bootstrap_js_1.isPortOpen)(driverPort);
|
|
55
60
|
}
|
|
56
61
|
if (!alive) {
|
|
@@ -69,6 +74,9 @@ async function ensureDriverRunning() {
|
|
|
69
74
|
// Health-check restart — don't dismiss, to avoid disrupting user's app
|
|
70
75
|
await (0, bootstrap_js_1.startTvOSDriver)(sessionName, driverPort, /* dismissAfterLaunch */ false);
|
|
71
76
|
}
|
|
77
|
+
else if (driverPlatform === 'web') {
|
|
78
|
+
await (0, web_server_js_1.startWebServer)(driverPort, (0, bootstrap_js_1.webBrowserName)(sessionName), dlog);
|
|
79
|
+
}
|
|
72
80
|
else {
|
|
73
81
|
await (0, bootstrap_js_1.startAndroidDriver)(sessionName, driverPort);
|
|
74
82
|
}
|
|
@@ -109,6 +117,10 @@ async function main() {
|
|
|
109
117
|
async function cleanup() {
|
|
110
118
|
if (healthTimer)
|
|
111
119
|
clearInterval(healthTimer);
|
|
120
|
+
if (logCollector) {
|
|
121
|
+
logCollector.stop();
|
|
122
|
+
logCollector = null;
|
|
123
|
+
}
|
|
112
124
|
try {
|
|
113
125
|
fs_1.default.unlinkSync(SOCKET_PATH);
|
|
114
126
|
}
|
|
@@ -140,6 +152,15 @@ async function main() {
|
|
|
140
152
|
if (driverPlatform === 'tvos') {
|
|
141
153
|
dlog('tvOS: leaving driver running to preserve app state');
|
|
142
154
|
}
|
|
155
|
+
else if (driverPlatform === 'web') {
|
|
156
|
+
dlog('Stopping web driver');
|
|
157
|
+
try {
|
|
158
|
+
await (0, web_server_js_1.stopWebServer)();
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
dlog(`Stop web driver error: ${err instanceof Error ? err.message : String(err)}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
143
164
|
else {
|
|
144
165
|
dlog(`Stopping driver on port ${driverPort}`);
|
|
145
166
|
try {
|
|
@@ -176,17 +197,53 @@ async function main() {
|
|
|
176
197
|
}, DRIVER_HEALTH_INTERVAL_MS);
|
|
177
198
|
healthTimer.unref(); // Don't keep the process alive just for health checks
|
|
178
199
|
}
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
200
|
+
// ── HTTP server on Unix socket ─────────────────────────────────────────────
|
|
201
|
+
// Replaces the old raw-TCP accept-and-close with a proper HTTP server so we
|
|
202
|
+
// can serve /status (aliveness) and /logs (buffered log entries).
|
|
203
|
+
function jsonResponse(res, body, status = 200) {
|
|
204
|
+
const json = JSON.stringify(body);
|
|
205
|
+
res.writeHead(status, {
|
|
206
|
+
'Content-Type': 'application/json',
|
|
207
|
+
'Content-Length': Buffer.byteLength(json),
|
|
186
208
|
});
|
|
209
|
+
res.end(json);
|
|
210
|
+
}
|
|
211
|
+
const server = http_1.default.createServer((req, res) => {
|
|
212
|
+
resetIdleTimer();
|
|
213
|
+
const parsed = url_1.default.parse(req.url ?? '/', true);
|
|
214
|
+
if (req.method === 'GET' && parsed.pathname === '/status') {
|
|
215
|
+
jsonResponse(res, { ok: true, platform: driverPlatform, driverPort });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (req.method === 'GET' && parsed.pathname === '/logs') {
|
|
219
|
+
if (!logCollector) {
|
|
220
|
+
jsonResponse(res, { entries: [] });
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const q = parsed.query;
|
|
224
|
+
// Opt-in Metro discovery: ?metro=8081 uses that port directly,
|
|
225
|
+
// ?metro (no value) or ?metro=auto triggers auto-discovery.
|
|
226
|
+
if (q.metro !== undefined) {
|
|
227
|
+
const metroPort = typeof q.metro === 'string' ? parseInt(q.metro, 10) : NaN;
|
|
228
|
+
if (metroPort > 0) {
|
|
229
|
+
logCollector.enableMetro(metroPort);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
logCollector.enableMetro(); // auto-discover
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const entries = logCollector.query({
|
|
236
|
+
since: typeof q.since === 'string' ? q.since : undefined,
|
|
237
|
+
level: typeof q.level === 'string' ? q.level : undefined,
|
|
238
|
+
limit: typeof q.limit === 'string' ? parseInt(q.limit, 10) || undefined : undefined,
|
|
239
|
+
});
|
|
240
|
+
jsonResponse(res, { entries });
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
jsonResponse(res, { error: 'not found' }, 404);
|
|
187
244
|
});
|
|
188
245
|
server.listen(SOCKET_PATH, () => {
|
|
189
|
-
dlog(`socket ready at ${SOCKET_PATH}`);
|
|
246
|
+
dlog(`HTTP socket ready at ${SOCKET_PATH}`);
|
|
190
247
|
resetIdleTimer();
|
|
191
248
|
// Start driver in the background after the socket is ready (so the client
|
|
192
249
|
// doesn't time out waiting for the socket while the driver is starting).
|
|
@@ -204,38 +261,59 @@ async function main() {
|
|
|
204
261
|
probe.close();
|
|
205
262
|
}
|
|
206
263
|
else {
|
|
207
|
-
//
|
|
264
|
+
// 'ios', 'tvos', and 'web' all use an HTTP server — port open = alive
|
|
208
265
|
driverAlive = await (0, bootstrap_js_1.isPortOpen)(driverPort);
|
|
209
266
|
}
|
|
210
267
|
if (driverAlive) {
|
|
211
268
|
_driverStarted = true;
|
|
212
269
|
dlog(`Driver already running on port ${driverPort}`);
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
// Android: install APKs before starting the driver.
|
|
216
|
-
// iOS/tvOS: xcodebuild installs silently via DependentProductPaths.
|
|
217
|
-
if (platform === 'android') {
|
|
218
|
-
dlog(`Installing Android driver on ${sessionName}`);
|
|
219
|
-
await (0, bootstrap_js_1.installDriver)(sessionName);
|
|
220
|
-
dlog(`Driver installation complete`);
|
|
221
270
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
271
|
+
else {
|
|
272
|
+
// Android: install APKs before starting the driver.
|
|
273
|
+
// iOS/tvOS: xcodebuild installs silently via DependentProductPaths.
|
|
274
|
+
// Web: ensure Playwright browser binary is installed.
|
|
275
|
+
if (platform === 'android') {
|
|
276
|
+
dlog(`Installing Android driver on ${sessionName}`);
|
|
277
|
+
await (0, bootstrap_js_1.installDriver)(sessionName);
|
|
278
|
+
dlog(`Driver installation complete`);
|
|
226
279
|
}
|
|
227
|
-
else if (platform === '
|
|
228
|
-
|
|
229
|
-
await (0, bootstrap_js_1.
|
|
280
|
+
else if (platform === 'web') {
|
|
281
|
+
const browser = (0, bootstrap_js_1.webBrowserName)(sessionName);
|
|
282
|
+
await (0, bootstrap_js_1.ensurePlaywrightBrowser)(browser, dlog);
|
|
230
283
|
}
|
|
231
|
-
|
|
232
|
-
|
|
284
|
+
dlog(`Starting ${platform} driver on port ${driverPort}`);
|
|
285
|
+
try {
|
|
286
|
+
if (platform === 'ios') {
|
|
287
|
+
await (0, bootstrap_js_1.startIOSDriver)(sessionName, driverPort);
|
|
288
|
+
}
|
|
289
|
+
else if (platform === 'tvos') {
|
|
290
|
+
// First install — dismiss the runner app to return to homescreen
|
|
291
|
+
await (0, bootstrap_js_1.startTvOSDriver)(sessionName, driverPort, /* dismissAfterLaunch */ true);
|
|
292
|
+
}
|
|
293
|
+
else if (platform === 'web') {
|
|
294
|
+
await (0, web_server_js_1.startWebServer)(driverPort, (0, bootstrap_js_1.webBrowserName)(sessionName), dlog);
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
await (0, bootstrap_js_1.startAndroidDriver)(sessionName, driverPort);
|
|
298
|
+
}
|
|
299
|
+
_driverStarted = true;
|
|
300
|
+
dlog(`Driver started successfully`);
|
|
301
|
+
}
|
|
302
|
+
catch (err) {
|
|
303
|
+
dlog(`Driver startup error: ${err instanceof Error ? err.message : String(err)}`);
|
|
233
304
|
}
|
|
234
|
-
_driverStarted = true;
|
|
235
|
-
dlog(`Driver started successfully`);
|
|
236
305
|
}
|
|
237
|
-
|
|
238
|
-
|
|
306
|
+
// Start collecting logs once the driver is (or was already) running.
|
|
307
|
+
if (_driverStarted) {
|
|
308
|
+
try {
|
|
309
|
+
const session = await (0, session_js_1.getSession)(sessionName);
|
|
310
|
+
logCollector = new log_collector_js_1.LogCollector(platform, sessionName, driverPort, session.appId, dlog);
|
|
311
|
+
await logCollector.start();
|
|
312
|
+
dlog(`Log collector started${session.appId ? ` (appId=${session.appId})` : ''}`);
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
dlog(`Log collector startup error: ${err instanceof Error ? err.message : String(err)}`);
|
|
316
|
+
}
|
|
239
317
|
}
|
|
240
318
|
})
|
|
241
319
|
.catch((err) => {
|