@xenon-device-management/xenon 2.8.2 → 2.8.3

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 (82) hide show
  1. package/lib/package.json +1 -1
  2. package/lib/public/assets/{AnnotationOverlay-BqBip3BK.js → AnnotationOverlay-DZrFxYPZ.js} +1 -1
  3. package/lib/public/assets/{ApiKeyGate-DUvekoqc.js → ApiKeyGate-DkyW0B7G.js} +1 -1
  4. package/lib/public/assets/{BugReportButton-BG0CrInp.js → BugReportButton-TyYrNWYM.js} +1 -1
  5. package/lib/public/assets/{DeviceMosaicView-B0SnOzKM.js → DeviceMosaicView-DY__bWHx.js} +2 -2
  6. package/lib/public/assets/{EmptyState-Dq-Bq8TT.js → EmptyState-CfkN-nUG.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-BIgXTGgC.js → FieldGroup-DQu6Zo15.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-CeJOFd6_.js → FilterMenu-BT_Q7-4t.js} +1 -1
  9. package/lib/public/assets/{Menu-B4H6_Xk9.js → Menu-DfwEC8pG.js} +1 -1
  10. package/lib/public/assets/{Modal-DJG1w3KG.js → Modal-DXyaClqh.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-CuzbuRlh.js → RecordingPage-j7H4_iT1.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-CW2IMjO4.js → RecordingsPage-ST7B99o1.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-DhQ3uYdc.js → SegmentedControl-DaqYH9BW.js} +1 -1
  14. package/lib/public/assets/{SettingCard-C5Gmj98i.js → SettingCard-CWTz_r_8.js} +1 -1
  15. package/lib/public/assets/{Table-D-WtrnfJ.js → Table-BDO5qlOf.js} +1 -1
  16. package/lib/public/assets/{activity-CvT65tfF.js → activity-CGdR3Ff6.js} +1 -1
  17. package/lib/public/assets/{ai-settings-DuCpE_dA.js → ai-settings-C8I0KSUg.js} +1 -1
  18. package/lib/public/assets/{api-keys-DEIUB9Bz.js → api-keys-BhbZQpGV.js} +1 -1
  19. package/lib/public/assets/{apps-B0z6NLY9.js → apps-DaBl9nMx.js} +1 -1
  20. package/lib/public/assets/{arrow-left-CCUPd12W.js → arrow-left-WRIrign1.js} +1 -1
  21. package/lib/public/assets/{arrow-right-CrLsc2tN.js → arrow-right-BuIztKwL.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-CiQmHVQo.js → arrow-up-right-Cz072oos.js} +1 -1
  23. package/lib/public/assets/{auth-shell-CHgiSQ9B.js → auth-shell-BS884uL2.js} +2 -2
  24. package/lib/public/assets/{builds-page-BvxRnAet.js → builds-page-B05iZMmN.js} +1 -1
  25. package/lib/public/assets/{button-pgE9Zb7y.js → button-DjeIU-Ty.js} +1 -1
  26. package/lib/public/assets/{calendar-CNJBqo5s.js → calendar-DUF9u2PF.js} +1 -1
  27. package/lib/public/assets/{check-BOAfxviA.js → check-DUR92DEE.js} +1 -1
  28. package/lib/public/assets/{chevron-left-DAYay1pY.js → chevron-left-CRvlIuad.js} +1 -1
  29. package/lib/public/assets/{chevron-right-B0FDOdzU.js → chevron-right-iRDZ5v7C.js} +1 -1
  30. package/lib/public/assets/{circle-check-DOIAC8XC.js → circle-check-CqDvbZtW.js} +1 -1
  31. package/lib/public/assets/{circle-x-z8FaSlcA.js → circle-x-CsYtRLC8.js} +1 -1
  32. package/lib/public/assets/{clock-BtaZSeEx.js → clock-DB9ihtTE.js} +1 -1
  33. package/lib/public/assets/{copy-D60KmcdV.js → copy-BufiyibK.js} +1 -1
  34. package/lib/public/assets/{device-explorer-Cph43t8H.js → device-explorer-BapnPGYt.js} +1 -1
  35. package/lib/public/assets/{download-s-xM4AGY.js → download-D14AYHRg.js} +1 -1
  36. package/lib/public/assets/{forgot-password-CKw6RVHP.js → forgot-password-2PqpH7ph.js} +1 -1
  37. package/lib/public/assets/{funnel-BvgEGRdQ.js → funnel-CgV-CZex.js} +1 -1
  38. package/lib/public/assets/{index-BB9sO7Nt.js → index-BE8OC-Wj.js} +1 -1
  39. package/lib/public/assets/{index-BpIf5nE2.js → index-CXxjosDd.js} +2 -2
  40. package/lib/public/assets/{input-CPS4hChq.js → input-DPn63eF4.js} +1 -1
  41. package/lib/public/assets/{keycodes-D5ZobFRW.js → keycodes-BYgvJDTE.js} +1 -1
  42. package/lib/public/assets/{layers-CrAsrOG4.js → layers-DJZQUOez.js} +1 -1
  43. package/lib/public/assets/{list-checks-BnEjyKfv.js → list-checks-YM2_VNMv.js} +1 -1
  44. package/lib/public/assets/{lock-Dm0hfbuM.js → lock-Cy-A3o47.js} +1 -1
  45. package/lib/public/assets/{login-DdlbwQwy.js → login-CRZ2J7Up.js} +1 -1
  46. package/lib/public/assets/{maintenance-settings-kocnslUP.js → maintenance-settings-4HkUk10V.js} +1 -1
  47. package/lib/public/assets/{monitor-CGU-u9zq.js → monitor-Cyvro78e.js} +1 -1
  48. package/lib/public/assets/{mouse-pointer-2-CWu4a7Ez.js → mouse-pointer-2-26YTYUOD.js} +1 -1
  49. package/lib/public/assets/{network-TV7kV1jl.js → network-D-W1VWM_.js} +1 -1
  50. package/lib/public/assets/{overview-C3pWhCtz.js → overview-pVmHrKhk.js} +1 -1
  51. package/lib/public/assets/{page-header-DCNc9cYT.js → page-header-DsTokPlH.js} +1 -1
  52. package/lib/public/assets/{plus-C6Uuyu5Z.js → plus-OjIdQkG_.js} +1 -1
  53. package/lib/public/assets/{profile-page-CjUENuCw.js → profile-page-D0jy_p5L.js} +1 -1
  54. package/lib/public/assets/{reset-password-rpbIm1Go.js → reset-password-DptmkgSN.js} +1 -1
  55. package/lib/public/assets/{runbook-page-BGkOmIkY.js → runbook-page-9QcpCPUo.js} +1 -1
  56. package/lib/public/assets/{select-6Q8XgEfq.js → select-CHKzkHre.js} +1 -1
  57. package/lib/public/assets/{selector-detail-page-BPIr14Yp.js → selector-detail-page-P5zDXMZC.js} +1 -1
  58. package/lib/public/assets/{selector-health-DhZ490H5.js → selector-health-Buz4eRW1.js} +1 -1
  59. package/lib/public/assets/{selector-health-page-Vycvp0ED.js → selector-health-page-BHZfbxGg.js} +1 -1
  60. package/lib/public/assets/{session-detail-page-D2rvJIgL.js → session-detail-page-JHU-GKSX.js} +1 -1
  61. package/lib/public/assets/{settings-bF2S7k19.js → settings-CRTRggHW.js} +1 -1
  62. package/lib/public/assets/stat-tile-Be2twadZ.js +8 -0
  63. package/lib/public/assets/{tablet-1Gmx6FV4.js → tablet-C7QkOkHs.js} +1 -1
  64. package/lib/public/assets/{teams-DHBNN_a7.js → teams-CwVhnDoL.js} +1 -1
  65. package/lib/public/assets/{trash-2-D3idPlqv.js → trash-2-yjlULOrf.js} +1 -1
  66. package/lib/public/assets/{upload-HOW_42FA.js → upload-BcrYG7kK.js} +1 -1
  67. package/lib/public/assets/{use-password-reset-mode-CVPBPGSb.js → use-password-reset-mode-BhRH2TTL.js} +1 -1
  68. package/lib/public/assets/{users-BnKiArxK.js → users-BSZL_J7x.js} +1 -1
  69. package/lib/public/assets/{users-DjGgZFe_.js → users-D3jRTB5K.js} +1 -1
  70. package/lib/public/assets/{video-off-CuipXFJK.js → video-off-BwbiJtmA.js} +1 -1
  71. package/lib/public/assets/{webhook-settings-7_s2Zky-.js → webhook-settings-CTIdd7DN.js} +1 -1
  72. package/lib/public/assets/{zap-Gl0D1sD4.js → zap-CZK3Cbup.js} +1 -1
  73. package/lib/public/index.html +1 -1
  74. package/lib/src/app/routers/dashboard.js +6 -0
  75. package/lib/src/app/routers/sessionInsights.js +23 -1
  76. package/lib/src/device-managers/ios/IOSStreamService.js +8 -0
  77. package/lib/src/device-managers/ios/IOSTunnels.js +44 -4
  78. package/lib/test/unit/ios-stream-port-leases.spec.js +26 -0
  79. package/lib/test/unit/ios-tunnels.spec.js +24 -0
  80. package/lib/test/unit/session-list-insights.spec.js +57 -0
  81. package/package.json +1 -1
  82. package/lib/public/assets/stat-tile-DOrnbGTr.js +0 -8
@@ -129,7 +129,13 @@ function getSessions(request, response) {
129
129
  const cursor = readQuery(response, () => (0, sessionInsights_1.cursorWhere)(request.query.before, request.query.beforeId));
130
130
  if (cursor === undefined)
131
131
  return;
132
+ const ids = readQuery(response, () => (0, sessionInsights_1.parseIds)(request.query.ids));
133
+ if (ids === undefined)
134
+ return;
132
135
  const where = {};
136
+ if (ids) {
137
+ where.id = { in: ids };
138
+ }
133
139
  if (since) {
134
140
  where.createdAt = { gte: since };
135
141
  }
@@ -9,10 +9,11 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.SESSION_PAGE_MAX = exports.SESSION_PAGE_DEFAULT = exports.InvalidQueryError = void 0;
12
+ exports.SESSION_IDS_MAX = exports.SESSION_PAGE_MAX = exports.SESSION_PAGE_DEFAULT = exports.InvalidQueryError = void 0;
13
13
  exports.sessionOutcome = sessionOutcome;
14
14
  exports.parseSince = parseSince;
15
15
  exports.parseLimit = parseLimit;
16
+ exports.parseIds = parseIds;
16
17
  exports.cursorWhere = cursorWhere;
17
18
  exports.describeSessions = describeSessions;
18
19
  exports.percentile = percentile;
@@ -77,6 +78,27 @@ function parseLimit(raw) {
77
78
  }
78
79
  return Math.min(n, exports.SESSION_PAGE_MAX);
79
80
  }
81
+ /** The most ids one GET /session takes. */
82
+ exports.SESSION_IDS_MAX = 200;
83
+ /**
84
+ * The `ids` query parameter: null when absent; else the comma-separated
85
+ * session ids, 1 to SESSION_IDS_MAX of them, or InvalidQueryError. The
86
+ * Sessions page re-reads older rows still running by id.
87
+ */
88
+ function parseIds(raw) {
89
+ if (raw === undefined)
90
+ return null;
91
+ const ids = typeof raw === 'string'
92
+ ? raw
93
+ .split(',')
94
+ .map((id) => id.trim())
95
+ .filter(Boolean)
96
+ : [];
97
+ if (ids.length === 0 || ids.length > exports.SESSION_IDS_MAX) {
98
+ throw new InvalidQueryError('invalid_ids', `ids must list 1 to ${exports.SESSION_IDS_MAX} session ids, separated by commas`);
99
+ }
100
+ return ids;
101
+ }
80
102
  /**
81
103
  * Where the next page starts: after the last row of the page before, given
82
104
  * as its `createdAt` (`before`) and `id` (`beforeId`). Pages go newest first
@@ -853,6 +853,14 @@ let IOSStreamService = class IOSStreamService {
853
853
  session.status = 'error';
854
854
  session.lastError = error.message;
855
855
  }
856
+ // A start that got its tunnel up and then failed (WDA didn't launch)
857
+ // stops that tunnel, so it doesn't run on, holding its ports, until
858
+ // the phone's next start. Not while an Appium session may use it.
859
+ if ((session === null || session === void 0 ? void 0 : session.tunnelPort) != null &&
860
+ !(yield this.appiumSessionMayUse(udid, "the failed start's go-ios tunnel"))) {
861
+ yield this.tunnels().stop(udid);
862
+ session.tunnelPort = null;
863
+ }
856
864
  // Back off before the next attempt so a polling client can't drive a
857
865
  // restart loop against a device that keeps failing to start.
858
866
  this.markRecoveryAttempt(udid);
@@ -20,6 +20,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
21
  exports.IOSTunnels = exports.TUNNEL_CHECK_MS = exports.TUNNEL_LEASE_TTL_MS = exports.TUNNEL_READY_POLL_MS = exports.TUNNEL_READY_TIMEOUT_MS = void 0;
22
22
  exports.iosVersionOf = iosVersionOf;
23
+ exports.goIosReason = goIosReason;
23
24
  /**
24
25
  * One go-ios tunnel per iOS 17+ phone, each on its own leased pair of ports.
25
26
  *
@@ -74,6 +75,40 @@ function iosVersionOf(info) {
74
75
  const match = versionStr.match(/(\d+\.?\d*)/);
75
76
  return match ? parseFloat(match[0]) : 0;
76
77
  }
78
+ /** The longest go-ios reason kept for an error message. */
79
+ const REASON_MAX = 200;
80
+ /**
81
+ * The last error or warning in a chunk of go-ios's stderr, for an error
82
+ * message. go-ios logs JSON lines: an ERROR or WARN line gives its `msg` and
83
+ * `error`, an INFO line gives nothing, and a line that isn't JSON (a panic)
84
+ * is kept as it is.
85
+ */
86
+ function goIosReason(text) {
87
+ var _a;
88
+ let reason;
89
+ for (const raw of text.split('\n')) {
90
+ const line = raw.trim();
91
+ if (!line)
92
+ continue;
93
+ let entry;
94
+ try {
95
+ entry = JSON.parse(line);
96
+ }
97
+ catch (_b) {
98
+ entry = undefined;
99
+ }
100
+ if (entry && typeof entry === 'object') {
101
+ const level = String((_a = entry.level) !== null && _a !== void 0 ? _a : '').toUpperCase();
102
+ if ((level === 'ERROR' || level === 'WARN') && entry.msg) {
103
+ reason = entry.error ? `${entry.msg}: ${entry.error}` : String(entry.msg);
104
+ }
105
+ }
106
+ else {
107
+ reason = line;
108
+ }
109
+ }
110
+ return reason === null || reason === void 0 ? void 0 : reason.slice(0, REASON_MAX);
111
+ }
77
112
  /** An answer that puts the phone's traffic anywhere but its leased P + 1. */
78
113
  function moved(info, port) {
79
114
  return info.state === 'up' && info.userspacePort !== undefined && info.userspacePort !== port + 1;
@@ -144,14 +179,15 @@ let IOSTunnels = class IOSTunnels {
144
179
  this.log.warn(`[${udid}] go-ios tunnel failed: ${err.message}`);
145
180
  ended();
146
181
  });
147
- this.logOutput(udid, proc);
182
+ this.logOutput(udid, tunnel);
148
183
  this.log.info(`[${udid}] iOS ${version}: starting a go-ios tunnel on ${port} (traffic on ${port + 1})`);
149
184
  const polls = exports.TUNNEL_READY_TIMEOUT_MS / exports.TUNNEL_READY_POLL_MS;
150
185
  for (let poll = 0; poll <= polls; poll++) {
151
186
  if (poll > 0)
152
187
  yield this.sleep(exports.TUNNEL_READY_POLL_MS);
153
188
  if (this.tunnels.get(udid) !== tunnel) {
154
- throw new Error(`The go-ios tunnel for ${udid} exited before it was ready (exit code ${proc.exitCode})`);
189
+ throw new Error(`The go-ios tunnel for ${udid} exited before it was ready (exit code ${proc.exitCode})` +
190
+ (tunnel.reason ? `: ${tunnel.reason}` : ''));
155
191
  }
156
192
  const info = yield this.tunnelInfo(port, udid);
157
193
  if (moved(info, port)) {
@@ -278,13 +314,16 @@ let IOSTunnels = class IOSTunnels {
278
314
  }
279
315
  /**
280
316
  * go-ios's output, at debug. Its repeated warning about a connected
281
- * pre-iOS 17 phone (which needs no tunnel) is logged once per phone.
317
+ * pre-iOS 17 phone (which needs no tunnel) is logged once per phone. Its
318
+ * last error or warning is kept, for the error if the tunnel fails.
282
319
  */
283
- logOutput(udid, proc) {
320
+ logOutput(udid, tunnel) {
284
321
  var _a, _b;
322
+ const proc = tunnel.process;
285
323
  (_a = proc.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (data) => this.log.debug(`Tunnel [${udid}]: ${data}`));
286
324
  const loggedUnsupported = new Set();
287
325
  (_b = proc.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (data) => {
326
+ var _a;
288
327
  const text = String(data);
289
328
  const { unsupported, udid: targetUdid } = (0, iosStreamDiagnostics_1.classifyTunnelStderr)(text);
290
329
  if (unsupported) {
@@ -295,6 +334,7 @@ let IOSTunnels = class IOSTunnels {
295
334
  }
296
335
  return;
297
336
  }
337
+ tunnel.reason = (_a = goIosReason(text)) !== null && _a !== void 0 ? _a : tunnel.reason;
298
338
  this.log.debug(`Tunnel Err [${udid}]: ${text}`);
299
339
  });
300
340
  }
@@ -108,6 +108,8 @@ describe('iOS stream port leases (fake processes, real sockets, scratch DB)', ()
108
108
  let appiumWdaPorts;
109
109
  let ios;
110
110
  let android;
111
+ /** The stream's runwda exits at once, as when WebDriverAgent fails to launch. */
112
+ let runwdaExits;
111
113
  let tunnels;
112
114
  beforeEach(() => {
113
115
  allocator = new PortAllocator_1.PortAllocator();
@@ -123,6 +125,7 @@ describe('iOS stream port leases (fake processes, real sockets, scratch DB)', ()
123
125
  spawned = [];
124
126
  extraServers = [];
125
127
  appiumWdaPorts = new Set();
128
+ runwdaExits = false;
126
129
  tunnels = {
127
130
  ensure: sinon_1.default.stub().resolves(null),
128
131
  envFor: sinon_1.default.stub().callsFake(() => (Object.assign(Object.assign({}, process.env), { ENABLE_GO_IOS_AGENT: 'yes' }))),
@@ -150,6 +153,8 @@ describe('iOS stream port leases (fake processes, real sockets, scratch DB)', ()
150
153
  server = listenNow(Number(args[args.length - 1].split(':')[0]));
151
154
  }
152
155
  const proc = new FakeProcess(command, args, server, opts);
156
+ if (runwdaExits && args[0] === 'runwda')
157
+ proc.exitCode = 1;
153
158
  spawned.push(proc);
154
159
  return proc;
155
160
  }));
@@ -236,6 +241,27 @@ describe('iOS stream port leases (fake processes, real sockets, scratch DB)', ()
236
241
  const runwda = spawned.find((p) => p.command === ios.goIOSPath && p.args[0] === 'runwda');
237
242
  (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
243
  }));
244
+ describe("a start whose WebDriverAgent fails after the iPhone's tunnel came up", () => {
245
+ beforeEach(() => {
246
+ tunnels.ensure.resolves(12100);
247
+ runwdaExits = true;
248
+ sinon_1.default.stub(ios, 'isGoIOSAvailable').resolves(true);
249
+ });
250
+ it('stops that tunnel, so it neither lingers nor holds its ports', () => __awaiter(void 0, void 0, void 0, function* () {
251
+ var _a, _b;
252
+ const err = yield ios.startStream(IPHONE).then(() => null, (e) => e);
253
+ (0, chai_1.expect)(err === null || err === void 0 ? void 0 : err.message).to.match(/WDA process exited/);
254
+ (0, chai_1.expect)(tunnels.stop.calledWith(IPHONE)).to.equal(true);
255
+ (0, chai_1.expect)((_a = ios.getStreamStatus(IPHONE)) === null || _a === void 0 ? void 0 : _a.status).to.equal('error');
256
+ (0, chai_1.expect)((_b = ios.getStreamStatus(IPHONE)) === null || _b === void 0 ? void 0 : _b.tunnelPort).to.equal(null);
257
+ }));
258
+ it('leaves it running while an Appium session holds the iPhone', () => __awaiter(void 0, void 0, void 0, function* () {
259
+ Object.assign(devices.get(IPHONE), { busy: true, session_id: '7f0c5a52-appium-session' });
260
+ const err = yield ios.startStream(IPHONE).then(() => null, (e) => e);
261
+ (0, chai_1.expect)(err === null || err === void 0 ? void 0 : err.message).to.match(/WDA process exited/);
262
+ (0, chai_1.expect)(tunnels.stop.called).to.equal(false);
263
+ }));
264
+ });
239
265
  it('stopping a stale iOS session never deletes a lease another device holds', () => __awaiter(void 0, void 0, void 0, function* () {
240
266
  var _a;
241
267
  // The S9+ has since been leased the port the stale iPhone session names.
@@ -181,6 +181,18 @@ describe('IOSTunnels: a go-ios tunnel per iPhone', () => {
181
181
  (0, chai_1.expect)(t.portFor(PHONE_A)).to.equal(undefined);
182
182
  (0, chai_1.expect)([...t.ports.leased.keys()]).to.deep.equal([]);
183
183
  }));
184
+ it("says why the tunnel exited before it was ready, in go-ios's words", () => __awaiter(void 0, void 0, void 0, function* () {
185
+ t.answers = () => {
186
+ const proc = t.spawned[0].proc;
187
+ proc.stderr.emit('data', Buffer.from('{"level":"INFO","msg":"Using userspace networking"}\n' +
188
+ '{"level":"ERROR","msg":"failed to start tunnel","error":"listen tcp 127.0.0.1:12101: bind: address already in use"}\n'));
189
+ proc.exit(1);
190
+ return false;
191
+ };
192
+ const err = yield t.ensure(PHONE_A).then(() => null, (e) => e);
193
+ (0, chai_1.expect)(err === null || err === void 0 ? void 0 : err.message).to.equal(`The go-ios tunnel for ${PHONE_A} exited before it was ready (exit code 1): ` +
194
+ 'failed to start tunnel: listen tcp 127.0.0.1:12101: bind: address already in use');
195
+ }));
184
196
  it('fails the start when the tunnel comes up on another traffic port, and stops it', () => __awaiter(void 0, void 0, void 0, function* () {
185
197
  t.answers = () => ({ state: 'up', userspacePort: 12102 });
186
198
  const err = yield t.ensure(PHONE_A).then(() => null, (e) => e);
@@ -320,6 +332,18 @@ describe('IOSTunnels: a phone that goes away (go-ios keeps its agent running)',
320
332
  }
321
333
  }));
322
334
  });
335
+ describe('goIosReason', () => {
336
+ it("reads go-ios's last error or warning", () => {
337
+ (0, chai_1.expect)((0, IOSTunnels_1.goIosReason)('{"level":"INFO","msg":"Tunnel server started"}\n')).to.equal(undefined);
338
+ (0, chai_1.expect)((0, IOSTunnels_1.goIosReason)('{"level":"WARN","msg":"failed to get tunnel info","udid":"X"}')).to.equal('failed to get tunnel info');
339
+ (0, chai_1.expect)((0, IOSTunnels_1.goIosReason)('{"level":"ERROR","msg":"first"}\n{"level":"ERROR","msg":"failed to start tunnel","error":"EOF"}\n')).to.equal('failed to start tunnel: EOF');
340
+ });
341
+ it('keeps a line that is not JSON, trimmed and at most 200 characters', () => {
342
+ (0, chai_1.expect)((0, IOSTunnels_1.goIosReason)(' panic: runtime error \n')).to.equal('panic: runtime error');
343
+ (0, chai_1.expect)((0, IOSTunnels_1.goIosReason)('x'.repeat(500))).to.have.length(200);
344
+ (0, chai_1.expect)((0, IOSTunnels_1.goIosReason)('\n \n')).to.equal(undefined);
345
+ });
346
+ });
323
347
  describe('iosVersionOf', () => {
324
348
  it('reads the version ios info reports', () => {
325
349
  (0, chai_1.expect)((0, IOSTunnels_1.iosVersionOf)({ ProductVersion: '17.2.1' })).to.equal(17.2);
@@ -111,6 +111,63 @@ describe('Sessions page data', () => {
111
111
  (0, chai_1.expect)(ids(res)).to.deep.equal(['a', 'c', 'b', 'd']);
112
112
  }));
113
113
  });
114
+ // The Sessions page re-reads, by id, the rows of its older pages that were
115
+ // still running when loaded. Those pages aren't polled, so a session that
116
+ // had finished kept showing "running" there.
117
+ describe('by id', () => {
118
+ const ids = (res) => res.body.map((r) => r.id);
119
+ beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
120
+ // A shared-pool phone, so a member may see these sessions too.
121
+ yield scratch.db.device.create({
122
+ data: {
123
+ udid: 'phone-1',
124
+ host: 'http://127.0.0.1:4723',
125
+ nodeId: 'hub-1',
126
+ platform: 'android',
127
+ },
128
+ });
129
+ yield session({ id: 'a', createdAt: ago(1 * MIN) });
130
+ yield session({ id: 'b', createdAt: ago(2 * MIN), status: 'running' });
131
+ yield session({ id: 'c', createdAt: ago(3 * MIN) });
132
+ }));
133
+ it('answers just the sessions asked for, newest first', () => __awaiter(void 0, void 0, void 0, function* () {
134
+ const res = yield (0, loopbackRequest_1.default)(app()).get('/session?ids=c,b');
135
+ (0, chai_1.expect)(res.status).to.equal(200);
136
+ (0, chai_1.expect)(ids(res)).to.deep.equal(['b', 'c']);
137
+ }));
138
+ it('leaves out an id it does not know', () => __awaiter(void 0, void 0, void 0, function* () {
139
+ (0, chai_1.expect)(ids(yield (0, loopbackRequest_1.default)(app()).get('/session?ids=b,nope'))).to.deep.equal(['b']);
140
+ }));
141
+ it('refuses an empty list, or more than 200 ids', () => __awaiter(void 0, void 0, void 0, function* () {
142
+ const many = Array.from({ length: 201 }, (_, i) => `x${i}`).join(',');
143
+ for (const q of ['ids=', 'ids=,', `ids=${many}`]) {
144
+ const res = yield (0, loopbackRequest_1.default)(app()).get(`/session?${q}`);
145
+ (0, chai_1.expect)(res.status, q.slice(0, 12)).to.equal(400);
146
+ (0, chai_1.expect)(res.body.error, q.slice(0, 12)).to.equal('invalid_ids');
147
+ }
148
+ }));
149
+ it('answers only the sessions the caller may see', () => __awaiter(void 0, void 0, void 0, function* () {
150
+ const team = yield scratch.db.team.create({ data: { name: 'by-id-team' } });
151
+ yield scratch.db.device.create({
152
+ data: {
153
+ udid: 'team-b',
154
+ host: 'http://127.0.0.1:4723',
155
+ nodeId: 'hub-1',
156
+ platform: 'android',
157
+ teamId: team.id,
158
+ },
159
+ });
160
+ yield session({ id: 'hidden', device_udid: 'team-b', status: 'running' });
161
+ caller = {
162
+ kind: 'user-session',
163
+ userId: 'member-1',
164
+ role: 'MEMBER',
165
+ scopes: 'sessions',
166
+ teamIds: [],
167
+ };
168
+ (0, chai_1.expect)(ids(yield (0, loopbackRequest_1.default)(app()).get('/session?ids=b,hidden'))).to.deep.equal(['b']);
169
+ }));
170
+ });
114
171
  it('keeps the sessions created since the given time, newest first', () => __awaiter(void 0, void 0, void 0, function* () {
115
172
  yield session({ id: 'old', createdAt: ago(3 * HOUR) });
116
173
  yield session({ id: 'recent', createdAt: ago(30 * MIN) });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xenon-device-management/xenon",
3
- "version": "2.8.2",
3
+ "version": "2.8.3",
4
4
  "description": "Xenon - Intelligent Mobile Infrastructure. A self-healing device orchestration platform for Appium.",
5
5
  "main": "./lib/src/index.js",
6
6
  "exports": {
@@ -1,8 +0,0 @@
1
- import{c as U,r as i,p as X,X as M,j as $}from"./index-BpIf5nE2.js";import{f as Y}from"./time-B8FIgwPE.js";/**
2
- * @license lucide-react v0.555.0 - ISC
3
- *
4
- * This source code is licensed under the ISC license.
5
- * See the LICENSE file in the root directory of this source tree.
6
- */const G=[["path",{d:"M10.1 2.182a10 10 0 0 1 3.8 0",key:"5ilxe3"}],["path",{d:"M13.9 21.818a10 10 0 0 1-3.8 0",key:"11zvb9"}],["path",{d:"M17.609 3.721a10 10 0 0 1 2.69 2.7",key:"1iw5b2"}],["path",{d:"M2.182 13.9a10 10 0 0 1 0-3.8",key:"c0bmvh"}],["path",{d:"M20.279 17.609a10 10 0 0 1-2.7 2.69",key:"1ruxm7"}],["path",{d:"M21.818 10.1a10 10 0 0 1 0 3.8",key:"qkgqxc"}],["path",{d:"M3.721 6.391a10 10 0 0 1 2.7-2.69",key:"1mcia2"}],["path",{d:"M6.391 20.279a10 10 0 0 1-2.69-2.7",key:"1fvljs"}]],ue=U("circle-dashed",G);function E(e){switch(e){case"success":case"passed":case"ended":return"passed";case"failed":case"error":case"timeout":return"failed";case"running":return"running";default:return"other"}}function ce(e){const t={all:e.length,passed:0,failed:0,running:0};for(const n of e){const r=E(n.status);r!=="other"&&(t[r]+=1)}return t}function V(e,t){return t==="all"?!0:E(e.status)===t}function le(e,t,n){const r=n.trim().toLowerCase();return e.filter(s=>{var c,f;return!(!V(s,t)||r&&![s.id,R(s).text,s.name,s.failure_reason,s.device_name,s.device_platform,s.device_version,s.node_id,s.ranOn,(c=s.owner)==null?void 0:c.name,(f=s.owner)==null?void 0:f.email].filter(Boolean).join(" ").toLowerCase().includes(r))})}function Z(e){var n;const t=(n=e.device_name)==null?void 0:n.trim();return t&&t.length>0?t:"Unknown Device"}function K(e){const t=e.device_platform??"";if(!t)return"—";const n=t.toLowerCase();return n==="ios"?"iOS":n==="tvos"?"tvOS":n.charAt(0).toUpperCase()+n.slice(1)}function fe(e){return Y(e)}function de(e){if(!e.startTime)return null;const t=Date.parse(e.startTime);if(!Number.isFinite(t))return null;const n=e.endTime?Date.parse(e.endTime):Date.now();return Number.isFinite(n)?Math.max(0,n-t):null}function me(e){if(e==null||!Number.isFinite(e)||e<0)return"—";const t=e/1e3,n=Math.floor(t/3600),r=Math.floor(t%3600/60),c=(t-n*3600-r*60).toFixed(1);return n>0?`${n}h ${r}m ${c}s`:r>0?`${r}m ${c}s`:`${c}s`}const O=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],D=e=>String(e).padStart(2,"0");function x(e,t){const n=e[`appium:${t}`]??e[t];return typeof n=="string"&&n.trim()?n.trim():null}function Q(e){if(!e)return null;let t;try{t=JSON.parse(e)}catch{return null}if(!t||typeof t!="object")return null;let n=t;if(n.capabilities&&typeof n.capabilities=="object"&&(n=n.capabilities),n.alwaysMatch||n.firstMatch){const r=Array.isArray(n.firstMatch)?n.firstMatch[0]:void 0;return{...n.alwaysMatch||{},...r||{}}}return n}function B(e){const t=Q(e);if(!t)return null;const n=x(t,"appPackage")??x(t,"bundleId");if(n)return n;const r=x(t,"app");if(r){const s=r.split(/[?#]/)[0].split(/[\\/]/).pop()??"";if(s&&s!=="download")return s}return x(t,"browserName")}function R(e){var r;const t=(r=e.name)==null?void 0:r.trim();if(t)return{text:t,source:"name"};const n=B(e.session_capabilities)??B(e.desired_capabilities);return n?{text:n,source:"app"}:{text:`Session ${e.id.slice(0,8)}`,source:"id"}}function k(e){return`${O[e.getMonth()]} ${e.getDate()}, ${D(e.getHours())}:${D(e.getMinutes())}`}function pe(e){const t=e?new Date(e):null;return t&&!Number.isNaN(t.getTime())?k(t):"—"}function W(e){var n;const t=(n=e.name)==null?void 0:n.trim();return!t||t==="Default Build"}function he(e){if(!W(e))return e.name.trim();const t=new Date(e.createdAt);return Number.isNaN(t.getTime())?"Build":`Build · ${k(t)}`}const ge={all:"All time","24h":"Last 24 hours","7d":"Last 7 days","30d":"Last 30 days"},ee={"24h":864e5,"7d":7*864e5,"30d":30*864e5};function te(e,t){return e==="all"?null:new Date(t-ee[e]).toISOString()}function Se(e){if(e==null||!Number.isFinite(e)||e<0)return"—";if(e<1e3)return"<1s";const t=Math.floor(e/1e3),n=Math.floor(t/3600),r=Math.floor(t%3600/60),s=t%60;return n>0?`${n}h ${r}m`:r>0?`${r}m ${s}s`:`${s}s`}function be(e,t=new Date){if(!e)return"—";const n=new Date(e);if(Number.isNaN(n.getTime()))return"—";const r=`${D(n.getHours())}:${D(n.getMinutes())}`;return n.getFullYear()!==t.getFullYear()?`${O[n.getMonth()]} ${n.getDate()}, ${n.getFullYear()} ${r}`:n.getMonth()===t.getMonth()&&n.getDate()===t.getDate()?r:k(n)}function C(e){const t=e.passed+e.failed;return t>0?e.passed/t*100:null}function ve(e,t){const n=C(e),r=t?C(t):null;return n===null||r===null?null:n-r}function ye(e){return e?e==="here"?"This server":e:null}function Me(e,t,n){var d,h;const r=n&&n.size>0?t.filter(o=>n.has(o.id)):t,s=r.filter(o=>E(o.status)==="failed").reverse();if(s.length===0)return{count:0,text:""};const c=n&&n.size>0?`${r.length} selected session${r.length===1?"":"s"}`:`${r.length} session${r.length===1?"":"s"}`,f=[`Failed tests in ${e}: ${s.length} of ${c}`,""];for(const o of s){const w=((h=(d=o.failure_reason)==null?void 0:d.split(`
7
- `)[0])==null?void 0:h.trim())||"No reason recorded",g=[K(o),o.device_version].filter(m=>m&&m!=="—").join(" ");f.push(`- ${R(o).text}`,` ${w}`,` ${[Z(o),g,`session ${o.id}`].filter(Boolean).join(" · ")}`)}return{count:s.length,text:f.join(`
8
- `)}}const ne=3e3,N=200,re=2e3,se=(e,t)=>e.createdAt===t.createdAt?e.id<t.id?1:-1:e.createdAt<t.createdAt?1:-1;function j(e,t){const n=new Map;for(const r of t)n.set(r.id,r);for(const r of e)n.set(r.id,r);return Array.from(n.values()).sort(se)}function $e(e={}){const{withSessions:t=!1,timeFilter:n="all"}=e,[r,s]=i.useState([]),[c,f]=i.useState([]),[d,h]=i.useState([]),o=i.useRef([]),[w,g]=i.useState(!1),[m,L]=i.useState(!1),[S,P]=i.useState(null),[H,q]=i.useState(!0),[J,A]=i.useState(null),l=i.useRef(!0),{on:b}=X(),v=i.useCallback(()=>S?{buildId:S}:{since:te(n,Date.now())??void 0},[S,n]);i.useEffect(()=>{o.current=[],h([]),g(!1)},[S,n]);const I=i.useCallback(async()=>{if(!t)return[];const a=o.current;if(a.length===0){const T=await M.getSessions({...v(),limit:N});return Array.isArray(T)&&g(T.length===N),T}const{since:u,..._}=v(),F=a[0].createdAt;return await M.getSessions({..._,since:u&&u>F?u:F,limit:re})},[t,v]),p=i.useCallback(async()=>{try{const[a,u]=await Promise.all([M.getBuilds(),I()]);if(!l.current)return;s(Array.isArray(a)?a:[]),f(Array.isArray(u)?u:[]),A(null)}catch(a){if(!l.current)return;A(a instanceof Error?a.message:String(a))}finally{l.current&&q(!1)}},[I]);i.useEffect(()=>{l.current=!0,p();const a=setInterval(p,ne);return()=>{l.current=!1,clearInterval(a)}},[p]),i.useEffect(()=>{const a=()=>p(),u=[b("session_started",a),b("session_stopped",a),b("device_added",a),b("device_removed",a)];return()=>{for(const _ of u)_()}},[b,p]);const y=i.useMemo(()=>j(c,d),[c,d]),z=i.useCallback(async()=>{const a=y[y.length-1];if(!(!t||m||!a)){L(!0);try{const u=await M.getSessions({...v(),before:a.createdAt,beforeId:a.id,limit:N});if(!l.current||!Array.isArray(u))return;o.current=j([],[...o.current,...u]),h(o.current),g(u.length===N)}catch(u){l.current&&A(u instanceof Error?u.message:String(u))}finally{l.current&&L(!1)}}},[y,t,m,v]);return{builds:r,selectedBuildId:S,sessions:y,loading:H,error:J,selectBuild:P,refresh:p,hasMore:w,loadingMore:m,showMore:z}}const ae={neutral:"text-[var(--text-muted)]",good:"text-[var(--color-success)]",bad:"text-[var(--color-danger)]"},xe=({label:e,value:t,valueCls:n="text-[var(--text)]",note:r,noteTone:s="neutral"})=>$.jsxs("div",{className:"min-w-0 rounded-lg border border-[var(--border)] bg-[var(--surface)] px-4 py-3",children:[$.jsx("div",{className:"text-xs text-[var(--text-muted)]",children:e}),$.jsx("div",{className:`mt-1 text-2xl font-semibold tabular-nums leading-tight truncate ${n}`,children:t}),$.jsx("div",{className:`mt-0.5 text-[11px] truncate ${ae[s]}`,children:r??" "})]});export{ue as C,ee as P,N as S,ge as T,ce as a,he as b,E as c,R as d,Z as e,pe as f,be as g,fe as h,W as i,Se as j,de as k,le as l,xe as m,C as n,ve as o,K as p,Me as q,ye as r,te as s,me as t,$e as u};