@xenon-device-management/xenon 2.7.0 → 2.8.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/lib/package.json +1 -1
- package/lib/public/assets/{AnnotationOverlay-oISPBknJ.js → AnnotationOverlay-XmDGA9CJ.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-BOm-SKAJ.js → ApiKeyGate-BilRCzB-.js} +1 -1
- package/lib/public/assets/{BugReportButton-C1WMf_3A.js → BugReportButton-_buEV98z.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-BPJ5n5fY.js → DeviceMosaicView-DTEiC4gk.js} +1 -1
- package/lib/public/assets/{EmptyState-CZg80GnN.js → EmptyState-BhQfPUga.js} +1 -1
- package/lib/public/assets/{FieldGroup-D7zftz-q.js → FieldGroup-VPsU3LwN.js} +1 -1
- package/lib/public/assets/{FilterMenu-BKHKlX-8.js → FilterMenu-Br9o7dBS.js} +1 -1
- package/lib/public/assets/{Menu-Ccznqijj.js → Menu-dF_HlxcB.js} +1 -1
- package/lib/public/assets/{Modal-FxdOVK12.js → Modal-51305CEc.js} +1 -1
- package/lib/public/assets/{RecordingPage-BL8B1IKY.js → RecordingPage-BYQfiIEd.js} +1 -1
- package/lib/public/assets/{RecordingsPage-CCCL2xwa.js → RecordingsPage-C7A2ZMU_.js} +1 -1
- package/lib/public/assets/{SegmentedControl-B3EMys8L.js → SegmentedControl--_Zp_x-E.js} +1 -1
- package/lib/public/assets/{SettingCard-BwvdX-C6.js → SettingCard-DKlLEbu9.js} +1 -1
- package/lib/public/assets/{Table-sGNs22pM.js → Table-mknFnLyL.js} +1 -1
- package/lib/public/assets/{activity-rQQOy6SD.js → activity-D4657Amj.js} +1 -1
- package/lib/public/assets/{ai-settings-uIrII3el.js → ai-settings-_sXFJMDz.js} +1 -1
- package/lib/public/assets/{api-keys-B1-QmrZJ.js → api-keys-BV7AQW2N.js} +1 -1
- package/lib/public/assets/{apps-B8gYjYhN.js → apps-D-qPk58H.js} +1 -1
- package/lib/public/assets/{arrow-left-CLT_rU6o.js → arrow-left-B6xZI-lv.js} +1 -1
- package/lib/public/assets/{arrow-right-DqK8eHkB.js → arrow-right-KOusXQT_.js} +1 -1
- package/lib/public/assets/{arrow-up-right-D96XxKQY.js → arrow-up-right--y5SjylA.js} +1 -1
- package/lib/public/assets/{auth-shell-DpBFmfin.js → auth-shell-BOGfjiRi.js} +2 -2
- package/lib/public/assets/{builds-page-DqD7ySkz.js → builds-page-CgobTBVN.js} +1 -1
- package/lib/public/assets/{button-BrVVNvSB.js → button-Ci58y6Ou.js} +1 -1
- package/lib/public/assets/{calendar-FX_no0De.js → calendar-BYLnN-JG.js} +1 -1
- package/lib/public/assets/{check-C6RAxVq9.js → check-BZFRUKhB.js} +1 -1
- package/lib/public/assets/{chevron-left-CcTJUfpS.js → chevron-left-C-olFL5E.js} +1 -1
- package/lib/public/assets/{chevron-right-C39WUDQP.js → chevron-right-CmxzLyEE.js} +1 -1
- package/lib/public/assets/{circle-check-Fo1eEzZe.js → circle-check-Duvlh9lf.js} +1 -1
- package/lib/public/assets/{circle-x-CXy5XzIu.js → circle-x-DoQgpxKQ.js} +1 -1
- package/lib/public/assets/{clock-PBGPHMrI.js → clock-yvaEQMYo.js} +1 -1
- package/lib/public/assets/{copy-ZGa-scbM.js → copy-CIcmMH4s.js} +1 -1
- package/lib/public/assets/{device-explorer-BRYO7iUM.js → device-explorer-Cf-d-udL.js} +1 -1
- package/lib/public/assets/{download-vaODl7Bn.js → download-Ck75G4Sr.js} +1 -1
- package/lib/public/assets/{forgot-password-YfGbiIOM.js → forgot-password-DcOi08XK.js} +1 -1
- package/lib/public/assets/{funnel-C05BIqke.js → funnel-CafvAmsz.js} +1 -1
- package/lib/public/assets/{index-DdsuniRs.js → index-9Npot8Nw.js} +2 -2
- package/lib/public/assets/{index-1ST_YG3o.js → index-QT33_ARg.js} +1 -1
- package/lib/public/assets/{input-C6Zp2Ibb.js → input-DUA3Tk33.js} +1 -1
- package/lib/public/assets/{keycodes-BlkuYXfQ.js → keycodes-Dj17elyp.js} +1 -1
- package/lib/public/assets/{layers-B-3IDp3k.js → layers-5SyoCGaK.js} +1 -1
- package/lib/public/assets/{list-checks-BUUYV0Vf.js → list-checks-Blsk6Sts.js} +1 -1
- package/lib/public/assets/{lock-BUdNqIao.js → lock-DUyg9vcf.js} +1 -1
- package/lib/public/assets/{login-CD8bag9i.js → login-DWRTaaNU.js} +1 -1
- package/lib/public/assets/{maintenance-settings-BWQP_nfK.js → maintenance-settings-CwthiKtl.js} +1 -1
- package/lib/public/assets/{monitor-qfb5f9oQ.js → monitor-D7kOVM-I.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-DAAlUY2l.js → mouse-pointer-2-BzwITYs-.js} +1 -1
- package/lib/public/assets/{network-Qib4-4Yj.js → network-3c26VFLV.js} +1 -1
- package/lib/public/assets/{overview-BZQf20qe.js → overview-BamopQBx.js} +1 -1
- package/lib/public/assets/{page-header-4jNxdU1M.js → page-header-Cr568Bj2.js} +1 -1
- package/lib/public/assets/{plus-DVhPNSQy.js → plus-egFVGGUW.js} +1 -1
- package/lib/public/assets/{profile-page-DfITRDDC.js → profile-page-BetCnNKO.js} +1 -1
- package/lib/public/assets/{reset-password-s2xU05oa.js → reset-password-BgjuXPN-.js} +1 -1
- package/lib/public/assets/{runbook-page-DdEiXQcm.js → runbook-page-BbAREoE4.js} +1 -1
- package/lib/public/assets/{select-Bzs3ZUvb.js → select-BfZw_3u2.js} +1 -1
- package/lib/public/assets/{selector-detail-page-QvRRVchI.js → selector-detail-page-BqswNrpr.js} +1 -1
- package/lib/public/assets/{selector-health-DwUFxrZx.js → selector-health-Cbdio_9h.js} +1 -1
- package/lib/public/assets/{selector-health-page-N0tA9q9c.js → selector-health-page-qdM7e6Lx.js} +1 -1
- package/lib/public/assets/{session-detail-page-DcGGXXxL.js → session-detail-page-DFnhbs82.js} +1 -1
- package/lib/public/assets/{settings-BUqAjzeY.js → settings-Bg81ksOI.js} +1 -1
- package/lib/public/assets/{stat-tile-BwZr7q7_.js → stat-tile-CCR1O7iE.js} +1 -1
- package/lib/public/assets/{tablet-DiOf-kLM.js → tablet-DAHOR-iq.js} +1 -1
- package/lib/public/assets/{teams-DA6AfWGO.js → teams-Bz13DBCK.js} +1 -1
- package/lib/public/assets/{trash-2-BZuuog-n.js → trash-2-BgqAVxUr.js} +1 -1
- package/lib/public/assets/{upload-DLHV2k4Z.js → upload-Rvl2CCKn.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DRY4lLdd.js → use-password-reset-mode--PRWElm2.js} +1 -1
- package/lib/public/assets/{users-BlhcXeTG.js → users-BRkxjzNQ.js} +1 -1
- package/lib/public/assets/{users-Bd4LKBWY.js → users-O123SAYx.js} +1 -1
- package/lib/public/assets/{video-off-DIaDZGNl.js → video-off-DkNxxAWE.js} +1 -1
- package/lib/public/assets/{webhook-settings-IWvqbRk2.js → webhook-settings-D6UmShyI.js} +1 -1
- package/lib/public/assets/{zap-4Qy4hVbF.js → zap-D6UAiMKL.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/src/device-managers/ios/IOSLogStreamService.js +6 -3
- package/lib/src/device-managers/ios/IOSStreamService.js +38 -122
- package/lib/src/device-managers/ios/IOSTunnels.js +376 -0
- package/lib/src/device-managers/ios/WDAClient.js +5 -2
- package/lib/src/services/PortAllocator.js +65 -0
- package/lib/test/unit/ios-go-ios-tunnel-env.spec.js +154 -0
- package/lib/test/unit/ios-stream-appium-tunnel.spec.js +122 -32
- package/lib/test/unit/ios-stream-port-leases.spec.js +26 -5
- package/lib/test/unit/ios-tunnels.spec.js +387 -0
- package/lib/test/unit/port-allocator-pairs.spec.js +110 -0
- package/lib/test/unit/stream-own-device-row.spec.js +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -67,6 +67,7 @@ const singleFlight_1 = require("../../helpers/singleFlight");
|
|
|
67
67
|
const LogcatMultiplexer_1 = require("../android/LogcatMultiplexer");
|
|
68
68
|
const ostraceParse_1 = require("../../services/logcat/ostraceParse");
|
|
69
69
|
const LogcatStreamService_1 = require("../android/LogcatStreamService");
|
|
70
|
+
const IOSTunnels_1 = require("./IOSTunnels");
|
|
70
71
|
/**
|
|
71
72
|
* Which slice of os_trace to stream. These are pushed down to `go-ios ostrace`
|
|
72
73
|
* rather than applied after the fact, because the unfiltered firehose cannot be
|
|
@@ -318,7 +319,9 @@ let IOSLogStreamService = class IOSLogStreamService {
|
|
|
318
319
|
// measured against an iPhone 14, every value tried — bare name, full path,
|
|
319
320
|
// altered case — returned 0 lines, while `--level` alone returned 48,195
|
|
320
321
|
// and `--pid` returned 58,960 for that same process.
|
|
321
|
-
|
|
322
|
+
// On iOS 17+ ostrace reaches the phone through the phone's own go-ios
|
|
323
|
+
// tunnel, found through GO_IOS_AGENT_PORT (see IOSTunnels).
|
|
324
|
+
const proc = this.spawnProcess(goIOS, args, typedi_1.Container.get(IOSTunnels_1.IOSTunnels).envFor(udid));
|
|
322
325
|
// A ChildProcess emitting 'error' with no listener crashes the process.
|
|
323
326
|
proc.on('error', (e) => logger_1.default.warn(`[${udid}] ostrace process error: ${e.message}`));
|
|
324
327
|
(_b = proc.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (d) => logger_1.default.debug(`[${udid}] ostrace: ${d.toString().trim()}`));
|
|
@@ -337,8 +340,8 @@ let IOSLogStreamService = class IOSLogStreamService {
|
|
|
337
340
|
return typedi_1.Container.get(IOSStreamService).goIOSPath;
|
|
338
341
|
});
|
|
339
342
|
}
|
|
340
|
-
spawnProcess(command, args) {
|
|
341
|
-
return (0, child_process_1.spawn)(command, args);
|
|
343
|
+
spawnProcess(command, args, env) {
|
|
344
|
+
return (0, child_process_1.spawn)(command, args, { env });
|
|
342
345
|
}
|
|
343
346
|
/**
|
|
344
347
|
* Executable → bundle id for the installed apps.
|
|
@@ -81,6 +81,7 @@ const device_store_1 = require("../../data-service/device-store");
|
|
|
81
81
|
const ownDeviceRow_1 = require("../ownDeviceRow");
|
|
82
82
|
const iosStreamDiagnostics_1 = require("./iosStreamDiagnostics");
|
|
83
83
|
const tunnelProcess_1 = require("./tunnelProcess");
|
|
84
|
+
const IOSTunnels_1 = require("./IOSTunnels");
|
|
84
85
|
const device_service_1 = require("../../data-service/device-service");
|
|
85
86
|
const manualLock_1 = require("../../services/recording/manualLock");
|
|
86
87
|
const recording_store_1 = require("../../services/recording/recording-store");
|
|
@@ -108,13 +109,6 @@ function shouldStopIdleIosStream(use) {
|
|
|
108
109
|
function heldByAppiumSession(device) {
|
|
109
110
|
return !!(device === null || device === void 0 ? void 0 : device.busy) && !!device.session_id && !(0, manualLock_1.isManualLock)(device.session_id);
|
|
110
111
|
}
|
|
111
|
-
/**
|
|
112
|
-
* The ports go-ios's tunnel process listens on: its tunnel-info API
|
|
113
|
-
* (`GO_IOS_AGENT_PORT`, default 60105) and the first phone's userspace tunnel
|
|
114
|
-
* (API port + 1). On iOS 17+ every go-ios command that talks to the phone,
|
|
115
|
-
* `runwda` included, goes through that process.
|
|
116
|
-
*/
|
|
117
|
-
const GO_IOS_AGENT_PORTS = [60105, 60106];
|
|
118
112
|
/** A start refused a restart because an Appium session holds the phone. */
|
|
119
113
|
class StreamRestartRefused extends Error {
|
|
120
114
|
}
|
|
@@ -151,6 +145,7 @@ let IOSStreamService = class IOSStreamService {
|
|
|
151
145
|
const portAllocator = typedi_2.Container.get(PortAllocator_1.PortAllocator);
|
|
152
146
|
yield portAllocator.touch(session.wdaPort, this.STREAM_PORT_TTL_MS);
|
|
153
147
|
yield portAllocator.touch(session.mjpegPort, this.STREAM_PORT_TTL_MS);
|
|
148
|
+
yield this.tunnels().touch(udid);
|
|
154
149
|
}
|
|
155
150
|
catch (_a) {
|
|
156
151
|
/* best-effort lease refresh */
|
|
@@ -270,128 +265,38 @@ let IOSStreamService = class IOSStreamService {
|
|
|
270
265
|
});
|
|
271
266
|
}
|
|
272
267
|
/**
|
|
273
|
-
*
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const { stdout } = yield execPromise(`"${this.goIOSPath}" info --udid ${udid}`, {
|
|
280
|
-
env: Object.assign(Object.assign({}, process.env), { ENABLE_GO_IOS_AGENT: 'yes' }),
|
|
281
|
-
});
|
|
282
|
-
const info = JSON.parse(stdout);
|
|
283
|
-
const versionStr = String(info.ProductVersion || info.HumanReadableProductVersionString || '0');
|
|
284
|
-
// Extract the first numeric sequence (e.g., "15.8.6" or "iOS 17.2")
|
|
285
|
-
const versionMatch = versionStr.match(/(\d+\.?\d*)/);
|
|
286
|
-
const version = versionMatch ? parseFloat(versionMatch[0]) : 0;
|
|
287
|
-
if (version >= 17) {
|
|
288
|
-
logger_1.default.info(`iOS ${version} detected for ${udid} (Raw: "${versionStr}"). Starting tunnel...`);
|
|
289
|
-
// Check if already running in our session tracker
|
|
290
|
-
const existing = [...this.sessions.values()].find((s) => s.udid === udid && s.tunnelProcess && s.tunnelProcess.exitCode === null);
|
|
291
|
-
if (existing && existing.tunnelProcess)
|
|
292
|
-
return existing.tunnelProcess;
|
|
293
|
-
// CRITICAL: Kill any orphan tunnel processes before starting new one
|
|
294
|
-
// This prevents 'address already in use' errors from stale processes
|
|
295
|
-
yield this.cleanupOrphanTunnels(udid);
|
|
296
|
-
const isolationService = typedi_2.Container.get(ResourceIsolationService_1.ResourceIsolationService);
|
|
297
|
-
const { command, args } = isolationService.wrapSpawn(this.goIOSPath, ['tunnel', 'start', '--udid', udid, '--userspace'], 'Performance');
|
|
298
|
-
// detached: the go-ios tunnel leads its own process group so its
|
|
299
|
-
// self-forking agent children are reaped as a group on cleanup instead
|
|
300
|
-
// of orphaning into a runaway respawn storm. See ./tunnelProcess.
|
|
301
|
-
const tunnelProcess = (0, child_process_1.spawn)(command, args, (0, tunnelProcess_1.tunnelSpawnOptions)(Object.assign(Object.assign({}, process.env), { ENABLE_GO_IOS_AGENT: 'yes' })));
|
|
302
|
-
typedi_2.Container.get(ProcessRegistry_1.ProcessRegistry).track({ kind: 'other', udid, process: tunnelProcess });
|
|
303
|
-
(_a = tunnelProcess.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (data) => logger_1.default.debug(`Tunnel [${udid}]: ${data}`));
|
|
304
|
-
// go-ios repeatedly warns about any connected pre-iOS-17 device (which
|
|
305
|
-
// needs no tunnel). Log that once — attributed to the real target udid,
|
|
306
|
-
// not this tunnel's owner — and suppress the rest to avoid log spam.
|
|
307
|
-
const loggedUnsupported = new Set();
|
|
308
|
-
(_b = tunnelProcess.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (data) => {
|
|
309
|
-
const text = String(data);
|
|
310
|
-
const { unsupported, udid: targetUdid } = (0, iosStreamDiagnostics_1.classifyTunnelStderr)(text);
|
|
311
|
-
if (unsupported) {
|
|
312
|
-
const key = targetUdid !== null && targetUdid !== void 0 ? targetUdid : 'unknown';
|
|
313
|
-
if (!loggedUnsupported.has(key)) {
|
|
314
|
-
loggedUnsupported.add(key);
|
|
315
|
-
logger_1.default.debug(`Tunnel [${udid}]: device ${key} is pre-iOS 17 and needs no tunnel; suppressing repeated go-ios warnings`);
|
|
316
|
-
}
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
logger_1.default.debug(`Tunnel Err [${udid}]: ${text}`);
|
|
320
|
-
});
|
|
321
|
-
// Wait for tunnel to establish by checking the go-ios agent port
|
|
322
|
-
logger_1.default.info('Waiting for tunnel agent on port 60105 to be ready...');
|
|
323
|
-
let tunnelReady = false;
|
|
324
|
-
const tunnelTimeout = 15000;
|
|
325
|
-
const subStartTime = Date.now();
|
|
326
|
-
while (Date.now() - subStartTime < tunnelTimeout) {
|
|
327
|
-
try {
|
|
328
|
-
tunnelReady = yield tcp_port_used_1.default.check(60105, '127.0.0.1');
|
|
329
|
-
if (tunnelReady)
|
|
330
|
-
break;
|
|
331
|
-
}
|
|
332
|
-
catch (e) {
|
|
333
|
-
/* ignore */
|
|
334
|
-
}
|
|
335
|
-
yield new Promise((resolve) => setTimeout(resolve, 1000));
|
|
336
|
-
}
|
|
337
|
-
if (!tunnelReady) {
|
|
338
|
-
logger_1.default.warn(`Tunnel agent port 60105 not ready after ${tunnelTimeout / 1000}s, proceeding anyway...`);
|
|
339
|
-
}
|
|
340
|
-
else {
|
|
341
|
-
logger_1.default.info('Tunnel agent is ready. Settling for 2s...');
|
|
342
|
-
yield new Promise((resolve) => setTimeout(resolve, 2000));
|
|
343
|
-
logger_1.default.info('Tunnel agent settled.');
|
|
344
|
-
}
|
|
345
|
-
return tunnelProcess;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
catch (error) {
|
|
349
|
-
logger_1.default.warn(`Failed to check version or start tunnel for ${udid}: ${error}`);
|
|
350
|
-
}
|
|
351
|
-
return null;
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
/**
|
|
355
|
-
* Kill go-ios tunnel processes left over from earlier runs, so a new tunnel
|
|
356
|
-
* can bind the agent's ports ('address already in use' otherwise).
|
|
268
|
+
* Clear this phone's go-ios leftovers before a start: the tunnel IOSTunnels
|
|
269
|
+
* tracks for it, then any untracked go-ios process for it from an earlier
|
|
270
|
+
* run. Never another phone's. Each phone's tunnel has its own ports, so
|
|
271
|
+
* nothing is killed by port. Through 2.7 this also kill -9'd whatever
|
|
272
|
+
* listened on go-ios's default ports, 60105 and 60106, which was another
|
|
273
|
+
* iPhone's live tunnel.
|
|
357
274
|
*
|
|
358
275
|
* Never while an Appium session holds the phone. That session may be
|
|
359
276
|
* driving a WDA go-ios launched (iOSCapabilities points a session at the
|
|
360
277
|
* stream's WDA whenever a stream runs), and on iOS 17+ that WDA reaches the
|
|
361
|
-
* phone through the tunnel
|
|
362
|
-
*
|
|
363
|
-
*
|
|
278
|
+
* phone through the phone's tunnel. Nothing here can tell that tunnel from
|
|
279
|
+
* an orphan, so the sweep waits for the next stop or start after the
|
|
280
|
+
* session ends; a restart reaps every go-ios process at boot.
|
|
364
281
|
*/
|
|
365
282
|
cleanupOrphanTunnels(udid) {
|
|
366
283
|
return __awaiter(this, void 0, void 0, function* () {
|
|
367
284
|
if (yield this.appiumSessionMayUse(udid, 'go-ios tunnels'))
|
|
368
285
|
return;
|
|
369
286
|
logger_1.default.debug(`Cleaning up orphan tunnels for ${udid}...`);
|
|
287
|
+
yield this.tunnels().stop(udid);
|
|
370
288
|
// Reap the tunnel process *group* for this udid so the self-forking go-ios
|
|
371
289
|
// agent children (whose argv carries no udid, so a udid-scoped pkill can't
|
|
372
290
|
// see them) die with their parent instead of orphaning. Group-scoped, so a
|
|
373
291
|
// second device's tunnel is left untouched. See ./tunnelProcess.
|
|
374
292
|
yield (0, tunnelProcess_1.reapTunnelsForUdid)(udid, execPromise);
|
|
375
|
-
// Whatever still listens on the go-ios agent's ports.
|
|
376
|
-
for (const port of GO_IOS_AGENT_PORTS) {
|
|
377
|
-
try {
|
|
378
|
-
const { stdout } = yield execPromise(`lsof -ti :${port}`);
|
|
379
|
-
const pids = stdout.trim().split('\n');
|
|
380
|
-
for (const pid of pids) {
|
|
381
|
-
if (pid) {
|
|
382
|
-
logger_1.default.debug(`Killing orphan process ${pid} on go-ios agent port ${port}`);
|
|
383
|
-
yield execPromise(`kill -9 ${pid}`);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
catch (err) {
|
|
388
|
-
/* ignore - lsof returns 1 if no port found */
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
293
|
// Small delay to ensure OS releases sockets
|
|
392
294
|
yield new Promise((resolve) => setTimeout(resolve, 500));
|
|
393
295
|
});
|
|
394
296
|
}
|
|
297
|
+
tunnels() {
|
|
298
|
+
return typedi_2.Container.get(IOSTunnels_1.IOSTunnels);
|
|
299
|
+
}
|
|
395
300
|
/**
|
|
396
301
|
* Whether an Appium session may be using what the caller is about to kill:
|
|
397
302
|
* one holds the phone, or the phone's row can't be read, in which case
|
|
@@ -741,7 +646,7 @@ let IOSStreamService = class IOSStreamService {
|
|
|
741
646
|
wdaProcess: null,
|
|
742
647
|
forwardWDAProcess: null,
|
|
743
648
|
forwardMJPEGProcess,
|
|
744
|
-
|
|
649
|
+
tunnelPort: null,
|
|
745
650
|
wdaPort,
|
|
746
651
|
mjpegPort,
|
|
747
652
|
status: 'running',
|
|
@@ -762,7 +667,7 @@ let IOSStreamService = class IOSStreamService {
|
|
|
762
667
|
wdaProcess: null,
|
|
763
668
|
forwardWDAProcess: null,
|
|
764
669
|
forwardMJPEGProcess: null,
|
|
765
|
-
|
|
670
|
+
tunnelPort: null,
|
|
766
671
|
wdaPort,
|
|
767
672
|
mjpegPort,
|
|
768
673
|
status: 'starting',
|
|
@@ -794,8 +699,8 @@ let IOSStreamService = class IOSStreamService {
|
|
|
794
699
|
logger_1.default.warn(`📱 Artisan WDA: Failed to provision WDA to ${udid}: ${e}`);
|
|
795
700
|
}
|
|
796
701
|
}
|
|
797
|
-
// 2.
|
|
798
|
-
session.
|
|
702
|
+
// 2. The phone's own go-ios tunnel, for iOS 17+ (see IOSTunnels)
|
|
703
|
+
session.tunnelPort = yield this.tunnels().ensure(udid);
|
|
799
704
|
// 2. Start Port Forwarding using iproxy (more reliable on Mac)
|
|
800
705
|
logger_1.default.info(`Forwarding ${udid}: ${wdaPort}->8100, ${mjpegPort}->9100 using iproxy`);
|
|
801
706
|
const isolationService = typedi_2.Container.get(ResourceIsolationService_1.ResourceIsolationService);
|
|
@@ -830,7 +735,9 @@ let IOSStreamService = class IOSStreamService {
|
|
|
830
735
|
udid,
|
|
831
736
|
], 'Performance'); // WDA deserves Performance mode
|
|
832
737
|
session.wdaProcess = (0, child_process_1.spawn)(wdaSpawn.command, wdaSpawn.args, {
|
|
833
|
-
|
|
738
|
+
// GO_IOS_AGENT_PORT: runwda reaches this phone through its own
|
|
739
|
+
// tunnel, not go-ios's default 60105.
|
|
740
|
+
env: this.tunnels().envFor(udid),
|
|
834
741
|
});
|
|
835
742
|
typedi_2.Container.get(ProcessRegistry_1.ProcessRegistry).track({ kind: 'wda', udid, process: session.wdaProcess });
|
|
836
743
|
const logDir = path_1.default.join(os_1.default.tmpdir(), 'xenon-logs');
|
|
@@ -1027,7 +934,7 @@ let IOSStreamService = class IOSStreamService {
|
|
|
1027
934
|
*/
|
|
1028
935
|
stopStream(udid_1) {
|
|
1029
936
|
return __awaiter(this, arguments, void 0, function* (udid, opts = {}) {
|
|
1030
|
-
var _a
|
|
937
|
+
var _a;
|
|
1031
938
|
const session = this.sessions.get(udid);
|
|
1032
939
|
if (!session)
|
|
1033
940
|
return;
|
|
@@ -1036,9 +943,7 @@ let IOSStreamService = class IOSStreamService {
|
|
|
1036
943
|
(yield this.appiumSessionMayUse(udid, "the stream's WDA and go-ios tunnel"))) {
|
|
1037
944
|
return;
|
|
1038
945
|
}
|
|
1039
|
-
// Kill sidecar processes.
|
|
1040
|
-
// group), so reap the whole group — a plain p.kill() would leave the
|
|
1041
|
-
// self-forking agent children behind to respawn. See ./tunnelProcess.
|
|
946
|
+
// Kill sidecar processes.
|
|
1042
947
|
[session.wdaProcess, session.forwardWDAProcess, session.forwardMJPEGProcess].forEach((p) => {
|
|
1043
948
|
if (p)
|
|
1044
949
|
try {
|
|
@@ -1048,13 +953,16 @@ let IOSStreamService = class IOSStreamService {
|
|
|
1048
953
|
// ignore
|
|
1049
954
|
}
|
|
1050
955
|
});
|
|
1051
|
-
|
|
956
|
+
// This stream's go-ios tunnel: its whole process group and its port pair.
|
|
957
|
+
// A stream attached to an Appium session's WDA started none.
|
|
958
|
+
if (session.tunnelPort != null)
|
|
959
|
+
yield this.tunnels().stop(udid);
|
|
1052
960
|
// Principal Fix: Only release the device lock if THIS STREAM SERVICE owns it.
|
|
1053
961
|
// The lock could belong to an Appium automation session (session_id is a real UUID).
|
|
1054
962
|
// We should only unblock if it's a manual control lock (session_id starts with 'manual_').
|
|
1055
963
|
try {
|
|
1056
964
|
const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
|
|
1057
|
-
if (device && ((
|
|
965
|
+
if (device && ((_a = device.session_id) === null || _a === void 0 ? void 0 : _a.startsWith('manual_'))) {
|
|
1058
966
|
logger_1.default.info(`Stream Stop: Releasing manual control lock for ${udid}`);
|
|
1059
967
|
yield (0, device_service_1.unblockDevice)(udid, device.host);
|
|
1060
968
|
}
|
|
@@ -1185,7 +1093,7 @@ let IOSStreamService = class IOSStreamService {
|
|
|
1185
1093
|
*/
|
|
1186
1094
|
reapOrphanTunnels() {
|
|
1187
1095
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1188
|
-
var _a;
|
|
1096
|
+
var _a, _b;
|
|
1189
1097
|
try {
|
|
1190
1098
|
const reaped = yield (0, tunnelProcess_1.reapAllOrphanTunnels)(this.goIOSPath, execPromise);
|
|
1191
1099
|
if (reaped > 0) {
|
|
@@ -1195,6 +1103,14 @@ let IOSStreamService = class IOSStreamService {
|
|
|
1195
1103
|
catch (err) {
|
|
1196
1104
|
logger_1.default.warn(`[IOSStreamService] Orphan tunnel reap failed: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
1197
1105
|
}
|
|
1106
|
+
// Every go-ios process is gone, so no tunnel holds its ports. A lease left
|
|
1107
|
+
// by an earlier run would otherwise keep its pair for up to 1.5 hours.
|
|
1108
|
+
try {
|
|
1109
|
+
yield typedi_2.Container.get(PortAllocator_1.PortAllocator).releasePurpose('tunnel');
|
|
1110
|
+
}
|
|
1111
|
+
catch (err) {
|
|
1112
|
+
logger_1.default.warn(`[IOSStreamService] Releasing tunnel port leases failed: ${(_b = err === null || err === void 0 ? void 0 : err.message) !== null && _b !== void 0 ? _b : err}`);
|
|
1113
|
+
}
|
|
1198
1114
|
});
|
|
1199
1115
|
}
|
|
1200
1116
|
/**
|