@xenon-device-management/xenon 2.9.0 → 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-DhRFPZ9-.js → AnnotationOverlay-Dm_URoOL.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-BmZy1Srn.js → ApiKeyGate-BdGOuXDm.js} +1 -1
- package/lib/public/assets/{BugReportButton-ByLSqwSk.js → BugReportButton-BuWGEjgw.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-DVfXSoXH.js → DeviceMosaicView-Pu3rJ99D.js} +1 -1
- package/lib/public/assets/{EmptyState-Bib6wal8.js → EmptyState-syyHElMd.js} +1 -1
- package/lib/public/assets/{FieldGroup-D5sTaKGR.js → FieldGroup-BZR5ZBVv.js} +1 -1
- package/lib/public/assets/{FilterMenu-D3XXKTwF.js → FilterMenu-CsgtrmPb.js} +1 -1
- package/lib/public/assets/{Menu-BiZhr0F7.js → Menu-DP8XOm1A.js} +1 -1
- package/lib/public/assets/{Modal-BxGNa3Zp.js → Modal-Dtd-8uI2.js} +1 -1
- package/lib/public/assets/{RecordingPage-Czml77Wb.js → RecordingPage-CdAtAmHj.js} +1 -1
- package/lib/public/assets/{RecordingsPage-Di7nyHqE.js → RecordingsPage-Br3CKxTe.js} +1 -1
- package/lib/public/assets/{SegmentedControl-XEKvCRwO.js → SegmentedControl-BOaNmUL-.js} +1 -1
- package/lib/public/assets/{SettingCard-YIpOQAjO.js → SettingCard-CQuK9ZZX.js} +1 -1
- package/lib/public/assets/{Table-CqwX6v-1.js → Table-CswCefnT.js} +1 -1
- package/lib/public/assets/{activity-Cur7Syd6.js → activity-6fhH3u4C.js} +1 -1
- package/lib/public/assets/{ai-settings-B1OGdSKu.js → ai-settings-CU24L3D4.js} +1 -1
- package/lib/public/assets/{api-keys-abTul2Oa.js → api-keys-DC3BZDaG.js} +1 -1
- package/lib/public/assets/{apps-yQL1MLfG.js → apps-Ce5Z-wNc.js} +1 -1
- package/lib/public/assets/{arrow-left-D0ipNN6o.js → arrow-left-DZJqYn7r.js} +1 -1
- package/lib/public/assets/{arrow-right-DwVz_Fly.js → arrow-right-nIvK8CyN.js} +1 -1
- package/lib/public/assets/{arrow-up-right-xKuKJfZf.js → arrow-up-right-BfugJj3f.js} +1 -1
- package/lib/public/assets/{auth-shell-00zHLFCk.js → auth-shell-B1Cz75Vq.js} +2 -2
- package/lib/public/assets/{builds-page-D4RJAbOZ.js → builds-page-5mlJGjIk.js} +1 -1
- package/lib/public/assets/{button-Cic7S8s2.js → button-D_W88sn-.js} +1 -1
- package/lib/public/assets/{calendar-DyNZ2bq9.js → calendar-Dc6pWZS9.js} +1 -1
- package/lib/public/assets/{check-CXGZdojb.js → check-BbvtnP8m.js} +1 -1
- package/lib/public/assets/{chevron-left-BcNiIt1C.js → chevron-left-DQDpR9ne.js} +1 -1
- package/lib/public/assets/{chevron-right-BxDjkb1I.js → chevron-right-CgBUaRCA.js} +1 -1
- package/lib/public/assets/{circle-check-9isFAWQ6.js → circle-check-BWjEKvA4.js} +1 -1
- package/lib/public/assets/{circle-x-CfnPCFLK.js → circle-x-BLUdeRhT.js} +1 -1
- package/lib/public/assets/{clock-Dc4TvnFF.js → clock-PAhKG1jT.js} +1 -1
- package/lib/public/assets/{copy-TFQpalJ2.js → copy-2CHdAPBf.js} +1 -1
- package/lib/public/assets/{device-explorer-CB4bSJtl.js → device-explorer-EyAhEL_S.js} +1 -1
- package/lib/public/assets/{download-1_bq_Ikd.js → download-B1vKGhGP.js} +1 -1
- package/lib/public/assets/{forgot-password-B92TZ1GF.js → forgot-password-NSrrBA5N.js} +1 -1
- package/lib/public/assets/{funnel-DC7DsjHe.js → funnel-C9mzkCSI.js} +1 -1
- package/lib/public/assets/{index-BSsnx09Q.js → index-BEk1D5-B.js} +1 -1
- package/lib/public/assets/{index-CC2N_uMU.js → index-BxaH84UO.js} +2 -2
- package/lib/public/assets/{input-D2DBpxK2.js → input-cA_lrNIw.js} +1 -1
- package/lib/public/assets/{keycodes-BQuIOMw0.js → keycodes-DZLCZ-uv.js} +1 -1
- package/lib/public/assets/{layers-DxtoRlvi.js → layers-BuXMIJz3.js} +1 -1
- package/lib/public/assets/{list-checks-jQ7YUgdE.js → list-checks-B8q68tPG.js} +1 -1
- package/lib/public/assets/{lock-WHSsj2dj.js → lock-ChoclF8X.js} +1 -1
- package/lib/public/assets/{login-BB_xyUWw.js → login-63DiXJ0M.js} +1 -1
- package/lib/public/assets/{maintenance-settings-Dwadp8IQ.js → maintenance-settings-A47Y-PYy.js} +1 -1
- package/lib/public/assets/{monitor-DIWQjUlh.js → monitor-BYPF43Ds.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-D11Re9Gj.js → mouse-pointer-2-Pcl7inmZ.js} +1 -1
- package/lib/public/assets/{network-BYBgSBjv.js → network-ChaLfnWe.js} +1 -1
- package/lib/public/assets/{overview-BISAwoyi.js → overview-bj_vktQG.js} +1 -1
- package/lib/public/assets/{page-header-ChX4YiMP.js → page-header-bv2AQSMG.js} +1 -1
- package/lib/public/assets/{plus-BWNj1XSQ.js → plus-u0x2Y5uW.js} +1 -1
- package/lib/public/assets/{profile-page-VZD3gRjV.js → profile-page-Cvmef2iY.js} +1 -1
- package/lib/public/assets/{reset-password-Dz9LzFeX.js → reset-password-DqStVhBo.js} +1 -1
- package/lib/public/assets/{runbook-page-Bl2FodZf.js → runbook-page-UK0C95QF.js} +1 -1
- package/lib/public/assets/{select-Bs9CQu5Z.js → select-CQ6O804R.js} +1 -1
- package/lib/public/assets/{selector-detail-page-By7ihhbY.js → selector-detail-page-D-PjU6mC.js} +1 -1
- package/lib/public/assets/{selector-health-xiBb8vNz.js → selector-health-CVm0cTOU.js} +1 -1
- package/lib/public/assets/{selector-health-page-Y4CTbOLH.js → selector-health-page-BdibBs8g.js} +1 -1
- package/lib/public/assets/{session-detail-page-DqeKRP6c.js → session-detail-page-BijicGOF.js} +1 -1
- package/lib/public/assets/{settings-DDhV_Dnz.js → settings-Cva7V4n-.js} +1 -1
- package/lib/public/assets/{stat-tile-DMBTDbwt.js → stat-tile-BdIoEoou.js} +1 -1
- package/lib/public/assets/{tablet-Cr5bOgng.js → tablet-BPtlTsKn.js} +1 -1
- package/lib/public/assets/{teams-Bm0UQkR6.js → teams-D4r1UMME.js} +1 -1
- package/lib/public/assets/{trash-2-Di_lMeiP.js → trash-2-CnysPtdR.js} +1 -1
- package/lib/public/assets/{upload-RvXDBmCX.js → upload-Uyj4-Cyq.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-C7pUQ7xe.js → use-password-reset-mode-Du-ykGHP.js} +1 -1
- package/lib/public/assets/{users-CIILdbvy.js → users-Cs00GNmy.js} +1 -1
- package/lib/public/assets/{users-DZ7B5ePh.js → users-ttkwKBos.js} +1 -1
- package/lib/public/assets/{video-off-aYil8B-X.js → video-off-BYa4qD8N.js} +1 -1
- package/lib/public/assets/{webhook-settings-17JYW1ZR.js → webhook-settings-DuEnPlet.js} +1 -1
- package/lib/public/assets/{zap-D9hchm-a.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 +8 -14
- package/lib/src/device-managers/ios/IOSTunnels.js +25 -16
- 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-stream-appium-tunnel.spec.js +0 -1
- package/lib/test/unit/ios-stream-port-leases.spec.js +0 -1
- package/lib/test/unit/ios-tunnels.spec.js +25 -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
|
*/
|
|
@@ -594,7 +586,7 @@ let IOSStreamService = class IOSStreamService {
|
|
|
594
586
|
// guarantee a non-colliding port; the lease is refreshed by the watchdog
|
|
595
587
|
// and released in stopStream().
|
|
596
588
|
const portAllocator = typedi_2.Container.get(PortAllocator_1.PortAllocator);
|
|
597
|
-
const ttl = { ttlMs:
|
|
589
|
+
const ttl = { ttlMs: IOSTunnels_1.IOS_STREAM_LEASE_TTL_MS };
|
|
598
590
|
// Principal Discovery: attach to the WDA an Appium session runs on this
|
|
599
591
|
// device instead of launching a second one over it. The one exception
|
|
600
592
|
// to the rule above: that session's forwarder listens on the Device
|
|
@@ -1101,7 +1093,9 @@ let IOSStreamService = class IOSStreamService {
|
|
|
1101
1093
|
* server owns no legitimate tunnel — i.e. on fresh boot (orphans from a
|
|
1102
1094
|
* previous run, or a hard-killed / crashed process whose graceful cleanup
|
|
1103
1095
|
* never ran) or during full shutdown. This is the catch-all that stops the
|
|
1104
|
-
* 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).
|
|
1105
1099
|
*/
|
|
1106
1100
|
reapOrphanTunnels() {
|
|
1107
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_BORROW_IDLE_MS = 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,25 +48,26 @@ 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;
|
|
72
73
|
/**
|
|
@@ -126,7 +127,7 @@ let IOSTunnels = class IOSTunnels {
|
|
|
126
127
|
this.tunnels = new Map();
|
|
127
128
|
this.checking = false;
|
|
128
129
|
this.versionCache = new Map();
|
|
129
|
-
this.goIOSPath =
|
|
130
|
+
this.goIOSPath = (0, goIosBinary_1.goIosBinaryPath)();
|
|
130
131
|
}
|
|
131
132
|
/**
|
|
132
133
|
* The phone's tunnel-info port for its stream, starting its tunnel if it
|
|
@@ -169,7 +170,7 @@ let IOSTunnels = class IOSTunnels {
|
|
|
169
170
|
if (version === null || version < 17)
|
|
170
171
|
return null;
|
|
171
172
|
const port = yield this.allocator().acquirePair('tunnel', udid, {
|
|
172
|
-
ttlMs: exports.
|
|
173
|
+
ttlMs: exports.IOS_STREAM_LEASE_TTL_MS,
|
|
173
174
|
});
|
|
174
175
|
let proc;
|
|
175
176
|
try {
|
|
@@ -209,7 +210,9 @@ let IOSTunnels = class IOSTunnels {
|
|
|
209
210
|
void this.releasePair(port, udid);
|
|
210
211
|
};
|
|
211
212
|
proc.once('exit', ended);
|
|
212
|
-
|
|
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) => {
|
|
213
216
|
this.log.warn(`[${udid}] go-ios tunnel failed: ${err.message}`);
|
|
214
217
|
ended();
|
|
215
218
|
});
|
|
@@ -230,10 +233,16 @@ let IOSTunnels = class IOSTunnels {
|
|
|
230
233
|
waitUntilReady(udid, tunnel) {
|
|
231
234
|
return __awaiter(this, void 0, void 0, function* () {
|
|
232
235
|
const { port, process: proc } = tunnel;
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
+
}
|
|
237
246
|
if (this.tunnels.get(udid) !== tunnel) {
|
|
238
247
|
throw new Error(`The go-ios tunnel for ${udid} exited before it was ready (exit code ${proc.exitCode})` +
|
|
239
248
|
(tunnel.reason ? `: ${tunnel.reason}` : ''));
|
|
@@ -376,8 +385,8 @@ let IOSTunnels = class IOSTunnels {
|
|
|
376
385
|
if (!tunnel)
|
|
377
386
|
return;
|
|
378
387
|
const allocator = this.allocator();
|
|
379
|
-
yield allocator.touch(tunnel.port, exports.
|
|
380
|
-
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);
|
|
381
390
|
});
|
|
382
391
|
}
|
|
383
392
|
releasePair(port, 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
|
+
});
|
|
@@ -105,7 +105,6 @@ function iosService() {
|
|
|
105
105
|
svc.startFlight = new singleFlight_1.SingleFlight();
|
|
106
106
|
svc.recoveryCooldowns = new Map();
|
|
107
107
|
svc.RECOVERY_COOLDOWN_MS = 30000;
|
|
108
|
-
svc.STREAM_PORT_TTL_MS = 90 * 60 * 1000;
|
|
109
108
|
svc.goIOSPath = '/nonexistent/go-ios';
|
|
110
109
|
return svc;
|
|
111
110
|
}
|
|
@@ -114,6 +114,7 @@ class TestTunnels extends IOSTunnels_1.IOSTunnels {
|
|
|
114
114
|
sleep(ms) {
|
|
115
115
|
return __awaiter(this, void 0, void 0, function* () {
|
|
116
116
|
this.sleeps.push(ms);
|
|
117
|
+
this.clock += ms;
|
|
117
118
|
});
|
|
118
119
|
}
|
|
119
120
|
killGroup(pid) {
|
|
@@ -150,7 +151,9 @@ describe('IOSTunnels: a go-ios tunnel per iPhone', () => {
|
|
|
150
151
|
it("leases a pair and starts the phone's own agent on it", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
151
152
|
const port = yield t.ensure(PHONE_A);
|
|
152
153
|
(0, chai_1.expect)(port).to.equal(12100);
|
|
153
|
-
(0, chai_1.expect)(t.ports.acquirePair.calledOnceWithExactly('tunnel', PHONE_A, {
|
|
154
|
+
(0, chai_1.expect)(t.ports.acquirePair.calledOnceWithExactly('tunnel', PHONE_A, {
|
|
155
|
+
ttlMs: IOSTunnels_1.IOS_STREAM_LEASE_TTL_MS,
|
|
156
|
+
})).to.equal(true);
|
|
154
157
|
(0, chai_1.expect)(t.spawned.map((s) => s.args)).to.deep.equal([
|
|
155
158
|
['tunnel', 'start', '--udid', PHONE_A, '--userspace', '--tunnel-info-port', '12100'],
|
|
156
159
|
]);
|
|
@@ -176,6 +179,25 @@ describe('IOSTunnels: a go-ios tunnel per iPhone', () => {
|
|
|
176
179
|
(0, chai_1.expect)(t.sleeps.reduce((a, b) => a + b, 0)).to.equal(IOSTunnels_1.TUNNEL_READY_TIMEOUT_MS);
|
|
177
180
|
(0, chai_1.expect)(t.portFor(PHONE_A), 'still tracked').to.equal(12100);
|
|
178
181
|
}));
|
|
182
|
+
it('goes on at the 20 s deadline when each check is slow to answer', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
183
|
+
const started = t.clock;
|
|
184
|
+
t.answers = () => {
|
|
185
|
+
t.clock += 1000; // a check that runs into its 1 s timeout
|
|
186
|
+
return { state: 'unknown' };
|
|
187
|
+
};
|
|
188
|
+
(0, chai_1.expect)(yield t.ensure(PHONE_A)).to.equal(12100);
|
|
189
|
+
// At most the one check that was under way at the deadline.
|
|
190
|
+
(0, chai_1.expect)(t.clock - started).to.be.at.most(IOSTunnels_1.TUNNEL_READY_TIMEOUT_MS + 1000);
|
|
191
|
+
}));
|
|
192
|
+
it('outlives a second error from the tunnel process, and gives its ports back once', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
193
|
+
yield t.ensure(PHONE_A);
|
|
194
|
+
const proc = t.spawned[0].proc;
|
|
195
|
+
proc.emit('error', new Error('spawn EACCES'));
|
|
196
|
+
(0, chai_1.expect)(() => proc.emit('error', new Error('kill ESRCH'))).not.to.throw();
|
|
197
|
+
yield settle();
|
|
198
|
+
(0, chai_1.expect)(t.portFor(PHONE_A)).to.equal(undefined);
|
|
199
|
+
(0, chai_1.expect)(t.ports.release.callCount, 'P and P + 1, once each').to.equal(2);
|
|
200
|
+
}));
|
|
179
201
|
it('fails the start when the tunnel exits before it is ready, and gives its ports back', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
180
202
|
t.answers = () => {
|
|
181
203
|
t.spawned[0].proc.exit(1);
|
|
@@ -270,8 +292,8 @@ describe('IOSTunnels: a go-ios tunnel per iPhone', () => {
|
|
|
270
292
|
yield t.touch(PHONE_A);
|
|
271
293
|
yield t.touch(PHONE_B); // no tunnel: nothing to touch
|
|
272
294
|
(0, chai_1.expect)(t.ports.touch.args).to.deep.equal([
|
|
273
|
-
[12100, IOSTunnels_1.
|
|
274
|
-
[12101, IOSTunnels_1.
|
|
295
|
+
[12100, IOSTunnels_1.IOS_STREAM_LEASE_TTL_MS],
|
|
296
|
+
[12101, IOSTunnels_1.IOS_STREAM_LEASE_TTL_MS],
|
|
275
297
|
]);
|
|
276
298
|
}));
|
|
277
299
|
});
|
|
@@ -14,6 +14,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
const chai_1 = require("chai");
|
|
16
16
|
const net_1 = __importDefault(require("net"));
|
|
17
|
+
const prisma_1 = require("../../src/prisma");
|
|
17
18
|
const PortAllocator_1 = require("../../src/services/PortAllocator");
|
|
18
19
|
const scratch_port_leases_1 = require("../helpers/scratch-port-leases");
|
|
19
20
|
/**
|
|
@@ -73,6 +74,31 @@ describe('PortAllocator.acquirePair (go-ios tunnel ports)', () => {
|
|
|
73
74
|
(0, chai_1.expect)(yield a.acquirePair('tunnel', 'phone-b')).to.equal(b + 2);
|
|
74
75
|
(0, chai_1.expect)(yield leasesOf('phone-b')).to.deep.equal([b + 2, b + 3]);
|
|
75
76
|
}));
|
|
77
|
+
it('gives two phones asking at once different pairs', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
78
|
+
const b = scratch.base;
|
|
79
|
+
const a = allocator([b, b + 9]);
|
|
80
|
+
const [first, second] = yield Promise.all([
|
|
81
|
+
a.acquirePair('tunnel', 'phone-a'),
|
|
82
|
+
a.acquirePair('tunnel', 'phone-b'),
|
|
83
|
+
]);
|
|
84
|
+
(0, chai_1.expect)(first).to.not.equal(second);
|
|
85
|
+
(0, chai_1.expect)(yield leasesOf('phone-a')).to.deep.equal([first, first + 1]);
|
|
86
|
+
(0, chai_1.expect)(yield leasesOf('phone-b')).to.deep.equal([second, second + 1]);
|
|
87
|
+
}));
|
|
88
|
+
it('gives the first port back when the second is taken mid-pair, and moves on', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
89
|
+
const b = scratch.base;
|
|
90
|
+
const create = prisma_1.prisma.portLease.create;
|
|
91
|
+
create.callsFake((args) => __awaiter(void 0, void 0, void 0, function* () {
|
|
92
|
+
if (args.data.port === b + 1 && args.data.leasedToUdid === 'phone-a') {
|
|
93
|
+
// Another phone takes P + 1 after the free ports were read.
|
|
94
|
+
yield leaseTo('phone-b', b + 1, 'tunnel');
|
|
95
|
+
}
|
|
96
|
+
return scratch.db.portLease.create(args);
|
|
97
|
+
}));
|
|
98
|
+
(0, chai_1.expect)(yield allocator([b, b + 5]).acquirePair('tunnel', 'phone-a')).to.equal(b + 2);
|
|
99
|
+
(0, chai_1.expect)(yield leasesOf('phone-a')).to.deep.equal([b + 2, b + 3]);
|
|
100
|
+
(0, chai_1.expect)(yield leasesOf('phone-b')).to.deep.equal([b + 1]);
|
|
101
|
+
}));
|
|
76
102
|
it('skips a pair whose second port is leased, whatever for, and keeps none of it', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
77
103
|
const b = scratch.base;
|
|
78
104
|
yield leaseTo('another-device', b + 1, 'wda');
|
|
@@ -99,6 +125,14 @@ describe('PortAllocator.acquirePair (go-ios tunnel ports)', () => {
|
|
|
99
125
|
(0, chai_1.expect)(err === null || err === void 0 ? void 0 : err.message).to.match(/tunnel/);
|
|
100
126
|
(0, chai_1.expect)(yield leasesOf('phone-a')).to.deep.equal([]);
|
|
101
127
|
}));
|
|
128
|
+
it("releaseForUdid, when a session ends, leaves the phone's tunnel pair to its tunnel", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
129
|
+
const b = scratch.base;
|
|
130
|
+
const a = allocator([b, b + 5]);
|
|
131
|
+
yield a.acquirePair('tunnel', 'phone-a');
|
|
132
|
+
yield leaseTo('phone-a', b + 50, 'wda');
|
|
133
|
+
yield a.releaseForUdid('phone-a');
|
|
134
|
+
(0, chai_1.expect)(yield leasesOf('phone-a')).to.deep.equal([b, b + 1]);
|
|
135
|
+
}));
|
|
102
136
|
it("releasePurpose deletes only that purpose's leases", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
103
137
|
const b = scratch.base;
|
|
104
138
|
const a = allocator([b, b + 5]);
|