@xenon-device-management/xenon 2.8.3 → 2.9.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-DZrFxYPZ.js → AnnotationOverlay-DhRFPZ9-.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-DkyW0B7G.js → ApiKeyGate-BmZy1Srn.js} +1 -1
- package/lib/public/assets/{BugReportButton-TyYrNWYM.js → BugReportButton-ByLSqwSk.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-DY__bWHx.js → DeviceMosaicView-DVfXSoXH.js} +1 -1
- package/lib/public/assets/{EmptyState-CfkN-nUG.js → EmptyState-Bib6wal8.js} +1 -1
- package/lib/public/assets/{FieldGroup-DQu6Zo15.js → FieldGroup-D5sTaKGR.js} +1 -1
- package/lib/public/assets/{FilterMenu-BT_Q7-4t.js → FilterMenu-D3XXKTwF.js} +1 -1
- package/lib/public/assets/{Menu-DfwEC8pG.js → Menu-BiZhr0F7.js} +1 -1
- package/lib/public/assets/{Modal-DXyaClqh.js → Modal-BxGNa3Zp.js} +1 -1
- package/lib/public/assets/{RecordingPage-j7H4_iT1.js → RecordingPage-Czml77Wb.js} +1 -1
- package/lib/public/assets/{RecordingsPage-ST7B99o1.js → RecordingsPage-Di7nyHqE.js} +1 -1
- package/lib/public/assets/{SegmentedControl-DaqYH9BW.js → SegmentedControl-XEKvCRwO.js} +1 -1
- package/lib/public/assets/{SettingCard-CWTz_r_8.js → SettingCard-YIpOQAjO.js} +1 -1
- package/lib/public/assets/{Table-BDO5qlOf.js → Table-CqwX6v-1.js} +1 -1
- package/lib/public/assets/{activity-CGdR3Ff6.js → activity-Cur7Syd6.js} +1 -1
- package/lib/public/assets/{ai-settings-C8I0KSUg.js → ai-settings-B1OGdSKu.js} +1 -1
- package/lib/public/assets/{api-keys-BhbZQpGV.js → api-keys-abTul2Oa.js} +1 -1
- package/lib/public/assets/{apps-DaBl9nMx.js → apps-yQL1MLfG.js} +1 -1
- package/lib/public/assets/{arrow-left-WRIrign1.js → arrow-left-D0ipNN6o.js} +1 -1
- package/lib/public/assets/{arrow-right-BuIztKwL.js → arrow-right-DwVz_Fly.js} +1 -1
- package/lib/public/assets/{arrow-up-right-Cz072oos.js → arrow-up-right-xKuKJfZf.js} +1 -1
- package/lib/public/assets/{auth-shell-BS884uL2.js → auth-shell-00zHLFCk.js} +2 -2
- package/lib/public/assets/{builds-page-B05iZMmN.js → builds-page-D4RJAbOZ.js} +1 -1
- package/lib/public/assets/{button-DjeIU-Ty.js → button-Cic7S8s2.js} +1 -1
- package/lib/public/assets/{calendar-DUF9u2PF.js → calendar-DyNZ2bq9.js} +1 -1
- package/lib/public/assets/{check-DUR92DEE.js → check-CXGZdojb.js} +1 -1
- package/lib/public/assets/{chevron-left-CRvlIuad.js → chevron-left-BcNiIt1C.js} +1 -1
- package/lib/public/assets/{chevron-right-iRDZ5v7C.js → chevron-right-BxDjkb1I.js} +1 -1
- package/lib/public/assets/{circle-check-CqDvbZtW.js → circle-check-9isFAWQ6.js} +1 -1
- package/lib/public/assets/{circle-x-CsYtRLC8.js → circle-x-CfnPCFLK.js} +1 -1
- package/lib/public/assets/{clock-DB9ihtTE.js → clock-Dc4TvnFF.js} +1 -1
- package/lib/public/assets/{copy-BufiyibK.js → copy-TFQpalJ2.js} +1 -1
- package/lib/public/assets/{device-explorer-BapnPGYt.js → device-explorer-CB4bSJtl.js} +1 -1
- package/lib/public/assets/{download-D14AYHRg.js → download-1_bq_Ikd.js} +1 -1
- package/lib/public/assets/{forgot-password-2PqpH7ph.js → forgot-password-B92TZ1GF.js} +1 -1
- package/lib/public/assets/{funnel-CgV-CZex.js → funnel-DC7DsjHe.js} +1 -1
- package/lib/public/assets/{index-BE8OC-Wj.js → index-BSsnx09Q.js} +1 -1
- package/lib/public/assets/{index-CXxjosDd.js → index-CC2N_uMU.js} +2 -2
- package/lib/public/assets/{input-DPn63eF4.js → input-D2DBpxK2.js} +1 -1
- package/lib/public/assets/{keycodes-BYgvJDTE.js → keycodes-BQuIOMw0.js} +1 -1
- package/lib/public/assets/{layers-DJZQUOez.js → layers-DxtoRlvi.js} +1 -1
- package/lib/public/assets/{list-checks-YM2_VNMv.js → list-checks-jQ7YUgdE.js} +1 -1
- package/lib/public/assets/{lock-Cy-A3o47.js → lock-WHSsj2dj.js} +1 -1
- package/lib/public/assets/{login-CRZ2J7Up.js → login-BB_xyUWw.js} +1 -1
- package/lib/public/assets/{maintenance-settings-4HkUk10V.js → maintenance-settings-Dwadp8IQ.js} +1 -1
- package/lib/public/assets/{monitor-Cyvro78e.js → monitor-DIWQjUlh.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-26YTYUOD.js → mouse-pointer-2-D11Re9Gj.js} +1 -1
- package/lib/public/assets/{network-D-W1VWM_.js → network-BYBgSBjv.js} +1 -1
- package/lib/public/assets/{overview-pVmHrKhk.js → overview-BISAwoyi.js} +1 -1
- package/lib/public/assets/{page-header-DsTokPlH.js → page-header-ChX4YiMP.js} +1 -1
- package/lib/public/assets/{plus-OjIdQkG_.js → plus-BWNj1XSQ.js} +1 -1
- package/lib/public/assets/{profile-page-D0jy_p5L.js → profile-page-VZD3gRjV.js} +1 -1
- package/lib/public/assets/{reset-password-DptmkgSN.js → reset-password-Dz9LzFeX.js} +1 -1
- package/lib/public/assets/{runbook-page-9QcpCPUo.js → runbook-page-Bl2FodZf.js} +1 -1
- package/lib/public/assets/{select-CHKzkHre.js → select-Bs9CQu5Z.js} +1 -1
- package/lib/public/assets/{selector-detail-page-P5zDXMZC.js → selector-detail-page-By7ihhbY.js} +1 -1
- package/lib/public/assets/{selector-health-page-BHZfbxGg.js → selector-health-page-Y4CTbOLH.js} +1 -1
- package/lib/public/assets/{selector-health-Buz4eRW1.js → selector-health-xiBb8vNz.js} +1 -1
- package/lib/public/assets/{session-detail-page-JHU-GKSX.js → session-detail-page-DqeKRP6c.js} +1 -1
- package/lib/public/assets/{settings-CRTRggHW.js → settings-DDhV_Dnz.js} +1 -1
- package/lib/public/assets/{stat-tile-Be2twadZ.js → stat-tile-DMBTDbwt.js} +1 -1
- package/lib/public/assets/{tablet-C7QkOkHs.js → tablet-Cr5bOgng.js} +1 -1
- package/lib/public/assets/{teams-CwVhnDoL.js → teams-Bm0UQkR6.js} +1 -1
- package/lib/public/assets/{trash-2-yjlULOrf.js → trash-2-Di_lMeiP.js} +1 -1
- package/lib/public/assets/{upload-BcrYG7kK.js → upload-RvXDBmCX.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-BhRH2TTL.js → use-password-reset-mode-C7pUQ7xe.js} +1 -1
- package/lib/public/assets/{users-D3jRTB5K.js → users-CIILdbvy.js} +1 -1
- package/lib/public/assets/{users-BSZL_J7x.js → users-DZ7B5ePh.js} +1 -1
- package/lib/public/assets/{video-off-BwbiJtmA.js → video-off-aYil8B-X.js} +1 -1
- package/lib/public/assets/{webhook-settings-CTIdd7DN.js → webhook-settings-17JYW1ZR.js} +1 -1
- package/lib/public/assets/{zap-CZK3Cbup.js → zap-D9hchm-a.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/src/device-managers/ios/IOSStreamService.js +4 -0
- package/lib/src/device-managers/ios/IOSTunnels.js +103 -19
- package/lib/src/device-managers/ios/WDAClient.js +18 -4
- package/lib/test/unit/ios-go-ios-tunnel-env.spec.js +38 -1
- package/lib/test/unit/ios-stream-appium-tunnel.spec.js +10 -0
- package/lib/test/unit/ios-tunnels.spec.js +66 -0
- package/package.json +1 -1
|
@@ -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.TUNNEL_LEASE_TTL_MS = exports.TUNNEL_READY_POLL_MS = exports.TUNNEL_READY_TIMEOUT_MS = void 0;
|
|
21
|
+
exports.IOSTunnels = exports.TUNNEL_BORROW_IDLE_MS = exports.TUNNEL_CHECK_MS = exports.TUNNEL_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
|
/**
|
|
@@ -69,6 +69,13 @@ exports.TUNNEL_READY_POLL_MS = 500;
|
|
|
69
69
|
exports.TUNNEL_LEASE_TTL_MS = 90 * 60 * 1000;
|
|
70
70
|
/** How often running tunnels are checked for a phone that went away. */
|
|
71
71
|
exports.TUNNEL_CHECK_MS = 5000;
|
|
72
|
+
/**
|
|
73
|
+
* How long a tunnel opened for screenshots stays after the last one: long
|
|
74
|
+
* enough that a client taking screenshots in a loop pays the start once.
|
|
75
|
+
*/
|
|
76
|
+
exports.TUNNEL_BORROW_IDLE_MS = 2 * 60 * 1000;
|
|
77
|
+
/** How long a phone's iOS version is trusted before `ios info` is asked again. */
|
|
78
|
+
const VERSION_CACHE_MS = 10 * 60 * 1000;
|
|
72
79
|
/** The iOS version `ios info` reports, as a number: 17.2 for "17.2.1" or "iOS 17.2", 0 for none. */
|
|
73
80
|
function iosVersionOf(info) {
|
|
74
81
|
const versionStr = String(info.ProductVersion || info.HumanReadableProductVersionString || '0');
|
|
@@ -118,29 +125,48 @@ let IOSTunnels = class IOSTunnels {
|
|
|
118
125
|
this.log = logger_1.default.scope('IOSTunnels');
|
|
119
126
|
this.tunnels = new Map();
|
|
120
127
|
this.checking = false;
|
|
128
|
+
this.versionCache = new Map();
|
|
121
129
|
this.goIOSPath = path_1.default.join((0, helpers_1.cachePath)('goIOS'), 'ios');
|
|
122
130
|
}
|
|
123
131
|
/**
|
|
124
|
-
* The phone's tunnel-info port, starting its tunnel if it
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
132
|
+
* The phone's tunnel-info port for its stream, starting its tunnel if it
|
|
133
|
+
* has none. A tunnel opened for screenshots is taken over: from then on
|
|
134
|
+
* only the stream's stop ends it. Null for a phone below iOS 17, or whose
|
|
135
|
+
* version can't be read: it needs no tunnel. Throws when no pair of ports
|
|
136
|
+
* is left, or when the tunnel exits before it is ready.
|
|
128
137
|
*/
|
|
129
138
|
ensure(udid) {
|
|
130
139
|
return __awaiter(this, void 0, void 0, function* () {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
return this.open(udid, 'stream');
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The phone's tunnel-info port for a screenshot, which on iOS 17+ is
|
|
145
|
+
* reached only through the tunnel. A phone that isn't streaming has none,
|
|
146
|
+
* so one is started; it stays while screenshots keep coming and stops
|
|
147
|
+
* {@link TUNNEL_BORROW_IDLE_MS} after the last one (checkTunnels). Null and
|
|
148
|
+
* throws as ensure.
|
|
149
|
+
*/
|
|
150
|
+
borrow(udid) {
|
|
151
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
152
|
+
return this.open(udid, 'screenshot');
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/** Whether the phone's tunnel was opened for screenshots and no stream has taken it. */
|
|
156
|
+
isOnDemand(udid) {
|
|
157
|
+
const tunnel = this.tunnels.get(udid);
|
|
158
|
+
return !!tunnel && tunnel.process.exitCode === null && !tunnel.streamOwned;
|
|
159
|
+
}
|
|
160
|
+
open(udid, use) {
|
|
161
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
162
|
+
const running = this.tunnels.get(udid);
|
|
163
|
+
if (running && running.process.exitCode === null) {
|
|
164
|
+
this.claim(running, use);
|
|
165
|
+
return running.whenReady;
|
|
142
166
|
}
|
|
143
|
-
|
|
167
|
+
// A screenshot also asks on a simulator, which has no version to read.
|
|
168
|
+
const version = yield this.versionOf(udid, use === 'screenshot');
|
|
169
|
+
if (version === null || version < 17)
|
|
144
170
|
return null;
|
|
145
171
|
const port = yield this.allocator().acquirePair('tunnel', udid, {
|
|
146
172
|
ttlMs: exports.TUNNEL_LEASE_TTL_MS,
|
|
@@ -161,7 +187,15 @@ let IOSTunnels = class IOSTunnels {
|
|
|
161
187
|
yield this.releasePair(port, udid);
|
|
162
188
|
throw e;
|
|
163
189
|
}
|
|
164
|
-
const tunnel = {
|
|
190
|
+
const tunnel = {
|
|
191
|
+
port,
|
|
192
|
+
process: proc,
|
|
193
|
+
ready: false,
|
|
194
|
+
whenReady: Promise.resolve(port),
|
|
195
|
+
streamOwned: false,
|
|
196
|
+
borrowedUntil: 0,
|
|
197
|
+
};
|
|
198
|
+
this.claim(tunnel, use);
|
|
165
199
|
this.tunnels.set(udid, tunnel);
|
|
166
200
|
this.watch();
|
|
167
201
|
// A tunnel whose process ends (go-ios crashed, or something killed it)
|
|
@@ -180,7 +214,22 @@ let IOSTunnels = class IOSTunnels {
|
|
|
180
214
|
ended();
|
|
181
215
|
});
|
|
182
216
|
this.logOutput(udid, tunnel);
|
|
183
|
-
this.log.info(`[${udid}] iOS ${version}: starting a go-ios tunnel on ${port} (traffic on ${port + 1})`
|
|
217
|
+
this.log.info(`[${udid}] iOS ${version}: starting a go-ios tunnel on ${port} (traffic on ${port + 1})` +
|
|
218
|
+
(use === 'screenshot' ? ' for screenshots' : ''));
|
|
219
|
+
// Whoever asks while it starts waits for the same start.
|
|
220
|
+
tunnel.whenReady = this.waitUntilReady(udid, tunnel);
|
|
221
|
+
return tunnel.whenReady;
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
claim(tunnel, use) {
|
|
225
|
+
if (use === 'stream')
|
|
226
|
+
tunnel.streamOwned = true;
|
|
227
|
+
else
|
|
228
|
+
tunnel.borrowedUntil = this.now() + exports.TUNNEL_BORROW_IDLE_MS;
|
|
229
|
+
}
|
|
230
|
+
waitUntilReady(udid, tunnel) {
|
|
231
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
232
|
+
const { port, process: proc } = tunnel;
|
|
184
233
|
const polls = exports.TUNNEL_READY_TIMEOUT_MS / exports.TUNNEL_READY_POLL_MS;
|
|
185
234
|
for (let poll = 0; poll <= polls; poll++) {
|
|
186
235
|
if (poll > 0)
|
|
@@ -206,6 +255,33 @@ let IOSTunnels = class IOSTunnels {
|
|
|
206
255
|
return port;
|
|
207
256
|
});
|
|
208
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* The phone's iOS version, from `ios info`, kept for VERSION_CACHE_MS: a
|
|
260
|
+
* screenshot on a phone below iOS 17 would otherwise ask every time. Null
|
|
261
|
+
* when it can't be read (not cached, so a phone still pairing is asked
|
|
262
|
+
* again); logged at debug when `quiet`.
|
|
263
|
+
*/
|
|
264
|
+
versionOf(udid_1) {
|
|
265
|
+
return __awaiter(this, arguments, void 0, function* (udid, quiet = false) {
|
|
266
|
+
var _a;
|
|
267
|
+
const cached = this.versionCache.get(udid);
|
|
268
|
+
if (cached && this.now() - cached.at < VERSION_CACHE_MS)
|
|
269
|
+
return cached.version;
|
|
270
|
+
try {
|
|
271
|
+
const version = yield this.iosVersion(udid);
|
|
272
|
+
this.versionCache.set(udid, { version, at: this.now() });
|
|
273
|
+
return version;
|
|
274
|
+
}
|
|
275
|
+
catch (e) {
|
|
276
|
+
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}`;
|
|
277
|
+
if (quiet)
|
|
278
|
+
this.log.debug(message);
|
|
279
|
+
else
|
|
280
|
+
this.log.warn(message);
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
209
285
|
/** The phone's tunnel-info port, while its tunnel runs. */
|
|
210
286
|
portFor(udid) {
|
|
211
287
|
const tunnel = this.tunnels.get(udid);
|
|
@@ -253,6 +329,11 @@ let IOSTunnels = class IOSTunnels {
|
|
|
253
329
|
this.checking = true;
|
|
254
330
|
try {
|
|
255
331
|
for (const [udid, tunnel] of [...this.tunnels]) {
|
|
332
|
+
if (!tunnel.streamOwned && tunnel.borrowedUntil <= this.now()) {
|
|
333
|
+
this.log.info(`[${udid}] go-ios tunnel on ${tunnel.port}, opened for screenshots, is idle; stopping it`);
|
|
334
|
+
yield this.stop(udid);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
256
337
|
const info = yield this.tunnelInfo(tunnel.port, udid);
|
|
257
338
|
if (this.tunnels.get(udid) !== tunnel)
|
|
258
339
|
continue;
|
|
@@ -400,6 +481,9 @@ let IOSTunnels = class IOSTunnels {
|
|
|
400
481
|
req.on('error', () => resolve({ state: 'unknown' }));
|
|
401
482
|
});
|
|
402
483
|
}
|
|
484
|
+
now() {
|
|
485
|
+
return Date.now();
|
|
486
|
+
}
|
|
403
487
|
sleep(ms) {
|
|
404
488
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
405
489
|
}
|
|
@@ -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) {
|
|
@@ -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;
|
|
@@ -198,6 +198,7 @@ describe('go-ios tunnels under a live Appium session (fake exec, fake processes)
|
|
|
198
198
|
envFor: sinon_1.default.stub().callsFake(() => (Object.assign(Object.assign({}, process.env), { ENABLE_GO_IOS_AGENT: 'yes' }))),
|
|
199
199
|
stop: sinon_1.default.stub().resolves(),
|
|
200
200
|
touch: sinon_1.default.stub().resolves(),
|
|
201
|
+
isOnDemand: sinon_1.default.stub().returns(false),
|
|
201
202
|
};
|
|
202
203
|
svc = iosService();
|
|
203
204
|
const real = typedi_1.Container.get.bind(typedi_1.Container);
|
|
@@ -363,6 +364,15 @@ describe('go-ios tunnels under a live Appium session (fake exec, fake processes)
|
|
|
363
364
|
(0, chai_1.expect)(defaultPorts()).to.deep.equal([]);
|
|
364
365
|
}));
|
|
365
366
|
});
|
|
367
|
+
describe('a start on a phone whose tunnel was opened for screenshots', () => {
|
|
368
|
+
it('takes that tunnel over, neither stopping nor reaping it', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
369
|
+
Object.assign(device, { busy: false, session_id: null });
|
|
370
|
+
tunnels.isOnDemand.withArgs(IPHONE).returns(true);
|
|
371
|
+
yield svc.cleanupOrphanTunnels(IPHONE);
|
|
372
|
+
(0, chai_1.expect)(tunnels.stop.called, 'the tracked tunnel').to.equal(false);
|
|
373
|
+
(0, chai_1.expect)(swept(), 'no pgrep reap of its process').to.deep.equal([]);
|
|
374
|
+
}));
|
|
375
|
+
});
|
|
366
376
|
describe('boot', () => {
|
|
367
377
|
it('reaps every go-ios process, then drops every tunnel port lease', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
368
378
|
yield svc.reapOrphanTunnels();
|
|
@@ -80,10 +80,16 @@ class TestTunnels extends IOSTunnels_1.IOSTunnels {
|
|
|
80
80
|
/** Each tunnel-info poll: true is the phone's tunnel on P + 1, false a 404. */
|
|
81
81
|
this.answers = () => true;
|
|
82
82
|
this.ports = fakeAllocator();
|
|
83
|
+
this.clock = 1000000;
|
|
84
|
+
this.versionReads = 0;
|
|
83
85
|
this.nextPid = 7000;
|
|
84
86
|
}
|
|
87
|
+
now() {
|
|
88
|
+
return this.clock;
|
|
89
|
+
}
|
|
85
90
|
iosVersion(udid) {
|
|
86
91
|
return __awaiter(this, void 0, void 0, function* () {
|
|
92
|
+
this.versionReads += 1;
|
|
87
93
|
const version = this.versions.get(udid);
|
|
88
94
|
if (version === undefined)
|
|
89
95
|
throw new Error(`no such phone: ${udid}`);
|
|
@@ -332,6 +338,66 @@ describe('IOSTunnels: a phone that goes away (go-ios keeps its agent running)',
|
|
|
332
338
|
}
|
|
333
339
|
}));
|
|
334
340
|
});
|
|
341
|
+
// A screenshot on iOS 17+ goes through the phone's tunnel, and a phone that
|
|
342
|
+
// isn't streaming has none: one is opened for screenshots and kept while they
|
|
343
|
+
// keep coming.
|
|
344
|
+
describe('IOSTunnels: a tunnel opened for screenshots', () => {
|
|
345
|
+
let t;
|
|
346
|
+
beforeEach(() => {
|
|
347
|
+
sinon_1.default.stub(process, 'kill');
|
|
348
|
+
t = new TestTunnels();
|
|
349
|
+
t.versions.set(PHONE_A, 17.4);
|
|
350
|
+
});
|
|
351
|
+
afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
352
|
+
yield t.stop(PHONE_A);
|
|
353
|
+
sinon_1.default.restore();
|
|
354
|
+
}));
|
|
355
|
+
it('opens one when the phone has none, and the next screenshot reuses it', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
356
|
+
(0, chai_1.expect)(yield t.borrow(PHONE_A)).to.equal(12100);
|
|
357
|
+
(0, chai_1.expect)(yield t.borrow(PHONE_A)).to.equal(12100);
|
|
358
|
+
(0, chai_1.expect)(t.spawned.length).to.equal(1);
|
|
359
|
+
(0, chai_1.expect)(t.envFor(PHONE_A).GO_IOS_AGENT_PORT).to.equal('12100');
|
|
360
|
+
}));
|
|
361
|
+
it('stops it 2 minutes after the last screenshot, and gives its ports back', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
362
|
+
yield t.borrow(PHONE_A);
|
|
363
|
+
t.clock += IOSTunnels_1.TUNNEL_BORROW_IDLE_MS - 1000;
|
|
364
|
+
yield t.borrow(PHONE_A); // another screenshot: the 2 minutes start again
|
|
365
|
+
t.clock += IOSTunnels_1.TUNNEL_BORROW_IDLE_MS - 1000;
|
|
366
|
+
yield t.checkTunnels();
|
|
367
|
+
(0, chai_1.expect)(t.portFor(PHONE_A), 'last used 119 s ago').to.equal(12100);
|
|
368
|
+
t.clock += 2000;
|
|
369
|
+
yield t.checkTunnels();
|
|
370
|
+
yield settle();
|
|
371
|
+
(0, chai_1.expect)(t.portFor(PHONE_A)).to.equal(undefined);
|
|
372
|
+
(0, chai_1.expect)(t.killed).to.deep.equal([t.spawned[0].proc.pid]);
|
|
373
|
+
(0, chai_1.expect)([...t.ports.leased.keys()]).to.deep.equal([]);
|
|
374
|
+
}));
|
|
375
|
+
it('never stops one a stream took over', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
376
|
+
yield t.borrow(PHONE_A);
|
|
377
|
+
(0, chai_1.expect)(t.isOnDemand(PHONE_A)).to.equal(true);
|
|
378
|
+
(0, chai_1.expect)(yield t.ensure(PHONE_A), 'the stream takes it').to.equal(12100);
|
|
379
|
+
(0, chai_1.expect)(t.isOnDemand(PHONE_A)).to.equal(false);
|
|
380
|
+
t.clock += 10 * IOSTunnels_1.TUNNEL_BORROW_IDLE_MS;
|
|
381
|
+
yield t.checkTunnels();
|
|
382
|
+
(0, chai_1.expect)(t.portFor(PHONE_A)).to.equal(12100);
|
|
383
|
+
(0, chai_1.expect)(t.spawned.length).to.equal(1);
|
|
384
|
+
}));
|
|
385
|
+
it("never stops a stream's own tunnel for being idle", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
386
|
+
yield t.ensure(PHONE_A);
|
|
387
|
+
yield t.borrow(PHONE_A); // a screenshot while the stream runs
|
|
388
|
+
t.clock += 10 * IOSTunnels_1.TUNNEL_BORROW_IDLE_MS;
|
|
389
|
+
yield t.checkTunnels();
|
|
390
|
+
(0, chai_1.expect)(t.portFor(PHONE_A)).to.equal(12100);
|
|
391
|
+
(0, chai_1.expect)(t.isOnDemand(PHONE_A)).to.equal(false);
|
|
392
|
+
}));
|
|
393
|
+
it('opens none for a phone below iOS 17, and reads its version once', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
394
|
+
t.versions.set('old-iphone', 16.7);
|
|
395
|
+
(0, chai_1.expect)(yield t.borrow('old-iphone')).to.equal(null);
|
|
396
|
+
(0, chai_1.expect)(yield t.borrow('old-iphone')).to.equal(null);
|
|
397
|
+
(0, chai_1.expect)(t.versionReads).to.equal(1);
|
|
398
|
+
(0, chai_1.expect)(t.spawned).to.deep.equal([]);
|
|
399
|
+
}));
|
|
400
|
+
});
|
|
335
401
|
describe('goIosReason', () => {
|
|
336
402
|
it("reads go-ios's last error or warning", () => {
|
|
337
403
|
(0, chai_1.expect)((0, IOSTunnels_1.goIosReason)('{"level":"INFO","msg":"Tunnel server started"}\n')).to.equal(undefined);
|
package/package.json
CHANGED