@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.
Files changed (88) hide show
  1. package/lib/package.json +1 -1
  2. package/lib/public/assets/{AnnotationOverlay-DhRFPZ9-.js → AnnotationOverlay-Dm_URoOL.js} +1 -1
  3. package/lib/public/assets/{ApiKeyGate-BmZy1Srn.js → ApiKeyGate-BdGOuXDm.js} +1 -1
  4. package/lib/public/assets/{BugReportButton-ByLSqwSk.js → BugReportButton-BuWGEjgw.js} +1 -1
  5. package/lib/public/assets/{DeviceMosaicView-DVfXSoXH.js → DeviceMosaicView-Pu3rJ99D.js} +1 -1
  6. package/lib/public/assets/{EmptyState-Bib6wal8.js → EmptyState-syyHElMd.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-D5sTaKGR.js → FieldGroup-BZR5ZBVv.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-D3XXKTwF.js → FilterMenu-CsgtrmPb.js} +1 -1
  9. package/lib/public/assets/{Menu-BiZhr0F7.js → Menu-DP8XOm1A.js} +1 -1
  10. package/lib/public/assets/{Modal-BxGNa3Zp.js → Modal-Dtd-8uI2.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-Czml77Wb.js → RecordingPage-CdAtAmHj.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-Di7nyHqE.js → RecordingsPage-Br3CKxTe.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-XEKvCRwO.js → SegmentedControl-BOaNmUL-.js} +1 -1
  14. package/lib/public/assets/{SettingCard-YIpOQAjO.js → SettingCard-CQuK9ZZX.js} +1 -1
  15. package/lib/public/assets/{Table-CqwX6v-1.js → Table-CswCefnT.js} +1 -1
  16. package/lib/public/assets/{activity-Cur7Syd6.js → activity-6fhH3u4C.js} +1 -1
  17. package/lib/public/assets/{ai-settings-B1OGdSKu.js → ai-settings-CU24L3D4.js} +1 -1
  18. package/lib/public/assets/{api-keys-abTul2Oa.js → api-keys-DC3BZDaG.js} +1 -1
  19. package/lib/public/assets/{apps-yQL1MLfG.js → apps-Ce5Z-wNc.js} +1 -1
  20. package/lib/public/assets/{arrow-left-D0ipNN6o.js → arrow-left-DZJqYn7r.js} +1 -1
  21. package/lib/public/assets/{arrow-right-DwVz_Fly.js → arrow-right-nIvK8CyN.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-xKuKJfZf.js → arrow-up-right-BfugJj3f.js} +1 -1
  23. package/lib/public/assets/{auth-shell-00zHLFCk.js → auth-shell-B1Cz75Vq.js} +2 -2
  24. package/lib/public/assets/{builds-page-D4RJAbOZ.js → builds-page-5mlJGjIk.js} +1 -1
  25. package/lib/public/assets/{button-Cic7S8s2.js → button-D_W88sn-.js} +1 -1
  26. package/lib/public/assets/{calendar-DyNZ2bq9.js → calendar-Dc6pWZS9.js} +1 -1
  27. package/lib/public/assets/{check-CXGZdojb.js → check-BbvtnP8m.js} +1 -1
  28. package/lib/public/assets/{chevron-left-BcNiIt1C.js → chevron-left-DQDpR9ne.js} +1 -1
  29. package/lib/public/assets/{chevron-right-BxDjkb1I.js → chevron-right-CgBUaRCA.js} +1 -1
  30. package/lib/public/assets/{circle-check-9isFAWQ6.js → circle-check-BWjEKvA4.js} +1 -1
  31. package/lib/public/assets/{circle-x-CfnPCFLK.js → circle-x-BLUdeRhT.js} +1 -1
  32. package/lib/public/assets/{clock-Dc4TvnFF.js → clock-PAhKG1jT.js} +1 -1
  33. package/lib/public/assets/{copy-TFQpalJ2.js → copy-2CHdAPBf.js} +1 -1
  34. package/lib/public/assets/{device-explorer-CB4bSJtl.js → device-explorer-EyAhEL_S.js} +1 -1
  35. package/lib/public/assets/{download-1_bq_Ikd.js → download-B1vKGhGP.js} +1 -1
  36. package/lib/public/assets/{forgot-password-B92TZ1GF.js → forgot-password-NSrrBA5N.js} +1 -1
  37. package/lib/public/assets/{funnel-DC7DsjHe.js → funnel-C9mzkCSI.js} +1 -1
  38. package/lib/public/assets/{index-BSsnx09Q.js → index-BEk1D5-B.js} +1 -1
  39. package/lib/public/assets/{index-CC2N_uMU.js → index-BxaH84UO.js} +2 -2
  40. package/lib/public/assets/{input-D2DBpxK2.js → input-cA_lrNIw.js} +1 -1
  41. package/lib/public/assets/{keycodes-BQuIOMw0.js → keycodes-DZLCZ-uv.js} +1 -1
  42. package/lib/public/assets/{layers-DxtoRlvi.js → layers-BuXMIJz3.js} +1 -1
  43. package/lib/public/assets/{list-checks-jQ7YUgdE.js → list-checks-B8q68tPG.js} +1 -1
  44. package/lib/public/assets/{lock-WHSsj2dj.js → lock-ChoclF8X.js} +1 -1
  45. package/lib/public/assets/{login-BB_xyUWw.js → login-63DiXJ0M.js} +1 -1
  46. package/lib/public/assets/{maintenance-settings-Dwadp8IQ.js → maintenance-settings-A47Y-PYy.js} +1 -1
  47. package/lib/public/assets/{monitor-DIWQjUlh.js → monitor-BYPF43Ds.js} +1 -1
  48. package/lib/public/assets/{mouse-pointer-2-D11Re9Gj.js → mouse-pointer-2-Pcl7inmZ.js} +1 -1
  49. package/lib/public/assets/{network-BYBgSBjv.js → network-ChaLfnWe.js} +1 -1
  50. package/lib/public/assets/{overview-BISAwoyi.js → overview-bj_vktQG.js} +1 -1
  51. package/lib/public/assets/{page-header-ChX4YiMP.js → page-header-bv2AQSMG.js} +1 -1
  52. package/lib/public/assets/{plus-BWNj1XSQ.js → plus-u0x2Y5uW.js} +1 -1
  53. package/lib/public/assets/{profile-page-VZD3gRjV.js → profile-page-Cvmef2iY.js} +1 -1
  54. package/lib/public/assets/{reset-password-Dz9LzFeX.js → reset-password-DqStVhBo.js} +1 -1
  55. package/lib/public/assets/{runbook-page-Bl2FodZf.js → runbook-page-UK0C95QF.js} +1 -1
  56. package/lib/public/assets/{select-Bs9CQu5Z.js → select-CQ6O804R.js} +1 -1
  57. package/lib/public/assets/{selector-detail-page-By7ihhbY.js → selector-detail-page-D-PjU6mC.js} +1 -1
  58. package/lib/public/assets/{selector-health-xiBb8vNz.js → selector-health-CVm0cTOU.js} +1 -1
  59. package/lib/public/assets/{selector-health-page-Y4CTbOLH.js → selector-health-page-BdibBs8g.js} +1 -1
  60. package/lib/public/assets/{session-detail-page-DqeKRP6c.js → session-detail-page-BijicGOF.js} +1 -1
  61. package/lib/public/assets/{settings-DDhV_Dnz.js → settings-Cva7V4n-.js} +1 -1
  62. package/lib/public/assets/{stat-tile-DMBTDbwt.js → stat-tile-BdIoEoou.js} +1 -1
  63. package/lib/public/assets/{tablet-Cr5bOgng.js → tablet-BPtlTsKn.js} +1 -1
  64. package/lib/public/assets/{teams-Bm0UQkR6.js → teams-D4r1UMME.js} +1 -1
  65. package/lib/public/assets/{trash-2-Di_lMeiP.js → trash-2-CnysPtdR.js} +1 -1
  66. package/lib/public/assets/{upload-RvXDBmCX.js → upload-Uyj4-Cyq.js} +1 -1
  67. package/lib/public/assets/{use-password-reset-mode-C7pUQ7xe.js → use-password-reset-mode-Du-ykGHP.js} +1 -1
  68. package/lib/public/assets/{users-CIILdbvy.js → users-Cs00GNmy.js} +1 -1
  69. package/lib/public/assets/{users-DZ7B5ePh.js → users-ttkwKBos.js} +1 -1
  70. package/lib/public/assets/{video-off-aYil8B-X.js → video-off-BYa4qD8N.js} +1 -1
  71. package/lib/public/assets/{webhook-settings-17JYW1ZR.js → webhook-settings-DuEnPlet.js} +1 -1
  72. package/lib/public/assets/{zap-D9hchm-a.js → zap-CxHn6Vwu.js} +1 -1
  73. package/lib/public/index.html +1 -1
  74. package/lib/src/device-managers/ios/IOSDiscoveryService.js +2 -4
  75. package/lib/src/device-managers/ios/IOSStreamService.js +8 -14
  76. package/lib/src/device-managers/ios/IOSTunnels.js +25 -16
  77. package/lib/src/device-managers/ios/goIosBinary.js +16 -0
  78. package/lib/src/plugin.js +2 -10
  79. package/lib/src/services/PortAllocator.js +8 -1
  80. package/lib/src/services/ServerManager.js +22 -0
  81. package/lib/test/unit/PortAllocator.test.js +1 -1
  82. package/lib/test/unit/boot-go-ios-reap.spec.js +80 -0
  83. package/lib/test/unit/ios-stream-appium-tunnel.spec.js +0 -1
  84. package/lib/test/unit/ios-stream-port-leases.spec.js +0 -1
  85. package/lib/test/unit/ios-tunnels.spec.js +25 -3
  86. package/lib/test/unit/port-allocator-pairs.spec.js +34 -0
  87. package/lib/tsconfig.tsbuildinfo +1 -1
  88. 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
- // Port-lease TTL for an active stream. Longer than the watchdog interval (1h),
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, this.STREAM_PORT_TTL_MS);
147
- yield portAllocator.touch(session.mjpegPort, this.STREAM_PORT_TTL_MS);
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: this.STREAM_PORT_TTL_MS };
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.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.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
- * A tunnel's port lease: the same as the stream's own ports
66
- * (IOSStreamService's STREAM_PORT_TTL_MS). The stream watchdog refreshes both
67
- * every hour, the tunnel's through {@link IOSTunnels.touch}.
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.TUNNEL_LEASE_TTL_MS = 90 * 60 * 1000;
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 = path_1.default.join((0, helpers_1.cachePath)('goIOS'), 'ios');
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.TUNNEL_LEASE_TTL_MS,
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
- proc.once('error', (err) => {
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
- const polls = exports.TUNNEL_READY_TIMEOUT_MS / exports.TUNNEL_READY_POLL_MS;
234
- for (let poll = 0; poll <= polls; poll++) {
235
- if (poll > 0)
236
- yield this.sleep(exports.TUNNEL_READY_POLL_MS);
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.TUNNEL_LEASE_TTL_MS);
380
- yield allocator.touch(tunnel.port + 1, exports.TUNNEL_LEASE_TTL_MS);
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
- // Reap orphan go-ios tunnels left by a previous run or a hard-killed/crashed
161
- // process. A fresh server owns no tunnels, so any surviving go-ios agents are
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({ where: { leasedToUdid: udid } });
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('releaseForUdid deletes all leases for that UDID', () => __awaiter(void 0, void 0, void 0, function* () {
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
  }
@@ -89,7 +89,6 @@ function iosService() {
89
89
  svc.startFlight = new singleFlight_1.SingleFlight();
90
90
  svc.recoveryCooldowns = new Map();
91
91
  svc.RECOVERY_COOLDOWN_MS = 30000;
92
- svc.STREAM_PORT_TTL_MS = 90 * 60 * 1000;
93
92
  svc.goIOSPath = '/nonexistent/go-ios';
94
93
  return svc;
95
94
  }
@@ -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, { ttlMs: IOSTunnels_1.TUNNEL_LEASE_TTL_MS })).to.equal(true);
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.TUNNEL_LEASE_TTL_MS],
274
- [12101, IOSTunnels_1.TUNNEL_LEASE_TTL_MS],
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]);