@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.
- package/lib/package.json +1 -1
- package/lib/public/assets/{AnnotationOverlay-BqBip3BK.js → AnnotationOverlay-DZrFxYPZ.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-DUvekoqc.js → ApiKeyGate-DkyW0B7G.js} +1 -1
- package/lib/public/assets/{BugReportButton-BG0CrInp.js → BugReportButton-TyYrNWYM.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-B0SnOzKM.js → DeviceMosaicView-DY__bWHx.js} +2 -2
- package/lib/public/assets/{EmptyState-Dq-Bq8TT.js → EmptyState-CfkN-nUG.js} +1 -1
- package/lib/public/assets/{FieldGroup-BIgXTGgC.js → FieldGroup-DQu6Zo15.js} +1 -1
- package/lib/public/assets/{FilterMenu-CeJOFd6_.js → FilterMenu-BT_Q7-4t.js} +1 -1
- package/lib/public/assets/{Menu-B4H6_Xk9.js → Menu-DfwEC8pG.js} +1 -1
- package/lib/public/assets/{Modal-DJG1w3KG.js → Modal-DXyaClqh.js} +1 -1
- package/lib/public/assets/{RecordingPage-CuzbuRlh.js → RecordingPage-j7H4_iT1.js} +1 -1
- package/lib/public/assets/{RecordingsPage-CW2IMjO4.js → RecordingsPage-ST7B99o1.js} +1 -1
- package/lib/public/assets/{SegmentedControl-DhQ3uYdc.js → SegmentedControl-DaqYH9BW.js} +1 -1
- package/lib/public/assets/{SettingCard-C5Gmj98i.js → SettingCard-CWTz_r_8.js} +1 -1
- package/lib/public/assets/{Table-D-WtrnfJ.js → Table-BDO5qlOf.js} +1 -1
- package/lib/public/assets/{activity-CvT65tfF.js → activity-CGdR3Ff6.js} +1 -1
- package/lib/public/assets/{ai-settings-DuCpE_dA.js → ai-settings-C8I0KSUg.js} +1 -1
- package/lib/public/assets/{api-keys-DEIUB9Bz.js → api-keys-BhbZQpGV.js} +1 -1
- package/lib/public/assets/{apps-B0z6NLY9.js → apps-DaBl9nMx.js} +1 -1
- package/lib/public/assets/{arrow-left-CCUPd12W.js → arrow-left-WRIrign1.js} +1 -1
- package/lib/public/assets/{arrow-right-CrLsc2tN.js → arrow-right-BuIztKwL.js} +1 -1
- package/lib/public/assets/{arrow-up-right-CiQmHVQo.js → arrow-up-right-Cz072oos.js} +1 -1
- package/lib/public/assets/{auth-shell-CHgiSQ9B.js → auth-shell-BS884uL2.js} +2 -2
- package/lib/public/assets/{builds-page-BvxRnAet.js → builds-page-B05iZMmN.js} +1 -1
- package/lib/public/assets/{button-pgE9Zb7y.js → button-DjeIU-Ty.js} +1 -1
- package/lib/public/assets/{calendar-CNJBqo5s.js → calendar-DUF9u2PF.js} +1 -1
- package/lib/public/assets/{check-BOAfxviA.js → check-DUR92DEE.js} +1 -1
- package/lib/public/assets/{chevron-left-DAYay1pY.js → chevron-left-CRvlIuad.js} +1 -1
- package/lib/public/assets/{chevron-right-B0FDOdzU.js → chevron-right-iRDZ5v7C.js} +1 -1
- package/lib/public/assets/{circle-check-DOIAC8XC.js → circle-check-CqDvbZtW.js} +1 -1
- package/lib/public/assets/{circle-x-z8FaSlcA.js → circle-x-CsYtRLC8.js} +1 -1
- package/lib/public/assets/{clock-BtaZSeEx.js → clock-DB9ihtTE.js} +1 -1
- package/lib/public/assets/{copy-D60KmcdV.js → copy-BufiyibK.js} +1 -1
- package/lib/public/assets/{device-explorer-Cph43t8H.js → device-explorer-BapnPGYt.js} +1 -1
- package/lib/public/assets/{download-s-xM4AGY.js → download-D14AYHRg.js} +1 -1
- package/lib/public/assets/{forgot-password-CKw6RVHP.js → forgot-password-2PqpH7ph.js} +1 -1
- package/lib/public/assets/{funnel-BvgEGRdQ.js → funnel-CgV-CZex.js} +1 -1
- package/lib/public/assets/{index-BB9sO7Nt.js → index-BE8OC-Wj.js} +1 -1
- package/lib/public/assets/{index-BpIf5nE2.js → index-CXxjosDd.js} +2 -2
- package/lib/public/assets/{input-CPS4hChq.js → input-DPn63eF4.js} +1 -1
- package/lib/public/assets/{keycodes-D5ZobFRW.js → keycodes-BYgvJDTE.js} +1 -1
- package/lib/public/assets/{layers-CrAsrOG4.js → layers-DJZQUOez.js} +1 -1
- package/lib/public/assets/{list-checks-BnEjyKfv.js → list-checks-YM2_VNMv.js} +1 -1
- package/lib/public/assets/{lock-Dm0hfbuM.js → lock-Cy-A3o47.js} +1 -1
- package/lib/public/assets/{login-DdlbwQwy.js → login-CRZ2J7Up.js} +1 -1
- package/lib/public/assets/{maintenance-settings-kocnslUP.js → maintenance-settings-4HkUk10V.js} +1 -1
- package/lib/public/assets/{monitor-CGU-u9zq.js → monitor-Cyvro78e.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CWu4a7Ez.js → mouse-pointer-2-26YTYUOD.js} +1 -1
- package/lib/public/assets/{network-TV7kV1jl.js → network-D-W1VWM_.js} +1 -1
- package/lib/public/assets/{overview-C3pWhCtz.js → overview-pVmHrKhk.js} +1 -1
- package/lib/public/assets/{page-header-DCNc9cYT.js → page-header-DsTokPlH.js} +1 -1
- package/lib/public/assets/{plus-C6Uuyu5Z.js → plus-OjIdQkG_.js} +1 -1
- package/lib/public/assets/{profile-page-CjUENuCw.js → profile-page-D0jy_p5L.js} +1 -1
- package/lib/public/assets/{reset-password-rpbIm1Go.js → reset-password-DptmkgSN.js} +1 -1
- package/lib/public/assets/{runbook-page-BGkOmIkY.js → runbook-page-9QcpCPUo.js} +1 -1
- package/lib/public/assets/{select-6Q8XgEfq.js → select-CHKzkHre.js} +1 -1
- package/lib/public/assets/{selector-detail-page-BPIr14Yp.js → selector-detail-page-P5zDXMZC.js} +1 -1
- package/lib/public/assets/{selector-health-DhZ490H5.js → selector-health-Buz4eRW1.js} +1 -1
- package/lib/public/assets/{selector-health-page-Vycvp0ED.js → selector-health-page-BHZfbxGg.js} +1 -1
- package/lib/public/assets/{session-detail-page-D2rvJIgL.js → session-detail-page-JHU-GKSX.js} +1 -1
- package/lib/public/assets/{settings-bF2S7k19.js → settings-CRTRggHW.js} +1 -1
- package/lib/public/assets/stat-tile-Be2twadZ.js +8 -0
- package/lib/public/assets/{tablet-1Gmx6FV4.js → tablet-C7QkOkHs.js} +1 -1
- package/lib/public/assets/{teams-DHBNN_a7.js → teams-CwVhnDoL.js} +1 -1
- package/lib/public/assets/{trash-2-D3idPlqv.js → trash-2-yjlULOrf.js} +1 -1
- package/lib/public/assets/{upload-HOW_42FA.js → upload-BcrYG7kK.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-CVPBPGSb.js → use-password-reset-mode-BhRH2TTL.js} +1 -1
- package/lib/public/assets/{users-BnKiArxK.js → users-BSZL_J7x.js} +1 -1
- package/lib/public/assets/{users-DjGgZFe_.js → users-D3jRTB5K.js} +1 -1
- package/lib/public/assets/{video-off-CuipXFJK.js → video-off-BwbiJtmA.js} +1 -1
- package/lib/public/assets/{webhook-settings-7_s2Zky-.js → webhook-settings-CTIdd7DN.js} +1 -1
- package/lib/public/assets/{zap-Gl0D1sD4.js → zap-CZK3Cbup.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/src/app/routers/dashboard.js +6 -0
- package/lib/src/app/routers/sessionInsights.js +23 -1
- package/lib/src/device-managers/ios/IOSStreamService.js +8 -0
- package/lib/src/device-managers/ios/IOSTunnels.js +44 -4
- package/lib/test/unit/ios-stream-port-leases.spec.js +26 -0
- package/lib/test/unit/ios-tunnels.spec.js +24 -0
- package/lib/test/unit/session-list-insights.spec.js +57 -0
- package/package.json +1 -1
- 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,
|
|
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,
|
|
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,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};
|