@xenon-device-management/xenon 2.9.0 → 2.9.2

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 (91) hide show
  1. package/lib/package.json +1 -1
  2. package/lib/public/assets/{AnnotationOverlay-DhRFPZ9-.js → AnnotationOverlay-B87n4pUu.js} +1 -1
  3. package/lib/public/assets/{ApiKeyGate-BmZy1Srn.js → ApiKeyGate-CNZJH6Cs.js} +1 -1
  4. package/lib/public/assets/{BugReportButton-ByLSqwSk.js → BugReportButton-DQ-R87ZV.js} +1 -1
  5. package/lib/public/assets/{DeviceMosaicView-DVfXSoXH.js → DeviceMosaicView-dX3e7O-w.js} +1 -1
  6. package/lib/public/assets/{EmptyState-Bib6wal8.js → EmptyState-D3fG2GHY.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-D5sTaKGR.js → FieldGroup-CUmbJZxI.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-D3XXKTwF.js → FilterMenu-Dm2c-UCk.js} +1 -1
  9. package/lib/public/assets/{Menu-BiZhr0F7.js → Menu-Dl4uXMkH.js} +1 -1
  10. package/lib/public/assets/{Modal-BxGNa3Zp.js → Modal-DNmn2RDs.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-Czml77Wb.js → RecordingPage-Dn3By34o.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-Di7nyHqE.js → RecordingsPage-lCUIM4pm.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-XEKvCRwO.js → SegmentedControl-BdA_mHiZ.js} +1 -1
  14. package/lib/public/assets/{SettingCard-YIpOQAjO.js → SettingCard-Dx3WhK7w.js} +1 -1
  15. package/lib/public/assets/{Table-CqwX6v-1.js → Table-D_2lhvRM.js} +1 -1
  16. package/lib/public/assets/{activity-Cur7Syd6.js → activity-cNHHOKx5.js} +1 -1
  17. package/lib/public/assets/{ai-settings-B1OGdSKu.js → ai-settings-t6RMrDkg.js} +1 -1
  18. package/lib/public/assets/{api-keys-abTul2Oa.js → api-keys-CK_qyQvW.js} +1 -1
  19. package/lib/public/assets/{apps-yQL1MLfG.js → apps-DCiI3UOb.js} +1 -1
  20. package/lib/public/assets/{arrow-left-D0ipNN6o.js → arrow-left-CnYs8HVw.js} +1 -1
  21. package/lib/public/assets/{arrow-right-DwVz_Fly.js → arrow-right-Cc9qm6UG.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-xKuKJfZf.js → arrow-up-right-e7yBOb-c.js} +1 -1
  23. package/lib/public/assets/{auth-shell-00zHLFCk.js → auth-shell-DKrtcIjG.js} +2 -2
  24. package/lib/public/assets/{builds-page-D4RJAbOZ.js → builds-page-CRd5oEz6.js} +1 -1
  25. package/lib/public/assets/{button-Cic7S8s2.js → button-C0caCjJB.js} +1 -1
  26. package/lib/public/assets/{calendar-DyNZ2bq9.js → calendar-BqOOkln_.js} +1 -1
  27. package/lib/public/assets/{check-CXGZdojb.js → check-DYSXRG79.js} +1 -1
  28. package/lib/public/assets/{chevron-left-BcNiIt1C.js → chevron-left-BSG6mcpo.js} +1 -1
  29. package/lib/public/assets/{chevron-right-BxDjkb1I.js → chevron-right-hCNu0CA-.js} +1 -1
  30. package/lib/public/assets/{circle-check-9isFAWQ6.js → circle-check-DnQ263fa.js} +1 -1
  31. package/lib/public/assets/{circle-x-CfnPCFLK.js → circle-x-BaRKZyz9.js} +1 -1
  32. package/lib/public/assets/{clock-Dc4TvnFF.js → clock-XP3iBAhg.js} +1 -1
  33. package/lib/public/assets/{copy-TFQpalJ2.js → copy-D8d9UzWE.js} +1 -1
  34. package/lib/public/assets/{device-explorer-CB4bSJtl.js → device-explorer-pdM3L9TC.js} +1 -1
  35. package/lib/public/assets/{download-1_bq_Ikd.js → download-SaN9-umX.js} +1 -1
  36. package/lib/public/assets/{forgot-password-B92TZ1GF.js → forgot-password-DfUhEZW5.js} +1 -1
  37. package/lib/public/assets/{funnel-DC7DsjHe.js → funnel-OefHE-54.js} +1 -1
  38. package/lib/public/assets/{index-CC2N_uMU.js → index-CzaIyL9t.js} +2 -2
  39. package/lib/public/assets/{index-BSsnx09Q.js → index-IefGCFQQ.js} +1 -1
  40. package/lib/public/assets/{input-D2DBpxK2.js → input-BqFzAQT3.js} +1 -1
  41. package/lib/public/assets/{keycodes-BQuIOMw0.js → keycodes-DDV2OlpQ.js} +1 -1
  42. package/lib/public/assets/{layers-DxtoRlvi.js → layers-a3GNz2DA.js} +1 -1
  43. package/lib/public/assets/{list-checks-jQ7YUgdE.js → list-checks-DMrMAFDt.js} +1 -1
  44. package/lib/public/assets/{lock-WHSsj2dj.js → lock-Eye1CG8p.js} +1 -1
  45. package/lib/public/assets/{login-BB_xyUWw.js → login-Co1IXuen.js} +1 -1
  46. package/lib/public/assets/{maintenance-settings-Dwadp8IQ.js → maintenance-settings-D1fw6iqm.js} +1 -1
  47. package/lib/public/assets/{monitor-DIWQjUlh.js → monitor-U5Y-PxW5.js} +1 -1
  48. package/lib/public/assets/{mouse-pointer-2-D11Re9Gj.js → mouse-pointer-2-CDAwphUm.js} +1 -1
  49. package/lib/public/assets/{network-BYBgSBjv.js → network-BP_U4iDZ.js} +1 -1
  50. package/lib/public/assets/{overview-BISAwoyi.js → overview-CsPyPv10.js} +1 -1
  51. package/lib/public/assets/{page-header-ChX4YiMP.js → page-header-ZdzNEJp9.js} +1 -1
  52. package/lib/public/assets/{plus-BWNj1XSQ.js → plus-1k0sxXyU.js} +1 -1
  53. package/lib/public/assets/{profile-page-VZD3gRjV.js → profile-page-B_GWKRy3.js} +1 -1
  54. package/lib/public/assets/{reset-password-Dz9LzFeX.js → reset-password-DizCTkdw.js} +1 -1
  55. package/lib/public/assets/{runbook-page-Bl2FodZf.js → runbook-page-DWzRiOLZ.js} +1 -1
  56. package/lib/public/assets/{select-Bs9CQu5Z.js → select-CiRW6v17.js} +1 -1
  57. package/lib/public/assets/{selector-detail-page-By7ihhbY.js → selector-detail-page-pA913STP.js} +1 -1
  58. package/lib/public/assets/{selector-health-xiBb8vNz.js → selector-health-CHLX8KqY.js} +1 -1
  59. package/lib/public/assets/{selector-health-page-Y4CTbOLH.js → selector-health-page-vVkg8Mii.js} +1 -1
  60. package/lib/public/assets/{session-detail-page-DqeKRP6c.js → session-detail-page-BCD49zDi.js} +1 -1
  61. package/lib/public/assets/{settings-DDhV_Dnz.js → settings-DhwZoWv7.js} +1 -1
  62. package/lib/public/assets/{stat-tile-DMBTDbwt.js → stat-tile-BaE76cnK.js} +1 -1
  63. package/lib/public/assets/{tablet-Cr5bOgng.js → tablet-CPJV3dBq.js} +1 -1
  64. package/lib/public/assets/{teams-Bm0UQkR6.js → teams-D0IBa2Ov.js} +1 -1
  65. package/lib/public/assets/{trash-2-Di_lMeiP.js → trash-2-u8MlXUBD.js} +1 -1
  66. package/lib/public/assets/{upload-RvXDBmCX.js → upload-Cq8L3kpw.js} +1 -1
  67. package/lib/public/assets/{use-password-reset-mode-C7pUQ7xe.js → use-password-reset-mode-Dbr2HEfw.js} +1 -1
  68. package/lib/public/assets/{users-DZ7B5ePh.js → users-CZqCE511.js} +1 -1
  69. package/lib/public/assets/{users-CIILdbvy.js → users-DxLHYmFm.js} +1 -1
  70. package/lib/public/assets/{video-off-aYil8B-X.js → video-off-mh1EEzP9.js} +1 -1
  71. package/lib/public/assets/{webhook-settings-17JYW1ZR.js → webhook-settings-BYuOib-Q.js} +1 -1
  72. package/lib/public/assets/{zap-D9hchm-a.js → zap-BOpUur0M.js} +1 -1
  73. package/lib/public/index.html +1 -1
  74. package/lib/src/device-managers/ios/IOSDiscoveryService.js +6 -67
  75. package/lib/src/device-managers/ios/IOSStreamService.js +15 -80
  76. package/lib/src/device-managers/ios/IOSTunnels.js +25 -16
  77. package/lib/src/device-managers/ios/WDAClient.js +65 -86
  78. package/lib/src/device-managers/ios/goIosBinary.js +16 -0
  79. package/lib/src/plugin.js +2 -10
  80. package/lib/src/services/PortAllocator.js +8 -1
  81. package/lib/src/services/ServerManager.js +22 -0
  82. package/lib/test/unit/PortAllocator.test.js +1 -1
  83. package/lib/test/unit/boot-go-ios-reap.spec.js +80 -0
  84. package/lib/test/unit/ios-discovery-same-host.spec.js +0 -1
  85. package/lib/test/unit/ios-no-network-fallback.spec.js +219 -0
  86. package/lib/test/unit/ios-stream-appium-tunnel.spec.js +0 -1
  87. package/lib/test/unit/ios-stream-port-leases.spec.js +0 -1
  88. package/lib/test/unit/ios-tunnels.spec.js +25 -3
  89. package/lib/test/unit/port-allocator-pairs.spec.js +34 -0
  90. package/lib/tsconfig.tsbuildinfo +1 -1
  91. package/package.json +1 -1
@@ -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
+ });
@@ -95,7 +95,6 @@ describe("iOS discovery takes only its own host's row for a udid", () => {
95
95
  });
96
96
  svc.trackingInitialized = true; // no usbmux listener
97
97
  sinon_1.default.stub(svc, 'getConnectedDevices').resolves(connected);
98
- sinon_1.default.stub(svc, 'fetchRealDeviceNetworkIp').resolves(''); // no go-ios
99
98
  return svc;
100
99
  }
101
100
  beforeEach(() => {
@@ -0,0 +1,219 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ var __importDefault = (this && this.__importDefault) || function (mod) {
45
+ return (mod && mod.__esModule) ? mod : { "default": mod };
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ require("reflect-metadata");
49
+ const chai_1 = require("chai");
50
+ const axios_1 = __importDefault(require("axios"));
51
+ const child_process_1 = __importDefault(require("child_process"));
52
+ const events_1 = require("events");
53
+ const fs_extra_1 = __importDefault(require("fs-extra"));
54
+ const sinon_1 = __importDefault(require("sinon"));
55
+ const typedi_1 = require("typedi");
56
+ const appium_ios_device_1 = require("appium-ios-device");
57
+ const IOSDiscoveryService_1 = require("../../src/device-managers/ios/IOSDiscoveryService");
58
+ const IOSStreamService_1 = __importDefault(require("../../src/device-managers/ios/IOSStreamService"));
59
+ const WDAClient_1 = require("../../src/device-managers/ios/WDAClient");
60
+ const ownDeviceRow = __importStar(require("../../src/device-managers/ownDeviceRow"));
61
+ const device_store_1 = require("../../src/data-service/device-store");
62
+ const deviceUtils = __importStar(require("../../src/device-utils"));
63
+ const PluginContext_1 = require("../../src/PluginContext");
64
+ const PortAllocator_1 = require("../../src/services/PortAllocator");
65
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
66
+ /**
67
+ * Xenon never reaches WebDriverAgent over the network, only through the
68
+ * phone's own forwarded port on 127.0.0.1.
69
+ *
70
+ * An iPhone's network address was meant to come from go-ios `ios info`, which
71
+ * has no such value (its lockdown values carry no IPAddress), so detection
72
+ * asked for it on every pass for nothing. The fallbacks that used it never
73
+ * ran for an iPhone. A simulator's address is the Mac's own, so they sent a
74
+ * simulator's commands to this Mac's port 8100, which can be an iPhone's
75
+ * WebDriverAgent: WDA names no phone.
76
+ */
77
+ const IP = '192.168.0.104';
78
+ const PHONE = 'test-iphone-00008110-net';
79
+ const ADDRESS = '10.9.8.7';
80
+ describe('iOS: WebDriverAgent only through the phone’s own forwarded port', () => {
81
+ beforeEach(() => {
82
+ sinon_1.default.stub(process, 'kill');
83
+ });
84
+ afterEach(() => sinon_1.default.restore());
85
+ describe('detection', () => {
86
+ let commands;
87
+ function discovery() {
88
+ var _a;
89
+ const ctx = new PluginContext_1.PluginContext();
90
+ ctx.setContext(Object.assign({}, IPluginArgs_1.DefaultPluginArgs, { bindHostOrIp: IP }), 4724, 'test', '');
91
+ const svc = new IOSDiscoveryService_1.IOSDiscoveryService(ctx);
92
+ // resetTestContainer() stubs this on the prototype in a full run.
93
+ const proto = IOSDiscoveryService_1.IOSDiscoveryService.prototype;
94
+ svc.fetchLocalIOSDevices =
95
+ (_a = proto.fetchLocalIOSDevices.wrappedMethod) !== null && _a !== void 0 ? _a : proto.fetchLocalIOSDevices;
96
+ svc.trackingInitialized = true; // no usbmux listener
97
+ sinon_1.default.stub(svc, 'getConnectedDevices').resolves([PHONE]);
98
+ return svc;
99
+ }
100
+ beforeEach(() => {
101
+ commands = [];
102
+ // The go-ios binary is present, and every command it would run is recorded.
103
+ const exists = fs_extra_1.default.existsSync;
104
+ sinon_1.default
105
+ .stub(fs_extra_1.default, 'existsSync')
106
+ .callsFake((p) => String(p).endsWith('goIOS/ios') || exists(p));
107
+ sinon_1.default.stub(child_process_1.default, 'exec').callsFake(((cmd, ...rest) => {
108
+ commands.push(cmd);
109
+ const done = rest.find((a) => typeof a === 'function');
110
+ done === null || done === void 0 ? void 0 : done(null, '{}', '');
111
+ return new events_1.EventEmitter();
112
+ }));
113
+ sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').returns({ findDevice: () => __awaiter(void 0, void 0, void 0, function* () { return null; }) });
114
+ const real = typedi_1.Container.get.bind(typedi_1.Container);
115
+ sinon_1.default.stub(typedi_1.Container, 'get').callsFake((token) => {
116
+ if (token === PortAllocator_1.PortAllocator)
117
+ return { tryAcquire: () => __awaiter(void 0, void 0, void 0, function* () { return 28400; }) };
118
+ if (token === IOSStreamService_1.default)
119
+ return { getStreamStatus: () => undefined };
120
+ return real(token);
121
+ });
122
+ sinon_1.default.stub(deviceUtils, 'getUtilizationTime').resolves(0);
123
+ sinon_1.default.stub(appium_ios_device_1.utilities, 'getOSVersion').resolves('26.5');
124
+ sinon_1.default.stub(appium_ios_device_1.utilities, 'getDeviceName').resolves('iPhone 14 Plus');
125
+ sinon_1.default.stub(appium_ios_device_1.utilities, 'getDeviceInfo').rejects(new Error('no lockdown in tests'));
126
+ });
127
+ const asksForAnAddress = () => commands.filter((c) => /\binfo --udid\b/.test(c));
128
+ it('asks go-ios nothing about a known iPhone on each pass', () => __awaiter(void 0, void 0, void 0, function* () {
129
+ const own = { udid: PHONE, host: `http://${IP}:4724`, platform: 'ios', ip: '' };
130
+ const [phone] = yield discovery().fetchLocalIOSDevices([own]);
131
+ (0, chai_1.expect)(phone.udid).to.equal(PHONE);
132
+ (0, chai_1.expect)(asksForAnAddress()).to.deep.equal([]);
133
+ }));
134
+ it('asks go-ios nothing about the address of an iPhone it adds', () => __awaiter(void 0, void 0, void 0, function* () {
135
+ const phone = yield discovery().getDeviceInfo(PHONE);
136
+ (0, chai_1.expect)(phone.ip).to.equal('');
137
+ (0, chai_1.expect)(asksForAnAddress()).to.deep.equal([]);
138
+ }));
139
+ });
140
+ describe('WDAClient', () => {
141
+ it("retries a failed command on no host but 127.0.0.1, even with the phone's address", () => __awaiter(void 0, void 0, void 0, function* () {
142
+ const urls = [];
143
+ const refuse = (url) => __awaiter(void 0, void 0, void 0, function* () {
144
+ urls.push(url);
145
+ throw Object.assign(new Error(`connect ECONNREFUSED ${url}`), { code: 'ECONNREFUSED' });
146
+ });
147
+ sinon_1.default.stub(axios_1.default, 'post').callsFake(refuse);
148
+ sinon_1.default.stub(axios_1.default, 'get').callsFake(refuse);
149
+ sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').returns({
150
+ findDevice: () => __awaiter(void 0, void 0, void 0, function* () { return ({ udid: PHONE, wdaLocalPort: 28100, ip: ADDRESS }); }),
151
+ });
152
+ const real = typedi_1.Container.get.bind(typedi_1.Container);
153
+ sinon_1.default.stub(typedi_1.Container, 'get').callsFake((token) => {
154
+ if (token === IOSStreamService_1.default) {
155
+ return {
156
+ getStreamStatus: () => undefined,
157
+ getWDASessionId: () => undefined,
158
+ setWDASessionId: () => undefined,
159
+ };
160
+ }
161
+ return real(token);
162
+ });
163
+ const err = yield new WDAClient_1.WDAClient()
164
+ .sendWDACommand(PHONE, 'post', '/wda/tap', { x: 10, y: 10 })
165
+ .then(() => null, (e) => e);
166
+ (0, chai_1.expect)(err === null || err === void 0 ? void 0 : err.message).to.match(/ECONNREFUSED/);
167
+ (0, chai_1.expect)(urls).to.not.deep.equal([]);
168
+ (0, chai_1.expect)(urls.filter((u) => !u.startsWith('http://127.0.0.1:28100/'))).to.deep.equal([]);
169
+ }));
170
+ });
171
+ describe('the stream watchdog', () => {
172
+ let spawned;
173
+ function fakeProcess(exitCode) {
174
+ return Object.assign(new events_1.EventEmitter(), { exitCode, kill: () => true });
175
+ }
176
+ function streamWith(forwardExitCode) {
177
+ // Bypass the constructor: it starts watchdog intervals.
178
+ const svc = Object.create(IOSStreamService_1.default.prototype);
179
+ svc.sessions = new Map([
180
+ [
181
+ PHONE,
182
+ {
183
+ udid: PHONE,
184
+ status: 'running',
185
+ wdaPort: 28100,
186
+ mjpegPort: 29100,
187
+ wdaProcess: fakeProcess(null),
188
+ forwardWDAProcess: fakeProcess(forwardExitCode),
189
+ forwardMJPEGProcess: fakeProcess(null),
190
+ },
191
+ ],
192
+ ]);
193
+ return svc;
194
+ }
195
+ beforeEach(() => {
196
+ spawned = [];
197
+ sinon_1.default.stub(child_process_1.default, 'spawn').callsFake(((cmd, args) => {
198
+ spawned.push([cmd, ...(args !== null && args !== void 0 ? args : [])].join(' '));
199
+ return fakeProcess(null);
200
+ }));
201
+ // Something answers ready on the phone's address, port 8100; the
202
+ // phone's own forwarded port refuses.
203
+ sinon_1.default.stub(axios_1.default, 'get').callsFake(((url) => __awaiter(void 0, void 0, void 0, function* () {
204
+ if (url.startsWith(`http://${ADDRESS}:`))
205
+ return { status: 200, data: { value: { ready: true } } };
206
+ throw Object.assign(new Error('connect ECONNREFUSED'), { code: 'ECONNREFUSED' });
207
+ })));
208
+ sinon_1.default.stub(ownDeviceRow, 'findOwnDevice').resolves({ udid: PHONE, ip: ADDRESS });
209
+ });
210
+ it('calls a stream whose forward died unresponsive, whatever answers on the network', () => __awaiter(void 0, void 0, void 0, function* () {
211
+ (0, chai_1.expect)(yield streamWith(1).isStreamResponsive(PHONE)).to.equal(false);
212
+ (0, chai_1.expect)(spawned, 'no forward-only restart').to.deep.equal([]);
213
+ }));
214
+ it('calls a stream whose forward is hung unresponsive, whatever answers on the network', () => __awaiter(void 0, void 0, void 0, function* () {
215
+ (0, chai_1.expect)(yield streamWith(null).isStreamResponsive(PHONE)).to.equal(false);
216
+ (0, chai_1.expect)(spawned, 'no forward-only restart').to.deep.equal([]);
217
+ }));
218
+ });
219
+ });
@@ -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]);