@xenon-device-management/xenon 2.6.2 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/lib/package.json +1 -1
  2. package/lib/public/assets/{AnnotationOverlay-U9aJANVi.js → AnnotationOverlay-XmDGA9CJ.js} +1 -1
  3. package/lib/public/assets/{ApiKeyGate-6265m84B.js → ApiKeyGate-BilRCzB-.js} +1 -1
  4. package/lib/public/assets/{BugReportButton-CGeVEuQ-.js → BugReportButton-_buEV98z.js} +1 -1
  5. package/lib/public/assets/{DeviceMosaicView-C_3HwhVZ.js → DeviceMosaicView-DTEiC4gk.js} +1 -1
  6. package/lib/public/assets/{EmptyState-D5BTZSIa.js → EmptyState-BhQfPUga.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-CDBhSDsT.js → FieldGroup-VPsU3LwN.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-7JEhwNEM.js → FilterMenu-Br9o7dBS.js} +1 -1
  9. package/lib/public/assets/{Menu-DWl5uJ9E.js → Menu-dF_HlxcB.js} +1 -1
  10. package/lib/public/assets/{Modal-BZEfRpq8.js → Modal-51305CEc.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-B_EK9yKX.js → RecordingPage-BYQfiIEd.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-CptMr6IM.js → RecordingsPage-C7A2ZMU_.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-D7BBuUDP.js → SegmentedControl--_Zp_x-E.js} +1 -1
  14. package/lib/public/assets/{SettingCard-W3QrpCyh.js → SettingCard-DKlLEbu9.js} +1 -1
  15. package/lib/public/assets/{Table-BthtE6KF.js → Table-mknFnLyL.js} +1 -1
  16. package/lib/public/assets/{activity-CdoCkXe1.js → activity-D4657Amj.js} +1 -1
  17. package/lib/public/assets/{ai-settings-CkGIJyKN.js → ai-settings-_sXFJMDz.js} +1 -1
  18. package/lib/public/assets/{api-keys-2lHmq44B.js → api-keys-BV7AQW2N.js} +1 -1
  19. package/lib/public/assets/{apps-BCHpRvo_.js → apps-D-qPk58H.js} +1 -1
  20. package/lib/public/assets/{arrow-left-CpaUvzcV.js → arrow-left-B6xZI-lv.js} +1 -1
  21. package/lib/public/assets/{arrow-right-TCqk2-9N.js → arrow-right-KOusXQT_.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-B6JBqR-t.js → arrow-up-right--y5SjylA.js} +1 -1
  23. package/lib/public/assets/{auth-shell-BSyFjCa8.js → auth-shell-BOGfjiRi.js} +2 -2
  24. package/lib/public/assets/builds-page-CgobTBVN.js +11 -0
  25. package/lib/public/assets/{button-CB1ehl9z.js → button-Ci58y6Ou.js} +1 -1
  26. package/lib/public/assets/{calendar-DXZ0-sa6.js → calendar-BYLnN-JG.js} +1 -1
  27. package/lib/public/assets/{check-Bq0QWhtG.js → check-BZFRUKhB.js} +1 -1
  28. package/lib/public/assets/{chevron-left-BMI8J11V.js → chevron-left-C-olFL5E.js} +1 -1
  29. package/lib/public/assets/{chevron-right-tbezHlhW.js → chevron-right-CmxzLyEE.js} +1 -1
  30. package/lib/public/assets/{circle-check-BgkLodxU.js → circle-check-Duvlh9lf.js} +1 -1
  31. package/lib/public/assets/{circle-x-DuFD0enL.js → circle-x-DoQgpxKQ.js} +1 -1
  32. package/lib/public/assets/{clock-C7AhkXMm.js → clock-yvaEQMYo.js} +1 -1
  33. package/lib/public/assets/{copy-CiNhxJMO.js → copy-CIcmMH4s.js} +1 -1
  34. package/lib/public/assets/{device-explorer-BFWU04bT.js → device-explorer-Cf-d-udL.js} +1 -1
  35. package/lib/public/assets/{download-Di_q2qj5.js → download-Ck75G4Sr.js} +1 -1
  36. package/lib/public/assets/{forgot-password-gtciaIez.js → forgot-password-DcOi08XK.js} +1 -1
  37. package/lib/public/assets/{funnel-BY1R0ydM.js → funnel-CafvAmsz.js} +1 -1
  38. package/lib/public/assets/{index-B1iJx0w1.js → index-9Npot8Nw.js} +2 -2
  39. package/lib/public/assets/{index-Bgx65Mb_.js → index-QT33_ARg.js} +1 -1
  40. package/lib/public/assets/{input-B83lhHJ_.js → input-DUA3Tk33.js} +1 -1
  41. package/lib/public/assets/{keycodes-BjiWgQik.js → keycodes-Dj17elyp.js} +1 -1
  42. package/lib/public/assets/{layers-CxMkF86z.js → layers-5SyoCGaK.js} +1 -1
  43. package/lib/public/assets/{list-checks-2GnUUcTR.js → list-checks-Blsk6Sts.js} +1 -1
  44. package/lib/public/assets/{lock-BRKQ_6ix.js → lock-DUyg9vcf.js} +1 -1
  45. package/lib/public/assets/{login-Chmwsz_d.js → login-DWRTaaNU.js} +1 -1
  46. package/lib/public/assets/{maintenance-settings-DEb0LrBA.js → maintenance-settings-CwthiKtl.js} +1 -1
  47. package/lib/public/assets/{monitor-ClL-76V7.js → monitor-D7kOVM-I.js} +1 -1
  48. package/lib/public/assets/{mouse-pointer-2-C815rsZl.js → mouse-pointer-2-BzwITYs-.js} +1 -1
  49. package/lib/public/assets/{network-CBd_S-GU.js → network-3c26VFLV.js} +1 -1
  50. package/lib/public/assets/{overview-B0paJ7DP.js → overview-BamopQBx.js} +1 -1
  51. package/lib/public/assets/{page-header-D9FkEgQm.js → page-header-Cr568Bj2.js} +1 -1
  52. package/lib/public/assets/{plus-DF-cFokG.js → plus-egFVGGUW.js} +1 -1
  53. package/lib/public/assets/{profile-page-BrdnJBRA.js → profile-page-BetCnNKO.js} +1 -1
  54. package/lib/public/assets/{reset-password-DJFBWJnT.js → reset-password-BgjuXPN-.js} +1 -1
  55. package/lib/public/assets/{runbook-page-D5SIpqrf.js → runbook-page-BbAREoE4.js} +1 -1
  56. package/lib/public/assets/{select-Wc_B9XUe.js → select-BfZw_3u2.js} +1 -1
  57. package/lib/public/assets/{selector-detail-page-q7-lRo_l.js → selector-detail-page-BqswNrpr.js} +1 -1
  58. package/lib/public/assets/{selector-health-Qe-NusTk.js → selector-health-Cbdio_9h.js} +1 -1
  59. package/lib/public/assets/{selector-health-page-Dw2WUlDJ.js → selector-health-page-qdM7e6Lx.js} +1 -1
  60. package/lib/public/assets/{session-detail-page-bZpHxz1l.js → session-detail-page-DFnhbs82.js} +1 -1
  61. package/lib/public/assets/{settings-DA_CIMVB.js → settings-Bg81ksOI.js} +1 -1
  62. package/lib/public/assets/stat-tile-CCR1O7iE.js +8 -0
  63. package/lib/public/assets/{tablet-_Uwg1m0t.js → tablet-DAHOR-iq.js} +1 -1
  64. package/lib/public/assets/{teams-ZTGk8Wky.js → teams-Bz13DBCK.js} +1 -1
  65. package/lib/public/assets/{trash-2-BcWlaBK8.js → trash-2-BgqAVxUr.js} +1 -1
  66. package/lib/public/assets/{upload-47Lf5U7r.js → upload-Rvl2CCKn.js} +1 -1
  67. package/lib/public/assets/{use-password-reset-mode-B3cWCSu5.js → use-password-reset-mode--PRWElm2.js} +1 -1
  68. package/lib/public/assets/{users-DGHET-4G.js → users-BRkxjzNQ.js} +1 -1
  69. package/lib/public/assets/{users-IiOEsNnr.js → users-O123SAYx.js} +1 -1
  70. package/lib/public/assets/{video-off-D-jBA0at.js → video-off-DkNxxAWE.js} +1 -1
  71. package/lib/public/assets/{webhook-settings-HqV4V6_J.js → webhook-settings-D6UmShyI.js} +1 -1
  72. package/lib/public/assets/{zap-iAfVTWG0.js → zap-D6UAiMKL.js} +1 -1
  73. package/lib/public/index.html +1 -1
  74. package/lib/src/app/routers/dashboard.js +21 -11
  75. package/lib/src/app/routers/sessionInsights.js +46 -9
  76. package/lib/src/device-managers/ios/IOSLogStreamService.js +6 -3
  77. package/lib/src/device-managers/ios/IOSStreamService.js +38 -122
  78. package/lib/src/device-managers/ios/IOSTunnels.js +376 -0
  79. package/lib/src/device-managers/ios/WDAClient.js +5 -2
  80. package/lib/src/services/PortAllocator.js +65 -0
  81. package/lib/test/unit/ios-go-ios-tunnel-env.spec.js +154 -0
  82. package/lib/test/unit/ios-stream-appium-tunnel.spec.js +122 -32
  83. package/lib/test/unit/ios-stream-port-leases.spec.js +26 -5
  84. package/lib/test/unit/ios-tunnels.spec.js +387 -0
  85. package/lib/test/unit/port-allocator-pairs.spec.js +110 -0
  86. package/lib/test/unit/session-list-insights.spec.js +39 -0
  87. package/lib/test/unit/stream-own-device-row.spec.js +1 -1
  88. package/lib/tsconfig.tsbuildinfo +1 -1
  89. package/package.json +1 -1
  90. package/lib/public/assets/builds-page-D0RnbdGV.js +0 -11
  91. package/lib/public/assets/stat-tile-CxsAGcDU.js +0 -8
@@ -54,6 +54,7 @@ const tcp_port_used_1 = __importDefault(require("tcp-port-used"));
54
54
  const typedi_1 = require("typedi");
55
55
  const util_1 = require("util");
56
56
  const IOSStreamService_1 = __importDefault(require("../../src/device-managers/ios/IOSStreamService"));
57
+ const IOSTunnels_1 = require("../../src/device-managers/ios/IOSTunnels");
57
58
  const singleFlight_1 = require("../../src/helpers/singleFlight");
58
59
  const PortAllocator_1 = require("../../src/services/PortAllocator");
59
60
  const ProcessRegistry_1 = require("../../src/services/ProcessRegistry");
@@ -61,23 +62,26 @@ const device_store_1 = require("../../src/data-service/device-store");
61
62
  const deviceService = __importStar(require("../../src/data-service/device-service"));
62
63
  const control_1 = require("../../src/app/routers/control");
63
64
  /**
64
- * go-ios's tunnel process serves its tunnel-info API on 60105 and the first
65
- * phone's userspace tunnel on 60106. On iOS 17+ a WDA that go-ios launched
66
- * (`runwda`) reaches the phone through it, and an Appium session rides such a
67
- * WDA whenever it was allocated while a stream ran: iOSCapabilities points
68
- * `webDriverAgentUrl` at the stream's WDA.
65
+ * Each iOS 17+ phone has its own go-ios tunnel (IOSTunnels) on its own pair of
66
+ * leased ports. On iOS 17+ a WDA that go-ios launched (`runwda`) reaches the
67
+ * phone through it, and an Appium session rides such a WDA whenever it was
68
+ * allocated while a stream ran: iOSCapabilities points `webDriverAgentUrl` at
69
+ * the stream's WDA.
69
70
  *
70
- * The stream service's orphan sweep kill -9'd whatever listened on those
71
- * ports, and a viewer's stop killed the stream's own WDA and tunnel, with a
72
- * live Appium session on the phone either way.
71
+ * So no go-ios cleanup runs, and a viewer's stop stops nothing, while an
72
+ * Appium session holds the phone. And nothing is killed by port any more:
73
+ * through 2.7 the orphan sweep kill -9'd whatever listened on go-ios's default
74
+ * ports, 60105 and 60106, which was another iPhone's live tunnel.
73
75
  *
74
76
  * Nothing real runs here. Every `exec` goes to a fake `execFile` (exec calls
75
77
  * `module.exports.execFile`, promisified or not), `process.kill` is stubbed,
76
- * and processes are fakes.
78
+ * IOSTunnels is a fake, and processes are fakes.
77
79
  */
78
80
  const IPHONE = 'test-iphone-00008150-tunnel';
79
- const TUNNEL_PID = 424242;
80
- const AGENT_LISTENER_PID = '515151';
81
+ const OTHER_IPHONE = 'test-iphone-00008110-other';
82
+ const TUNNEL_PORT = 12100;
83
+ /** Another iPhone's tunnel, on go-ios's default ports, as `lsof` would name it. */
84
+ const DEFAULT_PORT_LISTENER = '515151';
81
85
  class FakeProcess extends events_1.EventEmitter {
82
86
  constructor(pid) {
83
87
  super();
@@ -111,24 +115,26 @@ describe('go-ios tunnels under a live Appium session (fake exec, fake processes)
111
115
  let commands;
112
116
  let kill;
113
117
  let allocator;
118
+ let tunnels;
114
119
  let svc;
115
120
  const killed = () => commands.filter((c) => /^kill\b|^pkill\b/.test(c));
116
121
  const swept = () => commands.filter((c) => /^lsof\b|^pgrep\b/.test(c));
122
+ const defaultPorts = () => commands.filter((c) => /6010[56]/.test(c));
117
123
  /** A stream that launched its own WDA, forwarders and go-ios tunnel. */
118
- function ownStream() {
124
+ function ownStream(udid = IPHONE) {
119
125
  const s = {
120
- udid: IPHONE,
126
+ udid,
121
127
  wdaProcess: new FakeProcess(),
122
128
  forwardWDAProcess: new FakeProcess(),
123
129
  forwardMJPEGProcess: new FakeProcess(),
124
- tunnelProcess: new FakeProcess(TUNNEL_PID),
130
+ tunnelPort: TUNNEL_PORT,
125
131
  wdaPort: 28101,
126
132
  mjpegPort: 29101,
127
133
  status: 'running',
128
134
  lastViewerAt: Date.now(),
129
135
  viewerCount: 0,
130
136
  };
131
- svc.sessions.set(IPHONE, s);
137
+ svc.sessions.set(udid, s);
132
138
  return s;
133
139
  }
134
140
  /** A stream attached to the WDA the Appium session forwards: only its MJPEG forwarder is its own. */
@@ -138,7 +144,7 @@ describe('go-ios tunnels under a live Appium session (fake exec, fake processes)
138
144
  wdaProcess: null,
139
145
  forwardWDAProcess: null,
140
146
  forwardMJPEGProcess: new FakeProcess(),
141
- tunnelProcess: null,
147
+ tunnelPort: null,
142
148
  wdaPort: 28123,
143
149
  mjpegPort: 29101,
144
150
  status: 'running',
@@ -158,11 +164,12 @@ describe('go-ios tunnels under a live Appium session (fake exec, fake processes)
158
164
  wdaLocalPort: 28123,
159
165
  };
160
166
  commands = [];
161
- // Refuse everything real. lsof names the go-ios agent's listener, pgrep a
162
- // tunnel for this udid; both exit 1 with nothing to report otherwise.
167
+ // Refuse everything real. lsof names a listener on go-ios's default ports
168
+ // (another iPhone's tunnel), which must never be killed; pgrep finds no
169
+ // tunnel for this udid. Both exit 1 with nothing to report otherwise.
163
170
  sinon_1.default.stub(child_process_1.default, 'execFile').callsFake(((cmd, _opts, cb) => {
164
171
  commands.push(cmd);
165
- const stdout = /^lsof -ti :6010[56]$/.test(cmd) ? `${AGENT_LISTENER_PID}\n` : '';
172
+ const stdout = /^lsof -ti :6010[56]$/.test(cmd) ? `${DEFAULT_PORT_LISTENER}\n` : '';
166
173
  const nothingFound = /^(lsof|pgrep)\b/.test(cmd) && !stdout;
167
174
  const done = typeof cb === 'function' ? cb : () => undefined;
168
175
  process.nextTick(() => nothingFound
@@ -183,6 +190,14 @@ describe('go-ios tunnels under a live Appium session (fake exec, fake processes)
183
190
  claim: sinon_1.default.stub().resolves(true),
184
191
  acquire: sinon_1.default.stub().resolves(29102),
185
192
  release: sinon_1.default.stub().resolves(),
193
+ touch: sinon_1.default.stub().resolves(),
194
+ releasePurpose: sinon_1.default.stub().resolves(),
195
+ };
196
+ tunnels = {
197
+ ensure: sinon_1.default.stub().resolves(null),
198
+ envFor: sinon_1.default.stub().callsFake(() => (Object.assign(Object.assign({}, process.env), { ENABLE_GO_IOS_AGENT: 'yes' }))),
199
+ stop: sinon_1.default.stub().resolves(),
200
+ touch: sinon_1.default.stub().resolves(),
186
201
  };
187
202
  svc = iosService();
188
203
  const real = typedi_1.Container.get.bind(typedi_1.Container);
@@ -191,6 +206,8 @@ describe('go-ios tunnels under a live Appium session (fake exec, fake processes)
191
206
  return svc;
192
207
  if (token === PortAllocator_1.PortAllocator)
193
208
  return allocator;
209
+ if (token === IOSTunnels_1.IOSTunnels)
210
+ return tunnels;
194
211
  if (token === ProcessRegistry_1.ProcessRegistry)
195
212
  return { track: () => undefined };
196
213
  return real(token);
@@ -209,32 +226,34 @@ describe('go-ios tunnels under a live Appium session (fake exec, fake processes)
209
226
  yield control_1.previewLeaveDeps.stop(IPHONE);
210
227
  (0, chai_1.expect)(stream.wdaProcess.killed, 'the WDA the session may be driving').to.equal(false);
211
228
  (0, chai_1.expect)(stream.forwardWDAProcess.killed, "that WDA's forwarder").to.equal(false);
212
- (0, chai_1.expect)(kill.called, 'no process group is signalled (the tunnel)').to.equal(false);
229
+ (0, chai_1.expect)(tunnels.stop.called, "the phone's tunnel").to.equal(false);
230
+ (0, chai_1.expect)(kill.called, 'no process group is signalled').to.equal(false);
213
231
  (0, chai_1.expect)(killed(), 'no kill is run').to.deep.equal([]);
214
232
  (0, chai_1.expect)(svc.getStreamStatus(IPHONE), 'the stream stays, for the session teardown').to.equal(stream);
215
233
  (0, chai_1.expect)(allocator.release.called, 'its ports stay leased').to.equal(false);
216
234
  }));
217
- it('sweeps no go-ios port when the stream is attached to the Appium session’s own WDA', () => __awaiter(void 0, void 0, void 0, function* () {
235
+ it("stops no tunnel for a stream attached to the Appium session's own WDA", () => __awaiter(void 0, void 0, void 0, function* () {
218
236
  const stream = attachedStream();
219
237
  yield control_1.previewLeaveDeps.stop(IPHONE);
220
238
  (0, chai_1.expect)(swept(), 'no lsof/pgrep for go-ios processes').to.deep.equal([]);
221
- (0, chai_1.expect)(killed(), 'no kill -9 of the go-ios listener').to.deep.equal([]);
239
+ (0, chai_1.expect)(killed()).to.deep.equal([]);
240
+ (0, chai_1.expect)(tunnels.stop.called, 'it started no tunnel').to.equal(false);
222
241
  (0, chai_1.expect)(kill.called).to.equal(false);
223
242
  (0, chai_1.expect)(stream.forwardMJPEGProcess.killed, 'its own MJPEG forwarder still goes').to.equal(true);
224
243
  (0, chai_1.expect)(svc.getStreamStatus(IPHONE)).to.equal(undefined);
225
244
  }));
226
- it('still stops everything and sweeps real orphans once no Appium session holds the phone', () => __awaiter(void 0, void 0, void 0, function* () {
245
+ it("stops the stream's tunnel and this phone's leftovers, and nothing by port, once no Appium session holds the phone", () => __awaiter(void 0, void 0, void 0, function* () {
227
246
  Object.assign(device, { busy: false, session_id: null });
228
247
  const stream = ownStream();
229
248
  yield control_1.previewLeaveDeps.stop(IPHONE);
230
249
  (0, chai_1.expect)(stream.wdaProcess.killed).to.equal(true);
231
250
  (0, chai_1.expect)(stream.forwardWDAProcess.killed).to.equal(true);
232
251
  (0, chai_1.expect)(stream.forwardMJPEGProcess.killed).to.equal(true);
233
- (0, chai_1.expect)(kill.calledWith(-TUNNEL_PID, 'SIGKILL'), "the stream's tunnel group").to.equal(true);
252
+ (0, chai_1.expect)(tunnels.stop.calledWith(IPHONE), "the stream's tunnel").to.equal(true);
253
+ (0, chai_1.expect)(tunnels.stop.alwaysCalledWith(IPHONE), 'and no other phone’s').to.equal(true);
234
254
  (0, chai_1.expect)(commands).to.include(`pgrep -f "ios tunnel.*${IPHONE}"`);
235
- (0, chai_1.expect)(commands).to.include('lsof -ti :60105');
236
- (0, chai_1.expect)(commands).to.include('lsof -ti :60106');
237
- (0, chai_1.expect)(killed()).to.include(`kill -9 ${AGENT_LISTENER_PID}`);
255
+ (0, chai_1.expect)(defaultPorts(), 'nothing on 60105/60106 is looked at').to.deep.equal([]);
256
+ (0, chai_1.expect)(killed(), "another iPhone's tunnel is never kill -9'd").to.deep.equal([]);
238
257
  (0, chai_1.expect)(svc.getStreamStatus(IPHONE)).to.equal(undefined);
239
258
  }));
240
259
  it('treats a live-preview hold as no Appium session', () => __awaiter(void 0, void 0, void 0, function* () {
@@ -242,7 +261,18 @@ describe('go-ios tunnels under a live Appium session (fake exec, fake processes)
242
261
  const stream = ownStream();
243
262
  yield control_1.previewLeaveDeps.stop(IPHONE);
244
263
  (0, chai_1.expect)(stream.wdaProcess.killed).to.equal(true);
245
- (0, chai_1.expect)(killed()).to.include(`kill -9 ${AGENT_LISTENER_PID}`);
264
+ (0, chai_1.expect)(tunnels.stop.calledWith(IPHONE)).to.equal(true);
265
+ }));
266
+ it("stopping one iPhone's stream leaves another iPhone's tunnel alone", () => __awaiter(void 0, void 0, void 0, function* () {
267
+ Object.assign(device, { busy: false, session_id: null });
268
+ ownStream(IPHONE);
269
+ const other = ownStream(OTHER_IPHONE);
270
+ yield svc.stopStream(IPHONE);
271
+ (0, chai_1.expect)(tunnels.stop.calledWith(OTHER_IPHONE)).to.equal(false);
272
+ (0, chai_1.expect)(other.wdaProcess.killed).to.equal(false);
273
+ (0, chai_1.expect)(svc.getStreamStatus(OTHER_IPHONE)).to.equal(other);
274
+ (0, chai_1.expect)(commands.some((c) => c.includes(OTHER_IPHONE))).to.equal(false);
275
+ (0, chai_1.expect)(defaultPorts()).to.deep.equal([]);
246
276
  }));
247
277
  });
248
278
  describe('a start that attaches to the WDA the Appium session forwards', () => {
@@ -253,7 +283,7 @@ describe('go-ios tunnels under a live Appium session (fake exec, fake processes)
253
283
  wdaProcess: null,
254
284
  forwardWDAProcess: null,
255
285
  forwardMJPEGProcess: null,
256
- tunnelProcess: null,
286
+ tunnelPort: null,
257
287
  wdaPort: 28101,
258
288
  mjpegPort: 29101,
259
289
  status: 'error',
@@ -266,8 +296,10 @@ describe('go-ios tunnels under a live Appium session (fake exec, fake processes)
266
296
  const { wdaPort } = yield svc.startStream(IPHONE);
267
297
  (0, chai_1.expect)(wdaPort, 'attached to the session’s WDA').to.equal(device.wdaLocalPort);
268
298
  (0, chai_1.expect)(swept(), 'no lsof/pgrep for go-ios processes').to.deep.equal([]);
269
- (0, chai_1.expect)(killed(), 'no kill -9 of the go-ios listener').to.deep.equal([]);
299
+ (0, chai_1.expect)(killed()).to.deep.equal([]);
270
300
  (0, chai_1.expect)(kill.called).to.equal(false);
301
+ (0, chai_1.expect)(tunnels.ensure.called, 'no tunnel of its own').to.equal(false);
302
+ (0, chai_1.expect)(tunnels.stop.called).to.equal(false);
271
303
  }));
272
304
  });
273
305
  describe('a start over a running stream whose WDA does not answer', () => {
@@ -284,7 +316,8 @@ describe('go-ios tunnels under a live Appium session (fake exec, fake processes)
284
316
  (0, chai_1.expect)(err === null || err === void 0 ? void 0 : err.message).to.match(/Appium session .* holds the device/);
285
317
  (0, chai_1.expect)(stream.wdaProcess.killed, 'the WDA the session may be driving').to.equal(false);
286
318
  (0, chai_1.expect)(stream.forwardWDAProcess.killed).to.equal(false);
287
- (0, chai_1.expect)(kill.called, 'no process group is signalled (the tunnel)').to.equal(false);
319
+ (0, chai_1.expect)(tunnels.stop.called, "the phone's tunnel").to.equal(false);
320
+ (0, chai_1.expect)(kill.called).to.equal(false);
288
321
  (0, chai_1.expect)(commands, 'nothing is exec’d').to.deep.equal([]);
289
322
  (0, chai_1.expect)((_a = svc.getStreamStatus(IPHONE)) === null || _a === void 0 ? void 0 : _a.status, 'the stream is left as it was').to.equal('running');
290
323
  (0, chai_1.expect)(svc.recoveryCooldowns.has(IPHONE), 'no failed start to cool down from').to.equal(false);
@@ -302,10 +335,66 @@ describe('go-ios tunnels under a live Appium session (fake exec, fake processes)
302
335
  const stream = ownStream();
303
336
  const err = yield svc.startStream(IPHONE).then(() => null, (e) => e);
304
337
  (0, chai_1.expect)(stream.wdaProcess.killed, 'the dead stream is stopped').to.equal(true);
305
- (0, chai_1.expect)(kill.calledWith(-TUNNEL_PID, 'SIGKILL')).to.equal(true);
338
+ (0, chai_1.expect)(tunnels.stop.calledWith(IPHONE), 'with its tunnel').to.equal(true);
306
339
  (0, chai_1.expect)(err === null || err === void 0 ? void 0 : err.message, 'and a new start is attempted').to.equal('go-ios not available');
307
340
  }));
308
341
  });
342
+ describe('a start after a failed start', () => {
343
+ it("stops the failed start's tunnel before starting again", () => __awaiter(void 0, void 0, void 0, function* () {
344
+ Object.assign(device, { busy: false, session_id: null });
345
+ // The failed start brought its tunnel up, then WDA never answered.
346
+ svc.sessions.set(IPHONE, {
347
+ udid: IPHONE,
348
+ wdaProcess: null,
349
+ forwardWDAProcess: null,
350
+ forwardMJPEGProcess: null,
351
+ tunnelPort: TUNNEL_PORT,
352
+ wdaPort: 28101,
353
+ mjpegPort: 29101,
354
+ status: 'error',
355
+ lastViewerAt: Date.now(),
356
+ viewerCount: 0,
357
+ });
358
+ svc.isGoIOSAvailable = () => __awaiter(void 0, void 0, void 0, function* () { return false; });
359
+ const err = yield svc.startStream(IPHONE).then(() => null, (e) => e);
360
+ (0, chai_1.expect)(err === null || err === void 0 ? void 0 : err.message).to.equal('go-ios not available');
361
+ (0, chai_1.expect)(tunnels.stop.calledWith(IPHONE), "the failed start's tunnel").to.equal(true);
362
+ (0, chai_1.expect)(tunnels.stop.alwaysCalledWith(IPHONE)).to.equal(true);
363
+ (0, chai_1.expect)(defaultPorts()).to.deep.equal([]);
364
+ }));
365
+ });
366
+ describe('boot', () => {
367
+ it('reaps every go-ios process, then drops every tunnel port lease', () => __awaiter(void 0, void 0, void 0, function* () {
368
+ yield svc.reapOrphanTunnels();
369
+ (0, chai_1.expect)(commands).to.include('pgrep -f "/nonexistent/go-ios"');
370
+ (0, chai_1.expect)(allocator.releasePurpose.calledOnceWithExactly('tunnel')).to.equal(true);
371
+ }));
372
+ });
373
+ describe('the hourly watchdog', () => {
374
+ it("keeps a running stream's tunnel ports leased", () => __awaiter(void 0, void 0, void 0, function* () {
375
+ const clock = sinon_1.default.useFakeTimers({ toFake: ['setInterval'] });
376
+ try {
377
+ sinon_1.default.stub(IOSStreamService_1.default.prototype, 'isStreamResponsive').resolves(true);
378
+ const watched = new IOSStreamService_1.default();
379
+ watched.sessions.set(IPHONE, {
380
+ udid: IPHONE,
381
+ status: 'running',
382
+ tunnelPort: TUNNEL_PORT,
383
+ wdaPort: 28101,
384
+ mjpegPort: 29101,
385
+ lastViewerAt: Date.now(),
386
+ viewerCount: 1,
387
+ });
388
+ yield clock.tickAsync(60 * 60 * 1000);
389
+ yield new Promise((resolve) => setImmediate(resolve)); // let the tick's awaits finish
390
+ (0, chai_1.expect)(allocator.touch.calledWith(28101), 'its WDA port, as before').to.equal(true);
391
+ (0, chai_1.expect)(tunnels.touch.calledWith(IPHONE), 'and its tunnel').to.equal(true);
392
+ }
393
+ finally {
394
+ clock.restore();
395
+ }
396
+ }));
397
+ });
309
398
  it("leaves go-ios processes alone when the phone's row can't be read", () => __awaiter(void 0, void 0, void 0, function* () {
310
399
  attachedStream();
311
400
  findDevice.rejects(new Error('database is locked'));
@@ -313,5 +402,6 @@ describe('go-ios tunnels under a live Appium session (fake exec, fake processes)
313
402
  (0, chai_1.expect)(swept()).to.deep.equal([]);
314
403
  (0, chai_1.expect)(killed()).to.deep.equal([]);
315
404
  (0, chai_1.expect)(kill.called).to.equal(false);
405
+ (0, chai_1.expect)(tunnels.stop.called).to.equal(false);
316
406
  }));
317
407
  });
@@ -23,6 +23,7 @@ const path_1 = __importDefault(require("path"));
23
23
  const sinon_1 = __importDefault(require("sinon"));
24
24
  const typedi_1 = require("typedi");
25
25
  const IOSStreamService_1 = __importDefault(require("../../src/device-managers/ios/IOSStreamService"));
26
+ const IOSTunnels_1 = require("../../src/device-managers/ios/IOSTunnels");
26
27
  const AndroidStreamService_1 = __importDefault(require("../../src/device-managers/android/AndroidStreamService"));
27
28
  const singleFlight_1 = require("../../src/helpers/singleFlight");
28
29
  const PortAllocator_1 = require("../../src/services/PortAllocator");
@@ -51,11 +52,12 @@ const scratch_port_leases_1 = require("../helpers/scratch-port-leases");
51
52
  const IPHONE = 'test-iphone-00008150';
52
53
  const S9 = 'test-android-s9';
53
54
  class FakeProcess extends events_1.EventEmitter {
54
- constructor(command, args, server) {
55
+ constructor(command, args, server, opts) {
55
56
  super();
56
57
  this.command = command;
57
58
  this.args = args;
58
59
  this.server = server;
60
+ this.opts = opts;
59
61
  this.pid = undefined;
60
62
  this.exitCode = null;
61
63
  this.stdout = new events_1.EventEmitter();
@@ -106,6 +108,7 @@ describe('iOS stream port leases (fake processes, real sockets, scratch DB)', ()
106
108
  let appiumWdaPorts;
107
109
  let ios;
108
110
  let android;
111
+ let tunnels;
109
112
  beforeEach(() => {
110
113
  allocator = new PortAllocator_1.PortAllocator();
111
114
  // iOS and Android share the mjpeg range, as they do on the lab.
@@ -120,11 +123,19 @@ describe('iOS stream port leases (fake processes, real sockets, scratch DB)', ()
120
123
  spawned = [];
121
124
  extraServers = [];
122
125
  appiumWdaPorts = new Set();
126
+ tunnels = {
127
+ ensure: sinon_1.default.stub().resolves(null),
128
+ envFor: sinon_1.default.stub().callsFake(() => (Object.assign(Object.assign({}, process.env), { ENABLE_GO_IOS_AGENT: 'yes' }))),
129
+ stop: sinon_1.default.stub().resolves(),
130
+ touch: sinon_1.default.stub().resolves(),
131
+ };
123
132
  sinon_1.default.stub(process, 'kill');
124
133
  const real = typedi_1.Container.get.bind(typedi_1.Container);
125
134
  sinon_1.default.stub(typedi_1.Container, 'get').callsFake((token) => {
126
135
  if (token === PortAllocator_1.PortAllocator)
127
136
  return allocator;
137
+ if (token === IOSTunnels_1.IOSTunnels)
138
+ return tunnels;
128
139
  if (token === ProcessRegistry_1.ProcessRegistry)
129
140
  return { track: () => undefined };
130
141
  return real(token);
@@ -133,12 +144,12 @@ describe('iOS stream port leases (fake processes, real sockets, scratch DB)', ()
133
144
  findDevice: (_a) => __awaiter(void 0, [_a], void 0, function* ({ udid }) { return devices.get(udid); }),
134
145
  updateDevice: (udid, _host, data) => __awaiter(void 0, void 0, void 0, function* () { return Object.assign(devices.get(udid), data); }),
135
146
  });
136
- sinon_1.default.stub(child_process_1.default, 'spawn').callsFake(((command, args) => {
147
+ sinon_1.default.stub(child_process_1.default, 'spawn').callsFake(((command, args, opts) => {
137
148
  let server;
138
149
  if (command === 'iproxy') {
139
150
  server = listenNow(Number(args[args.length - 1].split(':')[0]));
140
151
  }
141
- const proc = new FakeProcess(command, args, server);
152
+ const proc = new FakeProcess(command, args, server, opts);
142
153
  spawned.push(proc);
143
154
  return proc;
144
155
  }));
@@ -157,7 +168,6 @@ describe('iOS stream port leases (fake processes, real sockets, scratch DB)', ()
157
168
  }));
158
169
  sinon_1.default.stub(ios, 'cleanupOrphanTunnels').resolves();
159
170
  sinon_1.default.stub(ios, 'killStaleProcesses').resolves();
160
- sinon_1.default.stub(ios, 'ensureTunnel').resolves(null);
161
171
  sinon_1.default.stub(ios, 'detectWDABundleId').resolves('com.test.WebDriverAgentRunner.xctrunner');
162
172
  sinon_1.default.stub(ios, 'updateWDASettings').resolves();
163
173
  sinon_1.default.stub(ios, 'createWDASession').resolves(null);
@@ -215,6 +225,17 @@ describe('iOS stream port leases (fake processes, real sockets, scratch DB)', ()
215
225
  (0, chai_1.expect)(mjpeg === null || mjpeg === void 0 ? void 0 : mjpeg.leasedToUdid, `mjpeg port ${mjpegPort} must stay leased to the iPhone`).to.equal(IPHONE);
216
226
  (0, chai_1.expect)(mjpeg === null || mjpeg === void 0 ? void 0 : mjpeg.purpose).to.equal('mjpeg');
217
227
  }));
228
+ it("runs the iPhone's WDA through the iPhone's own go-ios tunnel", () => __awaiter(void 0, void 0, void 0, function* () {
229
+ var _a, _b, _c;
230
+ tunnels.ensure.resolves(12100);
231
+ tunnels.envFor.callsFake((udid) => (Object.assign(Object.assign(Object.assign({}, process.env), { ENABLE_GO_IOS_AGENT: 'yes' }), (udid === IPHONE ? { GO_IOS_AGENT_PORT: '12100' } : {}))));
232
+ sinon_1.default.stub(ios, 'isGoIOSAvailable').resolves(true);
233
+ yield ios.startStream(IPHONE);
234
+ (0, chai_1.expect)(tunnels.ensure.calledWith(IPHONE)).to.equal(true);
235
+ (0, chai_1.expect)((_a = ios.getStreamStatus(IPHONE)) === null || _a === void 0 ? void 0 : _a.tunnelPort).to.equal(12100);
236
+ const runwda = spawned.find((p) => p.command === ios.goIOSPath && p.args[0] === 'runwda');
237
+ (0, chai_1.expect)((_c = (_b = runwda === null || runwda === void 0 ? void 0 : runwda.opts) === null || _b === void 0 ? void 0 : _b.env) === null || _c === void 0 ? void 0 : _c.GO_IOS_AGENT_PORT, "runwda finds this phone's tunnel").to.equal('12100');
238
+ }));
218
239
  it('stopping a stale iOS session never deletes a lease another device holds', () => __awaiter(void 0, void 0, void 0, function* () {
219
240
  var _a;
220
241
  // The S9+ has since been leased the port the stale iPhone session names.
@@ -233,7 +254,7 @@ describe('iOS stream port leases (fake processes, real sockets, scratch DB)', ()
233
254
  wdaProcess: null,
234
255
  forwardWDAProcess: null,
235
256
  forwardMJPEGProcess: null,
236
- tunnelProcess: null,
257
+ tunnelPort: null,
237
258
  wdaPort: scratch.base + 50,
238
259
  mjpegPort: scratch.base,
239
260
  status: 'error',