@xenon-device-management/xenon 2.8.3 → 2.9.1
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-DZrFxYPZ.js → AnnotationOverlay-Dm_URoOL.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-DkyW0B7G.js → ApiKeyGate-BdGOuXDm.js} +1 -1
- package/lib/public/assets/{BugReportButton-TyYrNWYM.js → BugReportButton-BuWGEjgw.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-DY__bWHx.js → DeviceMosaicView-Pu3rJ99D.js} +1 -1
- package/lib/public/assets/{EmptyState-CfkN-nUG.js → EmptyState-syyHElMd.js} +1 -1
- package/lib/public/assets/{FieldGroup-DQu6Zo15.js → FieldGroup-BZR5ZBVv.js} +1 -1
- package/lib/public/assets/{FilterMenu-BT_Q7-4t.js → FilterMenu-CsgtrmPb.js} +1 -1
- package/lib/public/assets/{Menu-DfwEC8pG.js → Menu-DP8XOm1A.js} +1 -1
- package/lib/public/assets/{Modal-DXyaClqh.js → Modal-Dtd-8uI2.js} +1 -1
- package/lib/public/assets/{RecordingPage-j7H4_iT1.js → RecordingPage-CdAtAmHj.js} +1 -1
- package/lib/public/assets/{RecordingsPage-ST7B99o1.js → RecordingsPage-Br3CKxTe.js} +1 -1
- package/lib/public/assets/{SegmentedControl-DaqYH9BW.js → SegmentedControl-BOaNmUL-.js} +1 -1
- package/lib/public/assets/{SettingCard-CWTz_r_8.js → SettingCard-CQuK9ZZX.js} +1 -1
- package/lib/public/assets/{Table-BDO5qlOf.js → Table-CswCefnT.js} +1 -1
- package/lib/public/assets/{activity-CGdR3Ff6.js → activity-6fhH3u4C.js} +1 -1
- package/lib/public/assets/{ai-settings-C8I0KSUg.js → ai-settings-CU24L3D4.js} +1 -1
- package/lib/public/assets/{api-keys-BhbZQpGV.js → api-keys-DC3BZDaG.js} +1 -1
- package/lib/public/assets/{apps-DaBl9nMx.js → apps-Ce5Z-wNc.js} +1 -1
- package/lib/public/assets/{arrow-left-WRIrign1.js → arrow-left-DZJqYn7r.js} +1 -1
- package/lib/public/assets/{arrow-right-BuIztKwL.js → arrow-right-nIvK8CyN.js} +1 -1
- package/lib/public/assets/{arrow-up-right-Cz072oos.js → arrow-up-right-BfugJj3f.js} +1 -1
- package/lib/public/assets/{auth-shell-BS884uL2.js → auth-shell-B1Cz75Vq.js} +2 -2
- package/lib/public/assets/{builds-page-B05iZMmN.js → builds-page-5mlJGjIk.js} +1 -1
- package/lib/public/assets/{button-DjeIU-Ty.js → button-D_W88sn-.js} +1 -1
- package/lib/public/assets/{calendar-DUF9u2PF.js → calendar-Dc6pWZS9.js} +1 -1
- package/lib/public/assets/{check-DUR92DEE.js → check-BbvtnP8m.js} +1 -1
- package/lib/public/assets/{chevron-left-CRvlIuad.js → chevron-left-DQDpR9ne.js} +1 -1
- package/lib/public/assets/{chevron-right-iRDZ5v7C.js → chevron-right-CgBUaRCA.js} +1 -1
- package/lib/public/assets/{circle-check-CqDvbZtW.js → circle-check-BWjEKvA4.js} +1 -1
- package/lib/public/assets/{circle-x-CsYtRLC8.js → circle-x-BLUdeRhT.js} +1 -1
- package/lib/public/assets/{clock-DB9ihtTE.js → clock-PAhKG1jT.js} +1 -1
- package/lib/public/assets/{copy-BufiyibK.js → copy-2CHdAPBf.js} +1 -1
- package/lib/public/assets/{device-explorer-BapnPGYt.js → device-explorer-EyAhEL_S.js} +1 -1
- package/lib/public/assets/{download-D14AYHRg.js → download-B1vKGhGP.js} +1 -1
- package/lib/public/assets/{forgot-password-2PqpH7ph.js → forgot-password-NSrrBA5N.js} +1 -1
- package/lib/public/assets/{funnel-CgV-CZex.js → funnel-C9mzkCSI.js} +1 -1
- package/lib/public/assets/{index-BE8OC-Wj.js → index-BEk1D5-B.js} +1 -1
- package/lib/public/assets/{index-CXxjosDd.js → index-BxaH84UO.js} +2 -2
- package/lib/public/assets/{input-DPn63eF4.js → input-cA_lrNIw.js} +1 -1
- package/lib/public/assets/{keycodes-BYgvJDTE.js → keycodes-DZLCZ-uv.js} +1 -1
- package/lib/public/assets/{layers-DJZQUOez.js → layers-BuXMIJz3.js} +1 -1
- package/lib/public/assets/{list-checks-YM2_VNMv.js → list-checks-B8q68tPG.js} +1 -1
- package/lib/public/assets/{lock-Cy-A3o47.js → lock-ChoclF8X.js} +1 -1
- package/lib/public/assets/{login-CRZ2J7Up.js → login-63DiXJ0M.js} +1 -1
- package/lib/public/assets/{maintenance-settings-4HkUk10V.js → maintenance-settings-A47Y-PYy.js} +1 -1
- package/lib/public/assets/{monitor-Cyvro78e.js → monitor-BYPF43Ds.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-26YTYUOD.js → mouse-pointer-2-Pcl7inmZ.js} +1 -1
- package/lib/public/assets/{network-D-W1VWM_.js → network-ChaLfnWe.js} +1 -1
- package/lib/public/assets/{overview-pVmHrKhk.js → overview-bj_vktQG.js} +1 -1
- package/lib/public/assets/{page-header-DsTokPlH.js → page-header-bv2AQSMG.js} +1 -1
- package/lib/public/assets/{plus-OjIdQkG_.js → plus-u0x2Y5uW.js} +1 -1
- package/lib/public/assets/{profile-page-D0jy_p5L.js → profile-page-Cvmef2iY.js} +1 -1
- package/lib/public/assets/{reset-password-DptmkgSN.js → reset-password-DqStVhBo.js} +1 -1
- package/lib/public/assets/{runbook-page-9QcpCPUo.js → runbook-page-UK0C95QF.js} +1 -1
- package/lib/public/assets/{select-CHKzkHre.js → select-CQ6O804R.js} +1 -1
- package/lib/public/assets/{selector-detail-page-P5zDXMZC.js → selector-detail-page-D-PjU6mC.js} +1 -1
- package/lib/public/assets/{selector-health-Buz4eRW1.js → selector-health-CVm0cTOU.js} +1 -1
- package/lib/public/assets/{selector-health-page-BHZfbxGg.js → selector-health-page-BdibBs8g.js} +1 -1
- package/lib/public/assets/{session-detail-page-JHU-GKSX.js → session-detail-page-BijicGOF.js} +1 -1
- package/lib/public/assets/{settings-CRTRggHW.js → settings-Cva7V4n-.js} +1 -1
- package/lib/public/assets/{stat-tile-Be2twadZ.js → stat-tile-BdIoEoou.js} +1 -1
- package/lib/public/assets/{tablet-C7QkOkHs.js → tablet-BPtlTsKn.js} +1 -1
- package/lib/public/assets/{teams-CwVhnDoL.js → teams-D4r1UMME.js} +1 -1
- package/lib/public/assets/{trash-2-yjlULOrf.js → trash-2-CnysPtdR.js} +1 -1
- package/lib/public/assets/{upload-BcrYG7kK.js → upload-Uyj4-Cyq.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-BhRH2TTL.js → use-password-reset-mode-Du-ykGHP.js} +1 -1
- package/lib/public/assets/{users-D3jRTB5K.js → users-Cs00GNmy.js} +1 -1
- package/lib/public/assets/{users-BSZL_J7x.js → users-ttkwKBos.js} +1 -1
- package/lib/public/assets/{video-off-BwbiJtmA.js → video-off-BYa4qD8N.js} +1 -1
- package/lib/public/assets/{webhook-settings-CTIdd7DN.js → webhook-settings-DuEnPlet.js} +1 -1
- package/lib/public/assets/{zap-CZK3Cbup.js → zap-CxHn6Vwu.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/src/device-managers/ios/IOSDiscoveryService.js +2 -4
- package/lib/src/device-managers/ios/IOSStreamService.js +12 -14
- package/lib/src/device-managers/ios/IOSTunnels.js +127 -34
- package/lib/src/device-managers/ios/WDAClient.js +18 -4
- package/lib/src/device-managers/ios/goIosBinary.js +16 -0
- package/lib/src/plugin.js +2 -10
- package/lib/src/services/PortAllocator.js +8 -1
- package/lib/src/services/ServerManager.js +22 -0
- package/lib/test/unit/PortAllocator.test.js +1 -1
- package/lib/test/unit/boot-go-ios-reap.spec.js +80 -0
- package/lib/test/unit/ios-go-ios-tunnel-env.spec.js +38 -1
- package/lib/test/unit/ios-stream-appium-tunnel.spec.js +10 -1
- package/lib/test/unit/ios-stream-port-leases.spec.js +0 -1
- package/lib/test/unit/ios-tunnels.spec.js +91 -3
- package/lib/test/unit/port-allocator-pairs.spec.js +34 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -74,7 +74,6 @@ const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
|
74
74
|
const tcp_port_used_1 = __importDefault(require("tcp-port-used"));
|
|
75
75
|
const InternalHttpClient_1 = require("../../InternalHttpClient");
|
|
76
76
|
const logger_1 = __importDefault(require("../../logger"));
|
|
77
|
-
const helpers_1 = require("../../helpers");
|
|
78
77
|
const singleFlight_1 = require("../../helpers/singleFlight");
|
|
79
78
|
const PortAllocator_1 = require("../../services/PortAllocator");
|
|
80
79
|
const device_store_1 = require("../../data-service/device-store");
|
|
@@ -82,6 +81,7 @@ const ownDeviceRow_1 = require("../ownDeviceRow");
|
|
|
82
81
|
const iosStreamDiagnostics_1 = require("./iosStreamDiagnostics");
|
|
83
82
|
const tunnelProcess_1 = require("./tunnelProcess");
|
|
84
83
|
const IOSTunnels_1 = require("./IOSTunnels");
|
|
84
|
+
const goIosBinary_1 = require("./goIosBinary");
|
|
85
85
|
const device_service_1 = require("../../data-service/device-service");
|
|
86
86
|
const manualLock_1 = require("../../services/recording/manualLock");
|
|
87
87
|
const recording_store_1 = require("../../services/recording/recording-store");
|
|
@@ -119,11 +119,7 @@ let IOSStreamService = class IOSStreamService {
|
|
|
119
119
|
this.startFlight = new singleFlight_1.SingleFlight();
|
|
120
120
|
this.recoveryCooldowns = new Map(); // Track last recovery attempt time
|
|
121
121
|
this.RECOVERY_COOLDOWN_MS = 30000; // 30s cooldown between recovery attempts
|
|
122
|
-
|
|
123
|
-
// which refreshes it each tick, so a long-lived stream's port never expires
|
|
124
|
-
// and gets reallocated to another device; stopStream() releases it explicitly.
|
|
125
|
-
this.STREAM_PORT_TTL_MS = 90 * 60 * 1000; // 1.5h
|
|
126
|
-
this.goIOSPath = this.getGoIOSPath();
|
|
122
|
+
this.goIOSPath = (0, goIosBinary_1.goIosBinaryPath)();
|
|
127
123
|
this.startWatchdog();
|
|
128
124
|
}
|
|
129
125
|
/**
|
|
@@ -143,8 +139,8 @@ let IOSStreamService = class IOSStreamService {
|
|
|
143
139
|
// its ports to another device mid-stream (TTL > this 1h interval).
|
|
144
140
|
try {
|
|
145
141
|
const portAllocator = typedi_2.Container.get(PortAllocator_1.PortAllocator);
|
|
146
|
-
yield portAllocator.touch(session.wdaPort,
|
|
147
|
-
yield portAllocator.touch(session.mjpegPort,
|
|
142
|
+
yield portAllocator.touch(session.wdaPort, IOSTunnels_1.IOS_STREAM_LEASE_TTL_MS);
|
|
143
|
+
yield portAllocator.touch(session.mjpegPort, IOSTunnels_1.IOS_STREAM_LEASE_TTL_MS);
|
|
148
144
|
yield this.tunnels().touch(udid);
|
|
149
145
|
}
|
|
150
146
|
catch (_a) {
|
|
@@ -223,10 +219,6 @@ let IOSStreamService = class IOSStreamService {
|
|
|
223
219
|
}
|
|
224
220
|
});
|
|
225
221
|
}
|
|
226
|
-
getGoIOSPath() {
|
|
227
|
-
const goIOSDir = (0, helpers_1.cachePath)('goIOS');
|
|
228
|
-
return path_1.default.join(goIOSDir, 'ios');
|
|
229
|
-
}
|
|
230
222
|
/**
|
|
231
223
|
* Check if go-ios binary exists
|
|
232
224
|
*/
|
|
@@ -283,6 +275,10 @@ let IOSStreamService = class IOSStreamService {
|
|
|
283
275
|
return __awaiter(this, void 0, void 0, function* () {
|
|
284
276
|
if (yield this.appiumSessionMayUse(udid, 'go-ios tunnels'))
|
|
285
277
|
return;
|
|
278
|
+
// A tunnel opened for screenshots is live and tracked, not a leftover:
|
|
279
|
+
// the start's ensure takes it over (IOSTunnels.borrow).
|
|
280
|
+
if (this.tunnels().isOnDemand(udid))
|
|
281
|
+
return;
|
|
286
282
|
logger_1.default.debug(`Cleaning up orphan tunnels for ${udid}...`);
|
|
287
283
|
yield this.tunnels().stop(udid);
|
|
288
284
|
// Reap the tunnel process *group* for this udid so the self-forking go-ios
|
|
@@ -590,7 +586,7 @@ let IOSStreamService = class IOSStreamService {
|
|
|
590
586
|
// guarantee a non-colliding port; the lease is refreshed by the watchdog
|
|
591
587
|
// and released in stopStream().
|
|
592
588
|
const portAllocator = typedi_2.Container.get(PortAllocator_1.PortAllocator);
|
|
593
|
-
const ttl = { ttlMs:
|
|
589
|
+
const ttl = { ttlMs: IOSTunnels_1.IOS_STREAM_LEASE_TTL_MS };
|
|
594
590
|
// Principal Discovery: attach to the WDA an Appium session runs on this
|
|
595
591
|
// device instead of launching a second one over it. The one exception
|
|
596
592
|
// to the rule above: that session's forwarder listens on the Device
|
|
@@ -1097,7 +1093,9 @@ let IOSStreamService = class IOSStreamService {
|
|
|
1097
1093
|
* server owns no legitimate tunnel — i.e. on fresh boot (orphans from a
|
|
1098
1094
|
* previous run, or a hard-killed / crashed process whose graceful cleanup
|
|
1099
1095
|
* never ran) or during full shutdown. This is the catch-all that stops the
|
|
1100
|
-
* self-forking go-ios agent storm from surviving across restarts.
|
|
1096
|
+
* self-forking go-ios agent storm from surviving across restarts. It kills
|
|
1097
|
+
* this process's own go-ios calls too, so boot runs it before device
|
|
1098
|
+
* detection starts (ServerManager.reapLeftoverGoIos).
|
|
1101
1099
|
*/
|
|
1102
1100
|
reapOrphanTunnels() {
|
|
1103
1101
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -18,7 +18,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
18
18
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
19
|
};
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
-
exports.IOSTunnels = exports.TUNNEL_CHECK_MS = exports.
|
|
21
|
+
exports.IOSTunnels = exports.TUNNEL_BORROW_IDLE_MS = exports.TUNNEL_CHECK_MS = exports.IOS_STREAM_LEASE_TTL_MS = exports.TUNNEL_READY_POLL_MS = exports.TUNNEL_READY_TIMEOUT_MS = void 0;
|
|
22
22
|
exports.iosVersionOf = iosVersionOf;
|
|
23
23
|
exports.goIosReason = goIosReason;
|
|
24
24
|
/**
|
|
@@ -48,27 +48,35 @@ exports.goIosReason = goIosReason;
|
|
|
48
48
|
const typedi_1 = require("typedi");
|
|
49
49
|
const child_process_1 = require("child_process");
|
|
50
50
|
const http_1 = __importDefault(require("http"));
|
|
51
|
-
const path_1 = __importDefault(require("path"));
|
|
52
51
|
const util_1 = require("util");
|
|
53
52
|
const logger_1 = __importDefault(require("../../logger"));
|
|
54
|
-
const helpers_1 = require("../../helpers");
|
|
55
53
|
const PortAllocator_1 = require("../../services/PortAllocator");
|
|
56
54
|
const ProcessRegistry_1 = require("../../services/ProcessRegistry");
|
|
57
55
|
const ResourceIsolationService_1 = require("../../services/ResourceIsolationService");
|
|
58
56
|
const iosStreamDiagnostics_1 = require("./iosStreamDiagnostics");
|
|
57
|
+
const goIosBinary_1 = require("./goIosBinary");
|
|
59
58
|
const tunnelProcess_1 = require("./tunnelProcess");
|
|
60
59
|
const execPromise = (0, util_1.promisify)(child_process_1.exec);
|
|
61
60
|
/** How long a new tunnel has to answer for its phone before the start goes on without it. */
|
|
62
61
|
exports.TUNNEL_READY_TIMEOUT_MS = 20000;
|
|
63
62
|
exports.TUNNEL_READY_POLL_MS = 500;
|
|
64
63
|
/**
|
|
65
|
-
*
|
|
66
|
-
* (IOSStreamService's
|
|
67
|
-
*
|
|
64
|
+
* How long an iPhone stream's port leases last: its WDA and MJPEG ports
|
|
65
|
+
* (IOSStreamService) and its tunnel's pair. Longer than the stream watchdog's
|
|
66
|
+
* hourly tick, which refreshes them (the tunnel's through
|
|
67
|
+
* {@link IOSTunnels.touch}), so a long stream never loses its ports to another
|
|
68
|
+
* phone. Stopping the stream or the tunnel releases them.
|
|
68
69
|
*/
|
|
69
|
-
exports.
|
|
70
|
+
exports.IOS_STREAM_LEASE_TTL_MS = 90 * 60 * 1000;
|
|
70
71
|
/** How often running tunnels are checked for a phone that went away. */
|
|
71
72
|
exports.TUNNEL_CHECK_MS = 5000;
|
|
73
|
+
/**
|
|
74
|
+
* How long a tunnel opened for screenshots stays after the last one: long
|
|
75
|
+
* enough that a client taking screenshots in a loop pays the start once.
|
|
76
|
+
*/
|
|
77
|
+
exports.TUNNEL_BORROW_IDLE_MS = 2 * 60 * 1000;
|
|
78
|
+
/** How long a phone's iOS version is trusted before `ios info` is asked again. */
|
|
79
|
+
const VERSION_CACHE_MS = 10 * 60 * 1000;
|
|
72
80
|
/** The iOS version `ios info` reports, as a number: 17.2 for "17.2.1" or "iOS 17.2", 0 for none. */
|
|
73
81
|
function iosVersionOf(info) {
|
|
74
82
|
const versionStr = String(info.ProductVersion || info.HumanReadableProductVersionString || '0');
|
|
@@ -118,32 +126,51 @@ let IOSTunnels = class IOSTunnels {
|
|
|
118
126
|
this.log = logger_1.default.scope('IOSTunnels');
|
|
119
127
|
this.tunnels = new Map();
|
|
120
128
|
this.checking = false;
|
|
121
|
-
this.
|
|
129
|
+
this.versionCache = new Map();
|
|
130
|
+
this.goIOSPath = (0, goIosBinary_1.goIosBinaryPath)();
|
|
122
131
|
}
|
|
123
132
|
/**
|
|
124
|
-
* The phone's tunnel-info port, starting its tunnel if it
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
133
|
+
* The phone's tunnel-info port for its stream, starting its tunnel if it
|
|
134
|
+
* has none. A tunnel opened for screenshots is taken over: from then on
|
|
135
|
+
* only the stream's stop ends it. Null for a phone below iOS 17, or whose
|
|
136
|
+
* version can't be read: it needs no tunnel. Throws when no pair of ports
|
|
137
|
+
* is left, or when the tunnel exits before it is ready.
|
|
128
138
|
*/
|
|
129
139
|
ensure(udid) {
|
|
130
140
|
return __awaiter(this, void 0, void 0, function* () {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
141
|
+
return this.open(udid, 'stream');
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The phone's tunnel-info port for a screenshot, which on iOS 17+ is
|
|
146
|
+
* reached only through the tunnel. A phone that isn't streaming has none,
|
|
147
|
+
* so one is started; it stays while screenshots keep coming and stops
|
|
148
|
+
* {@link TUNNEL_BORROW_IDLE_MS} after the last one (checkTunnels). Null and
|
|
149
|
+
* throws as ensure.
|
|
150
|
+
*/
|
|
151
|
+
borrow(udid) {
|
|
152
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
153
|
+
return this.open(udid, 'screenshot');
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/** Whether the phone's tunnel was opened for screenshots and no stream has taken it. */
|
|
157
|
+
isOnDemand(udid) {
|
|
158
|
+
const tunnel = this.tunnels.get(udid);
|
|
159
|
+
return !!tunnel && tunnel.process.exitCode === null && !tunnel.streamOwned;
|
|
160
|
+
}
|
|
161
|
+
open(udid, use) {
|
|
162
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
163
|
+
const running = this.tunnels.get(udid);
|
|
164
|
+
if (running && running.process.exitCode === null) {
|
|
165
|
+
this.claim(running, use);
|
|
166
|
+
return running.whenReady;
|
|
142
167
|
}
|
|
143
|
-
|
|
168
|
+
// A screenshot also asks on a simulator, which has no version to read.
|
|
169
|
+
const version = yield this.versionOf(udid, use === 'screenshot');
|
|
170
|
+
if (version === null || version < 17)
|
|
144
171
|
return null;
|
|
145
172
|
const port = yield this.allocator().acquirePair('tunnel', udid, {
|
|
146
|
-
ttlMs: exports.
|
|
173
|
+
ttlMs: exports.IOS_STREAM_LEASE_TTL_MS,
|
|
147
174
|
});
|
|
148
175
|
let proc;
|
|
149
176
|
try {
|
|
@@ -161,7 +188,15 @@ let IOSTunnels = class IOSTunnels {
|
|
|
161
188
|
yield this.releasePair(port, udid);
|
|
162
189
|
throw e;
|
|
163
190
|
}
|
|
164
|
-
const tunnel = {
|
|
191
|
+
const tunnel = {
|
|
192
|
+
port,
|
|
193
|
+
process: proc,
|
|
194
|
+
ready: false,
|
|
195
|
+
whenReady: Promise.resolve(port),
|
|
196
|
+
streamOwned: false,
|
|
197
|
+
borrowedUntil: 0,
|
|
198
|
+
};
|
|
199
|
+
this.claim(tunnel, use);
|
|
165
200
|
this.tunnels.set(udid, tunnel);
|
|
166
201
|
this.watch();
|
|
167
202
|
// A tunnel whose process ends (go-ios crashed, or something killed it)
|
|
@@ -175,16 +210,39 @@ let IOSTunnels = class IOSTunnels {
|
|
|
175
210
|
void this.releasePair(port, udid);
|
|
176
211
|
};
|
|
177
212
|
proc.once('exit', ended);
|
|
178
|
-
|
|
213
|
+
// `on`, not `once`: a second 'error' with no listener left would be
|
|
214
|
+
// thrown, and take the server down. ended() only acts once.
|
|
215
|
+
proc.on('error', (err) => {
|
|
179
216
|
this.log.warn(`[${udid}] go-ios tunnel failed: ${err.message}`);
|
|
180
217
|
ended();
|
|
181
218
|
});
|
|
182
219
|
this.logOutput(udid, tunnel);
|
|
183
|
-
this.log.info(`[${udid}] iOS ${version}: starting a go-ios tunnel on ${port} (traffic on ${port + 1})`
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
220
|
+
this.log.info(`[${udid}] iOS ${version}: starting a go-ios tunnel on ${port} (traffic on ${port + 1})` +
|
|
221
|
+
(use === 'screenshot' ? ' for screenshots' : ''));
|
|
222
|
+
// Whoever asks while it starts waits for the same start.
|
|
223
|
+
tunnel.whenReady = this.waitUntilReady(udid, tunnel);
|
|
224
|
+
return tunnel.whenReady;
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
claim(tunnel, use) {
|
|
228
|
+
if (use === 'stream')
|
|
229
|
+
tunnel.streamOwned = true;
|
|
230
|
+
else
|
|
231
|
+
tunnel.borrowedUntil = this.now() + exports.TUNNEL_BORROW_IDLE_MS;
|
|
232
|
+
}
|
|
233
|
+
waitUntilReady(udid, tunnel) {
|
|
234
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
235
|
+
const { port, process: proc } = tunnel;
|
|
236
|
+
// By the clock, not a count of checks: each check can take up to its own
|
|
237
|
+
// 1 s timeout, which stretched 41 checks to about a minute.
|
|
238
|
+
const deadline = this.now() + exports.TUNNEL_READY_TIMEOUT_MS;
|
|
239
|
+
for (let first = true;; first = false) {
|
|
240
|
+
if (!first) {
|
|
241
|
+
const left = deadline - this.now();
|
|
242
|
+
if (left <= 0)
|
|
243
|
+
break;
|
|
244
|
+
yield this.sleep(Math.min(exports.TUNNEL_READY_POLL_MS, left));
|
|
245
|
+
}
|
|
188
246
|
if (this.tunnels.get(udid) !== tunnel) {
|
|
189
247
|
throw new Error(`The go-ios tunnel for ${udid} exited before it was ready (exit code ${proc.exitCode})` +
|
|
190
248
|
(tunnel.reason ? `: ${tunnel.reason}` : ''));
|
|
@@ -206,6 +264,33 @@ let IOSTunnels = class IOSTunnels {
|
|
|
206
264
|
return port;
|
|
207
265
|
});
|
|
208
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* The phone's iOS version, from `ios info`, kept for VERSION_CACHE_MS: a
|
|
269
|
+
* screenshot on a phone below iOS 17 would otherwise ask every time. Null
|
|
270
|
+
* when it can't be read (not cached, so a phone still pairing is asked
|
|
271
|
+
* again); logged at debug when `quiet`.
|
|
272
|
+
*/
|
|
273
|
+
versionOf(udid_1) {
|
|
274
|
+
return __awaiter(this, arguments, void 0, function* (udid, quiet = false) {
|
|
275
|
+
var _a;
|
|
276
|
+
const cached = this.versionCache.get(udid);
|
|
277
|
+
if (cached && this.now() - cached.at < VERSION_CACHE_MS)
|
|
278
|
+
return cached.version;
|
|
279
|
+
try {
|
|
280
|
+
const version = yield this.iosVersion(udid);
|
|
281
|
+
this.versionCache.set(udid, { version, at: this.now() });
|
|
282
|
+
return version;
|
|
283
|
+
}
|
|
284
|
+
catch (e) {
|
|
285
|
+
const message = `[${udid}] Could not read the iOS version, so no go-ios tunnel: ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}`;
|
|
286
|
+
if (quiet)
|
|
287
|
+
this.log.debug(message);
|
|
288
|
+
else
|
|
289
|
+
this.log.warn(message);
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
209
294
|
/** The phone's tunnel-info port, while its tunnel runs. */
|
|
210
295
|
portFor(udid) {
|
|
211
296
|
const tunnel = this.tunnels.get(udid);
|
|
@@ -253,6 +338,11 @@ let IOSTunnels = class IOSTunnels {
|
|
|
253
338
|
this.checking = true;
|
|
254
339
|
try {
|
|
255
340
|
for (const [udid, tunnel] of [...this.tunnels]) {
|
|
341
|
+
if (!tunnel.streamOwned && tunnel.borrowedUntil <= this.now()) {
|
|
342
|
+
this.log.info(`[${udid}] go-ios tunnel on ${tunnel.port}, opened for screenshots, is idle; stopping it`);
|
|
343
|
+
yield this.stop(udid);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
256
346
|
const info = yield this.tunnelInfo(tunnel.port, udid);
|
|
257
347
|
if (this.tunnels.get(udid) !== tunnel)
|
|
258
348
|
continue;
|
|
@@ -295,8 +385,8 @@ let IOSTunnels = class IOSTunnels {
|
|
|
295
385
|
if (!tunnel)
|
|
296
386
|
return;
|
|
297
387
|
const allocator = this.allocator();
|
|
298
|
-
yield allocator.touch(tunnel.port, exports.
|
|
299
|
-
yield allocator.touch(tunnel.port + 1, exports.
|
|
388
|
+
yield allocator.touch(tunnel.port, exports.IOS_STREAM_LEASE_TTL_MS);
|
|
389
|
+
yield allocator.touch(tunnel.port + 1, exports.IOS_STREAM_LEASE_TTL_MS);
|
|
300
390
|
});
|
|
301
391
|
}
|
|
302
392
|
releasePair(port, udid) {
|
|
@@ -400,6 +490,9 @@ let IOSTunnels = class IOSTunnels {
|
|
|
400
490
|
req.on('error', () => resolve({ state: 'unknown' }));
|
|
401
491
|
});
|
|
402
492
|
}
|
|
493
|
+
now() {
|
|
494
|
+
return Date.now();
|
|
495
|
+
}
|
|
403
496
|
sleep(ms) {
|
|
404
497
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
405
498
|
}
|
|
@@ -395,12 +395,20 @@ let WDAClient = WDAClient_1 = class WDAClient {
|
|
|
395
395
|
return __awaiter(this, void 0, void 0, function* () {
|
|
396
396
|
var _a, _b, _c;
|
|
397
397
|
const s = typedi_1.Container.get(IOSStreamService_1.default);
|
|
398
|
+
let tunnelError;
|
|
398
399
|
if (yield s.isGoIOSAvailable()) {
|
|
399
400
|
try {
|
|
401
|
+
// On iOS 17+ go-ios reaches the screenshot service only through the
|
|
402
|
+
// phone's own tunnel, and a phone that isn't streaming has none: open
|
|
403
|
+
// one, kept while screenshots keep coming (see IOSTunnels.borrow).
|
|
404
|
+
const tunnels = typedi_1.Container.get(IOSTunnels_1.IOSTunnels);
|
|
405
|
+
yield tunnels.borrow(udid).catch((e) => {
|
|
406
|
+
tunnelError = e;
|
|
407
|
+
throw e;
|
|
408
|
+
});
|
|
400
409
|
const p = path_1.default.join(os_1.default.tmpdir(), `screenshot-${udid}.png`);
|
|
401
410
|
yield execFilePromise(s.goIOSPath, ['screenshot', '--udid', udid, '--output', p], {
|
|
402
|
-
|
|
403
|
-
env: typedi_1.Container.get(IOSTunnels_1.IOSTunnels).envFor(udid),
|
|
411
|
+
env: tunnels.envFor(udid),
|
|
404
412
|
});
|
|
405
413
|
const b = yield fs_extra_1.default.readFile(p);
|
|
406
414
|
yield fs_extra_1.default.remove(p);
|
|
@@ -410,8 +418,14 @@ let WDAClient = WDAClient_1 = class WDAClient {
|
|
|
410
418
|
this.log.debug(`go-ios screenshot failed for ${udid}: ${e.message}\n${e.stack}`);
|
|
411
419
|
}
|
|
412
420
|
}
|
|
413
|
-
|
|
414
|
-
|
|
421
|
+
try {
|
|
422
|
+
const res = yield this.sendWDACommand(udid, 'get', '/screenshot');
|
|
423
|
+
return ((_b = (_a = res.data) === null || _a === void 0 ? void 0 : _a.value) === null || _b === void 0 ? void 0 : _b.screenshot) || ((_c = res.data) === null || _c === void 0 ? void 0 : _c.value) || '';
|
|
424
|
+
}
|
|
425
|
+
catch (e) {
|
|
426
|
+
// WebDriverAgent couldn't answer either; the tunnel's reason says more.
|
|
427
|
+
throw tunnelError !== null && tunnelError !== void 0 ? tunnelError : e;
|
|
428
|
+
}
|
|
415
429
|
});
|
|
416
430
|
}
|
|
417
431
|
getClipboard(udid) {
|
|
@@ -0,0 +1,16 @@
|
|
|
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.goIosBinaryPath = goIosBinaryPath;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const helpers_1 = require("../../helpers");
|
|
9
|
+
/**
|
|
10
|
+
* The go-ios binary Xenon vendors, where src/scripts/install-go-ios.ts puts
|
|
11
|
+
* it. Every go-ios command Xenon runs uses this one path: the boot and
|
|
12
|
+
* shutdown reaps find go-ios processes by it.
|
|
13
|
+
*/
|
|
14
|
+
function goIosBinaryPath() {
|
|
15
|
+
return path_1.default.join((0, helpers_1.cachePath)('goIOS'), 'ios');
|
|
16
|
+
}
|
package/lib/src/plugin.js
CHANGED
|
@@ -157,16 +157,8 @@ class XenonPlugin extends base_plugin_1.default {
|
|
|
157
157
|
catch (err) {
|
|
158
158
|
logger_1.default.warn(`[plugin] RecordingOrchestrator.recoverOnBoot failed: ${err === null || err === void 0 ? void 0 : err.message}`);
|
|
159
159
|
}
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
// orphans that would otherwise keep self-forking into a runaway process storm.
|
|
163
|
-
try {
|
|
164
|
-
const { default: IOSStreamService } = yield Promise.resolve().then(() => __importStar(require('./device-managers/ios/IOSStreamService')));
|
|
165
|
-
yield typedi_1.Container.get(IOSStreamService).reapOrphanTunnels();
|
|
166
|
-
}
|
|
167
|
-
catch (err) {
|
|
168
|
-
logger_1.default.warn(`[plugin] IOSStreamService.reapOrphanTunnels failed: ${err === null || err === void 0 ? void 0 : err.message}`);
|
|
169
|
-
}
|
|
160
|
+
// Leftover go-ios is reaped by ServerManager.updateServer, before device
|
|
161
|
+
// detection starts: here, the reap killed detection's own go-ios calls.
|
|
170
162
|
});
|
|
171
163
|
}
|
|
172
164
|
createSession(next, driver, jwpDesCaps, jwpReqCaps, caps) {
|
|
@@ -291,9 +291,16 @@ let PortAllocator = class PortAllocator {
|
|
|
291
291
|
.catch(() => undefined);
|
|
292
292
|
});
|
|
293
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* Delete a phone's leases when a session on it ends. Not its go-ios
|
|
296
|
+
* tunnel's pair: the phone's stream, and so its tunnel, can outlive the
|
|
297
|
+
* session, and IOSTunnels gives the pair back when the tunnel stops.
|
|
298
|
+
*/
|
|
294
299
|
releaseForUdid(udid) {
|
|
295
300
|
return __awaiter(this, void 0, void 0, function* () {
|
|
296
|
-
yield prisma_1.prisma.portLease.deleteMany({
|
|
301
|
+
yield prisma_1.prisma.portLease.deleteMany({
|
|
302
|
+
where: { leasedToUdid: udid, purpose: { not: 'tunnel' } },
|
|
303
|
+
});
|
|
297
304
|
});
|
|
298
305
|
}
|
|
299
306
|
purgeExpired() {
|
|
@@ -130,6 +130,8 @@ let ServerManager = ServerManager_1 = class ServerManager {
|
|
|
130
130
|
}
|
|
131
131
|
yield this.syncDatabaseAndAIConfig(pluginArgs);
|
|
132
132
|
yield this.initializeCoreSubsystems(pluginArgs, cliArgs.port);
|
|
133
|
+
// Before anything below starts go-ios: the reap would kill it too.
|
|
134
|
+
yield this.reapLeftoverGoIos();
|
|
133
135
|
this.registerRoutes(expressApp, httpServer, cliArgs, pluginArgs);
|
|
134
136
|
yield this.bootEmulators(pluginArgs);
|
|
135
137
|
this.registerDependenciesInContainer(pluginArgs, cliArgs, nodeId);
|
|
@@ -364,6 +366,26 @@ let ServerManager = ServerManager_1 = class ServerManager {
|
|
|
364
366
|
typedi_1.Container.set(ARTIFACT_STORE, new FsArtifactStore(config_1.config.recordingsAssetsPath));
|
|
365
367
|
});
|
|
366
368
|
}
|
|
369
|
+
/**
|
|
370
|
+
* Reap what a previous run (or a hard-killed one) left of go-ios: tunnels
|
|
371
|
+
* whose agents keep self-forking, WebDriverAgent runners and log streams.
|
|
372
|
+
* It kills every process running the go-ios binary, this process's own
|
|
373
|
+
* included, so it runs before anything here starts one. It used to run at
|
|
374
|
+
* the end of boot, after device detection had started, and killed the
|
|
375
|
+
* `ios info` call detection makes in the background for each plugged-in
|
|
376
|
+
* iPhone ("Failed to fetch IP via go-ios" at every boot).
|
|
377
|
+
*/
|
|
378
|
+
reapLeftoverGoIos() {
|
|
379
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
380
|
+
try {
|
|
381
|
+
const { default: IOSStreamService } = yield Promise.resolve().then(() => __importStar(require('../device-managers/ios/IOSStreamService')));
|
|
382
|
+
yield typedi_1.Container.get(IOSStreamService).reapOrphanTunnels();
|
|
383
|
+
}
|
|
384
|
+
catch (err) {
|
|
385
|
+
this.logger.warn(`Reaping go-ios left by a previous run failed: ${err === null || err === void 0 ? void 0 : err.message}`);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
}
|
|
367
389
|
registerRoutes(expressApp, httpServer, cliArgs, pluginArgs) {
|
|
368
390
|
expressApp.use('/xenon', (0, app_1.createRouter)(pluginArgs));
|
|
369
391
|
// The session gateway goes in front of Appium's routes: Xenon's own
|
|
@@ -93,7 +93,7 @@ describe('PortAllocator', () => {
|
|
|
93
93
|
(0, chai_1.expect)(deleteStub.calledWith({ where: { port: 9100 } })).to.be.true;
|
|
94
94
|
(0, chai_1.expect)(createStub.called, 'should allocate a replacement lease').to.be.true;
|
|
95
95
|
}));
|
|
96
|
-
it(
|
|
96
|
+
it("releaseForUdid deletes that UDID's leases", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
97
97
|
deleteManyStub.resolves({ count: 2 });
|
|
98
98
|
const allocator = makeAllocator({ wda: [8100, 8102] });
|
|
99
99
|
yield allocator.releaseForUdid('udid-1');
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
18
|
+
const typedi_1 = require("typedi");
|
|
19
|
+
const plugin_1 = require("../../src/plugin");
|
|
20
|
+
const ServerManager_1 = require("../../src/services/ServerManager");
|
|
21
|
+
const PluginContext_1 = require("../../src/PluginContext");
|
|
22
|
+
const IOSStreamService_1 = __importDefault(require("../../src/device-managers/ios/IOSStreamService"));
|
|
23
|
+
const RecordingOrchestrator_1 = require("../../src/services/recording/RecordingOrchestrator");
|
|
24
|
+
const logger_1 = __importDefault(require("../../src/logger"));
|
|
25
|
+
/**
|
|
26
|
+
* At boot Xenon reaps what a previous run left of go-ios (tunnels whose agents
|
|
27
|
+
* self-fork, WebDriverAgent runners, log streams). The reap kills every process
|
|
28
|
+
* running the vendored go-ios binary, so it also kills this process's own
|
|
29
|
+
* go-ios calls. It used to run last, after device detection had started:
|
|
30
|
+
* detection asks `ios info` about each plugged-in iPhone in the background,
|
|
31
|
+
* and those calls were killed ("Failed to fetch IP via go-ios" for every
|
|
32
|
+
* iPhone, at every boot). It now runs as soon as the database is ready,
|
|
33
|
+
* before this process starts go-ios at all.
|
|
34
|
+
*/
|
|
35
|
+
describe('reaping go-ios left by a previous run, at boot', () => {
|
|
36
|
+
let statics;
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
statics = {
|
|
39
|
+
nodeId: plugin_1.XenonPlugin.NODE_ID,
|
|
40
|
+
port: plugin_1.XenonPlugin.port,
|
|
41
|
+
basePath: plugin_1.XenonPlugin.nodeBasePath,
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
sinon_1.default.restore();
|
|
46
|
+
plugin_1.XenonPlugin.NODE_ID = statics.nodeId;
|
|
47
|
+
plugin_1.XenonPlugin.port = statics.port;
|
|
48
|
+
plugin_1.XenonPlugin.nodeBasePath = statics.basePath;
|
|
49
|
+
});
|
|
50
|
+
it('runs once the database is ready, before routes and device detection start', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
51
|
+
const steps = [];
|
|
52
|
+
const step = (name) => () => {
|
|
53
|
+
steps.push(name);
|
|
54
|
+
return Promise.resolve();
|
|
55
|
+
};
|
|
56
|
+
const boot = ServerManager_1.ServerManager.prototype;
|
|
57
|
+
sinon_1.default.stub(boot, 'resolvePluginArgs').resolves({});
|
|
58
|
+
sinon_1.default.stub(logger_1.default, 'banner');
|
|
59
|
+
sinon_1.default.stub(PluginContext_1.PluginContext.prototype, 'setContext');
|
|
60
|
+
sinon_1.default.stub(boot, 'syncDatabaseAndAIConfig').callsFake(step('database config'));
|
|
61
|
+
sinon_1.default.stub(boot, 'initializeCoreSubsystems').callsFake(step('database and migrations'));
|
|
62
|
+
sinon_1.default.stub(IOSStreamService_1.default.prototype, 'reapOrphanTunnels').callsFake(step('reap'));
|
|
63
|
+
// Everything that starts go-ios (device managers, detection, the hub's and
|
|
64
|
+
// node's discovery crons) comes after the routes; stop there.
|
|
65
|
+
sinon_1.default.stub(boot, 'registerRoutes').callsFake(() => {
|
|
66
|
+
steps.push('routes');
|
|
67
|
+
throw new Error('boot stopped by the test');
|
|
68
|
+
});
|
|
69
|
+
const booting = typedi_1.Container.get(ServerManager_1.ServerManager).updateServer({}, null, { port: 4723 });
|
|
70
|
+
yield booting.then(() => chai_1.expect.fail('the stubbed routes step should have stopped the boot'), (err) => (0, chai_1.expect)(err.message).to.equal('boot stopped by the test'));
|
|
71
|
+
(0, chai_1.expect)(steps).to.deep.equal(['database config', 'database and migrations', 'reap', 'routes']);
|
|
72
|
+
}));
|
|
73
|
+
it('is not left to the plugin, which boots after detection has started', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
74
|
+
sinon_1.default.stub(ServerManager_1.ServerManager.prototype, 'updateServer').resolves();
|
|
75
|
+
sinon_1.default.stub(RecordingOrchestrator_1.RecordingOrchestrator.prototype, 'recoverOnBoot').resolves();
|
|
76
|
+
const reap = sinon_1.default.stub(IOSStreamService_1.default.prototype, 'reapOrphanTunnels').resolves();
|
|
77
|
+
yield plugin_1.XenonPlugin.updateServer({}, null, {});
|
|
78
|
+
(0, chai_1.expect)(reap.called).to.equal(false);
|
|
79
|
+
}));
|
|
80
|
+
});
|
|
@@ -43,13 +43,33 @@ case "$1" in
|
|
|
43
43
|
syslog) printf '{"msg":"port=%s"}\\n' "$GO_IOS_AGENT_PORT"; exec sleep 5 ;;
|
|
44
44
|
esac
|
|
45
45
|
`;
|
|
46
|
-
/**
|
|
46
|
+
/** iOS 17+, not streaming: it has a tunnel only once one is opened for it. */
|
|
47
|
+
const NEEDS_TUNNEL = 'test-iphone-idle-00008120';
|
|
48
|
+
/**
|
|
49
|
+
* IOSTunnels as if WITH_TUNNEL's tunnel ran on 12100, NO_TUNNEL needed none
|
|
50
|
+
* (below iOS 17), and NEEDS_TUNNEL got one on 12102 once borrowed.
|
|
51
|
+
*/
|
|
47
52
|
const tunnels = {
|
|
53
|
+
borrowed: new Set(),
|
|
54
|
+
borrowCalls: [],
|
|
55
|
+
borrowError: undefined,
|
|
56
|
+
borrow(udid) {
|
|
57
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
58
|
+
this.borrowCalls.push(udid);
|
|
59
|
+
if (this.borrowError)
|
|
60
|
+
throw this.borrowError;
|
|
61
|
+
if (udid === NEEDS_TUNNEL)
|
|
62
|
+
this.borrowed.add(udid);
|
|
63
|
+
return udid === WITH_TUNNEL ? 12100 : udid === NEEDS_TUNNEL ? 12102 : null;
|
|
64
|
+
});
|
|
65
|
+
},
|
|
48
66
|
envFor(udid) {
|
|
49
67
|
const env = Object.assign(Object.assign({}, process.env), { ENABLE_GO_IOS_AGENT: 'yes' });
|
|
50
68
|
delete env.GO_IOS_AGENT_PORT;
|
|
51
69
|
if (udid === WITH_TUNNEL)
|
|
52
70
|
env.GO_IOS_AGENT_PORT = '12100';
|
|
71
|
+
if (this.borrowed.has(udid))
|
|
72
|
+
env.GO_IOS_AGENT_PORT = '12102';
|
|
53
73
|
return env;
|
|
54
74
|
},
|
|
55
75
|
};
|
|
@@ -63,6 +83,9 @@ describe("go-ios commands find their own phone's tunnel (GO_IOS_AGENT_PORT)", ()
|
|
|
63
83
|
});
|
|
64
84
|
after(() => fs_1.default.rmSync(dir, { recursive: true, force: true }));
|
|
65
85
|
beforeEach(() => {
|
|
86
|
+
tunnels.borrowed.clear();
|
|
87
|
+
tunnels.borrowCalls = [];
|
|
88
|
+
tunnels.borrowError = undefined;
|
|
66
89
|
const real = typedi_1.Container.get.bind(typedi_1.Container);
|
|
67
90
|
sinon_1.default.stub(typedi_1.Container, 'get').callsFake((token) => {
|
|
68
91
|
if (token === IOSTunnels_1.IOSTunnels)
|
|
@@ -88,6 +111,20 @@ describe("go-ios commands find their own phone's tunnel (GO_IOS_AGENT_PORT)", ()
|
|
|
88
111
|
const png = yield new WDAClient_1.WDAClient().getScreenshot(NO_TUNNEL);
|
|
89
112
|
(0, chai_1.expect)(Buffer.from(png, 'base64').toString()).to.equal('port=');
|
|
90
113
|
}));
|
|
114
|
+
// On iOS 17+ the screenshot service is reached only through the tunnel,
|
|
115
|
+
// and a phone that isn't streaming has none.
|
|
116
|
+
it("opens the phone's tunnel for a screenshot when it has none", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
117
|
+
const png = yield new WDAClient_1.WDAClient().getScreenshot(NEEDS_TUNNEL);
|
|
118
|
+
(0, chai_1.expect)(tunnels.borrowCalls).to.deep.equal([NEEDS_TUNNEL]);
|
|
119
|
+
(0, chai_1.expect)(Buffer.from(png, 'base64').toString()).to.equal('port=12102');
|
|
120
|
+
}));
|
|
121
|
+
it("says why, when the tunnel can't start and WebDriverAgent can't answer either", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
122
|
+
tunnels.borrowError = new Error('The go-ios tunnel for X exited before it was ready (exit code 1): failed to start tunnel: EOF');
|
|
123
|
+
const client = new WDAClient_1.WDAClient();
|
|
124
|
+
sinon_1.default.stub(client, 'sendWDACommand').rejects(new Error('connect ECONNREFUSED'));
|
|
125
|
+
const err = yield client.getScreenshot(NEEDS_TUNNEL).then(() => null, (e) => e);
|
|
126
|
+
(0, chai_1.expect)(err === null || err === void 0 ? void 0 : err.message).to.match(/exited before it was ready .*failed to start tunnel: EOF/);
|
|
127
|
+
}));
|
|
91
128
|
});
|
|
92
129
|
describe('the logs read (WDAClient syslog)', () => {
|
|
93
130
|
let client;
|