@xenon-device-management/xenon 2.15.0 → 2.17.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.
- package/README.md +38 -26
- package/lib/package.json +2 -1
- package/lib/public/assets/{AnnotationOverlay-Btq3dZRI.js → AnnotationOverlay-CjmMCS0S.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-C9OoAgg_.js → ApiKeyGate-B4Ob0v3C.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-DBC_xczB.js → DeviceMosaicView-DTVOB_NO.js} +1 -1
- package/lib/public/assets/{EmptyState-DqWU2-vM.js → EmptyState-DcEG-oAi.js} +1 -1
- package/lib/public/assets/{FieldGroup-CveQ3xrA.js → FieldGroup-CtnCDshE.js} +1 -1
- package/lib/public/assets/{FilterMenu-BfKDSfHz.js → FilterMenu-BK0dE9cc.js} +1 -1
- package/lib/public/assets/{Menu-CtdCybzA.js → Menu-eKsmLpKS.js} +1 -1
- package/lib/public/assets/{Modal-Bsvj1tm2.js → Modal-DPwop3Lz.js} +1 -1
- package/lib/public/assets/{RecordingPage-CRofOiyx.js → RecordingPage-CfgSkZkB.js} +1 -1
- package/lib/public/assets/{RecordingsPage-jNQHb2IR.js → RecordingsPage-Bzl9pTUY.js} +1 -1
- package/lib/public/assets/{SegmentedControl-C8HtTha-.js → SegmentedControl-B_TZTrPm.js} +1 -1
- package/lib/public/assets/{SettingCard-D8K1LZNo.js → SettingCard-DlmvZJPM.js} +1 -1
- package/lib/public/assets/{Table-zNTX92Pn.js → Table-BigPWKXi.js} +1 -1
- package/lib/public/assets/{activity-CJ8LwZq9.js → activity-CvuZdQYu.js} +1 -1
- package/lib/public/assets/{ai-settings-CgxQZqrc.js → ai-settings-lI2UJ2n7.js} +1 -1
- package/lib/public/assets/{api-keys-ECpo-zTo.js → api-keys-CPgf9XJM.js} +1 -1
- package/lib/public/assets/{apps-BjGymyf3.js → apps-DIzOvtUp.js} +1 -1
- package/lib/public/assets/{arrow-left-BWfCbeHP.js → arrow-left-DiP5ywBT.js} +1 -1
- package/lib/public/assets/{arrow-right-DC7UGf_n.js → arrow-right-DtW_8hm5.js} +1 -1
- package/lib/public/assets/{arrow-up-right-BOjwybDk.js → arrow-up-right-C89FbNU0.js} +1 -1
- package/lib/public/assets/{auth-shell-CxMTo9aH.js → auth-shell-B7sUDb--.js} +2 -2
- package/lib/public/assets/{builds-page-DOCm4HkA.js → builds-page-BBhvrMfA.js} +1 -1
- package/lib/public/assets/{button-Cg08-EMT.js → button-DugsdUw1.js} +1 -1
- package/lib/public/assets/{calendar-CA1ikLJs.js → calendar-grKd08ZH.js} +1 -1
- package/lib/public/assets/{check-BMTaH72W.js → check-ORjyNfnX.js} +1 -1
- package/lib/public/assets/{chevron-right-B2QOv9Nx.js → chevron-right-Ei2FxUXq.js} +1 -1
- package/lib/public/assets/{circle-check-CYXBoUqT.js → circle-check-Day0HjW4.js} +1 -1
- package/lib/public/assets/{circle-x-DTwnSAnl.js → circle-x-CHxSRLHp.js} +1 -1
- package/lib/public/assets/{clock-C8_HViQP.js → clock-BDHewWTx.js} +1 -1
- package/lib/public/assets/{copy-Be7LNMXV.js → copy-CTwT4I3D.js} +1 -1
- package/lib/public/assets/{device-explorer-BOqtfGXz.js → device-explorer-DlhrKJ-g.js} +1 -1
- package/lib/public/assets/{download-B56lwxWP.js → download-D3bjz_vr.js} +1 -1
- package/lib/public/assets/{forgot-password-CCU6bzLn.js → forgot-password-BNBLi023.js} +1 -1
- package/lib/public/assets/{h264Stream-C-OtmIuk.js → h264Stream-C51Au2i_.js} +1 -1
- package/lib/public/assets/index-Dk5J6HB1.js +219 -0
- package/lib/public/assets/{index-B9KQ6LsY.js → index-DncSz5LG.js} +1 -1
- package/lib/public/assets/{index-CfxsUslp.js → index-Z7_BY3pJ.js} +1 -1
- package/lib/public/assets/{input-CZHFjbSg.js → input-bTXM4l09.js} +1 -1
- package/lib/public/assets/{line-chart-Gl_Dz3X_.js → line-chart-B3YmVYyr.js} +1 -1
- package/lib/public/assets/{list-checks-CCgjeyGY.js → list-checks-BClKEj1-.js} +1 -1
- package/lib/public/assets/{lock-CkOwgDcx.js → lock-CZsenX3D.js} +1 -1
- package/lib/public/assets/{login-D5cc4w7M.js → login-B6A6tOp9.js} +1 -1
- package/lib/public/assets/{maintenance-settings-t7qr3fvX.js → maintenance-settings-CmCJGy7O.js} +1 -1
- package/lib/public/assets/{monitor-DEJixKQO.js → monitor-BdTNEglU.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CtOrk4M8.js → mouse-pointer-2-BM8CaLtt.js} +1 -1
- package/lib/public/assets/{network-Dii9qdvk.js → network-BS6vUc1x.js} +1 -1
- package/lib/public/assets/{overview-rolAACrO.js → overview-Yfk_Y7g3.js} +1 -1
- package/lib/public/assets/{page-header-COQZxJfR.js → page-header-DhXCwpml.js} +1 -1
- package/lib/public/assets/{play-DrN1Se_R.js → play-D08gP1td.js} +1 -1
- package/lib/public/assets/{plus-5aq-9uRx.js → plus-BKboorfa.js} +1 -1
- package/lib/public/assets/{profile-page-6i5kY6kF.js → profile-page-Cd9NodrA.js} +1 -1
- package/lib/public/assets/{reset-password-sp2IBrc7.js → reset-password-acLDyyDW.js} +1 -1
- package/lib/public/assets/{runbook-page-DJDjeopU.js → runbook-page-CWLkC1Fd.js} +153 -102
- package/lib/public/assets/{select-Bpyn7rWO.js → select-DZoRKNti.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BH74vmKv.js → selector-detail-redirect-DS4qrt84.js} +1 -1
- package/lib/public/assets/{selector-health-page-Cw_g218H.js → selector-health-page-TpmN61G3.js} +1 -1
- package/lib/public/assets/session-detail-page-C0wYaXqy.js +4 -0
- package/lib/public/assets/{settings-C3uyGhyc.js → settings-CxPGEY2j.js} +1 -1
- package/lib/public/assets/{stat-tile-xVqAKIlI.js → stat-tile-DUpyYgl-.js} +1 -1
- package/lib/public/assets/{tablet-BxUScAeO.js → tablet-SA02BJNQ.js} +1 -1
- package/lib/public/assets/{teams-B5ljGkY2.js → teams-jamD0Kxr.js} +1 -1
- package/lib/public/assets/{trash-2-B701i8sy.js → trash-2-DO8fv6w4.js} +1 -1
- package/lib/public/assets/{upload-C2gr44RS.js → upload-CaiGCStb.js} +1 -1
- package/lib/public/assets/{use-builds-data-NeQOFEoy.js → use-builds-data-jS-fcvVf.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DEzJq5ef.js → use-password-reset-mode-BzoenegM.js} +1 -1
- package/lib/public/assets/{users-MY6eNe6s.js → users-Bnw7Vs_B.js} +1 -1
- package/lib/public/assets/{users-D_vv_yz4.js → users-DHvDrriG.js} +1 -1
- package/lib/public/assets/{video-off-OK1lNZQn.js → video-off-FEvy0w5U.js} +1 -1
- package/lib/public/assets/{webhook-settings-BU-C9lBf.js → webhook-settings-CBVB5lxi.js} +1 -1
- package/lib/public/assets/{zap-DO9ry1iz.js → zap-BMrTJO6M.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +11 -7
- package/lib/src/InternalHttpClient.js +27 -5
- package/lib/src/XenonCapabilityManager.js +10 -3
- package/lib/src/app/index.js +2 -0
- package/lib/src/app/openapi/grid.yaml +102 -0
- package/lib/src/app/openapi/sessions.yaml +7 -5
- package/lib/src/app/routers/auth.js +2 -1
- package/lib/src/app/routers/users.js +2 -1
- package/lib/src/app/ws/nodeSocketRelay.js +77 -11
- package/lib/src/dashboard/commandLogFields.js +42 -0
- package/lib/src/dashboard/event-manager.js +23 -154
- package/lib/src/dashboard/services/failure-analysis-service.js +8 -86
- package/lib/src/dashboard/services/failureCategories.js +264 -0
- package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
- package/lib/src/device-managers/IOSDeviceManager.js +10 -0
- package/lib/src/device-utils.js +25 -8
- package/lib/src/gateway/forwardToNode.js +4 -11
- package/lib/src/gateway/hubSessionToken.js +27 -4
- package/lib/src/gateway/nodeAsk.js +57 -0
- package/lib/src/gateway/nodeSessionDeviceLogs.js +35 -0
- package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
- package/lib/src/helpers/outboundProxy.js +219 -0
- package/lib/src/index.js +21 -0
- package/lib/src/interceptors/CommandInterceptor.js +13 -15
- package/lib/src/interceptors/xenonScripts.js +28 -7
- package/lib/src/logger.js +3 -1
- package/lib/src/plugin.js +12 -4
- package/lib/src/scripts/database-check.js +3 -3
- package/lib/src/scripts/initialize-database.js +29 -33
- package/lib/src/scripts/run-migrations.js +320 -25
- package/lib/src/services/ApiKeyService.js +16 -5
- package/lib/src/services/InterceptorService.js +6 -2
- package/lib/src/services/OrphanSweeper.js +6 -0
- package/lib/src/services/SelectorStateService.js +11 -7
- package/lib/src/services/SelectorVerificationJob.js +6 -2
- package/lib/src/services/SessionLifecycleService.js +52 -23
- package/lib/src/services/SocketClient.js +11 -0
- package/lib/src/services/SocketServer.js +361 -42
- package/lib/src/services/TeamService.js +5 -1
- package/lib/src/services/TracingService.js +171 -60
- package/lib/src/services/UserService.js +14 -3
- package/lib/src/services/UserSessionService.js +16 -4
- package/lib/src/services/healing/HealingOrchestrator.js +3 -1
- package/lib/src/services/identity/identityChanges.js +41 -0
- package/lib/src/services/interceptor/CertManager.js +50 -16
- package/lib/src/services/logcat/NodeDeviceLogStore.js +99 -0
- package/lib/src/services/logcat/NodeDeviceLogsCollector.js +164 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +279 -37
- package/lib/src/services/logcat/deviceLogBook.js +5 -4
- package/lib/src/services/logcat/iosDriverLog.js +178 -0
- package/lib/src/services/logcat/nodeDeviceLogs.js +91 -0
- package/lib/src/services/metrics/nodeMetrics.js +20 -49
- package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
- package/lib/src/services/token/JwtKeyService.js +8 -2
- package/lib/src/services/token/mcpTokenTtl.js +27 -0
- package/lib/src/sessions/LocalSession.js +29 -1
- package/lib/src/sessions/RemoteSession.js +23 -4
- package/lib/src/sessions/SessionManager.js +33 -9
- package/lib/test/helpers/driver-log.js +38 -0
- package/lib/test/helpers/fake-http-proxy.js +106 -0
- package/lib/test/helpers/fake-logcat.js +80 -0
- package/lib/test/helpers/hub-node-pair.js +227 -0
- package/lib/test/helpers/otlp-probe.js +107 -0
- package/lib/test/integration/androidDevices.spec.js +1 -1
- package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
- package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
- package/lib/test/integration/selector-state-flow.spec.js +5 -5
- package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
- package/lib/test/unit/android-adb-retry.spec.js +112 -0
- package/lib/test/unit/command-log-network-capture.spec.js +191 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +74 -11
- package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
- package/lib/test/unit/database-check.spec.js +1 -0
- package/lib/test/unit/device-utils.spec.js +13 -4
- package/lib/test/unit/event-log-session-data.spec.js +11 -4
- package/lib/test/unit/failure-analysis.spec.js +22 -8
- package/lib/test/unit/failure-categories.spec.js +24 -22
- package/lib/test/unit/failure-categorization.spec.js +393 -0
- package/lib/test/unit/healing-tier-gate.spec.js +3 -2
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +72 -1
- package/lib/test/unit/hub-node-iphone-readiness.spec.js +134 -0
- package/lib/test/unit/hub-node-session-device-logs.spec.js +194 -0
- package/lib/test/unit/hub-node-session-metrics.spec.js +24 -192
- package/lib/test/unit/hub-remote-session-heartbeat.spec.js +8 -1
- package/lib/test/unit/interceptor/CertManager.spec.js +89 -0
- package/lib/test/unit/ios-driver-log.spec.js +168 -0
- package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
- package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -0
- package/lib/test/unit/logcat-ws.spec.js +78 -37
- package/lib/test/unit/logger-json-levels.spec.js +44 -0
- package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
- package/lib/test/unit/node-device-log-store.spec.js +102 -0
- package/lib/test/unit/node-device-logs-collector.spec.js +343 -0
- package/lib/test/unit/node-gateway-create-appium-server.spec.js +8 -1
- package/lib/test/unit/node-hub-session-owner.spec.js +8 -1
- package/lib/test/unit/node-session-device-logs-route.spec.js +103 -0
- package/lib/test/unit/observability-example-loki.spec.js +25 -0
- package/lib/test/unit/openapi-for-site.spec.js +80 -0
- package/lib/test/unit/outbound-proxy.spec.js +320 -0
- package/lib/test/unit/port-exhaustion.spec.js +2 -1
- package/lib/test/unit/proxy-option.spec.js +358 -0
- package/lib/test/unit/run-migrations-database.spec.js +440 -0
- package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
- package/lib/test/unit/run-migrations.test.js +264 -0
- package/lib/test/unit/selector-health-actions.spec.js +3 -1
- package/lib/test/unit/selector-state-service.spec.js +72 -23
- package/lib/test/unit/selector-verification-clean-build.spec.js +1 -1
- package/lib/test/unit/selector-verification-job.spec.js +15 -6
- package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
- package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
- package/lib/test/unit/session-device-logs-hub-node.spec.js +340 -0
- package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
- package/lib/test/unit/session-device-logs-lifecycle.spec.js +137 -0
- package/lib/test/unit/session-device-logs.spec.js +43 -96
- package/lib/test/unit/session-end-failure-analysis.spec.js +5 -5
- package/lib/test/unit/session-log-indexes.spec.js +16 -1
- package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
- package/lib/test/unit/tracing-export-routing.spec.js +137 -0
- package/lib/test/unit/tracing-session-spans.spec.js +337 -0
- package/lib/test/unit/unexpected-shutdown-reason.spec.js +2 -2
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -1
- package/schema.json +11 -7
- package/scripts/dev/readme-screenshots.js +276 -23
- package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
- package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
- package/scripts/dev/screenshot-fixtures/session.js +242 -0
- package/scripts/export-openapi.js +32 -0
- package/scripts/lib/openapi-for-site.js +79 -0
- package/lib/public/assets/index-BOxe43p1.js +0 -219
- package/lib/public/assets/session-detail-page-AZJE6XXE.js +0 -4
- package/lib/src/scripts/prepare-prisma.js +0 -89
|
@@ -91,13 +91,14 @@ const keyOf = (r) => `${r.pid} ${r.tid} ${r.level} ${r.tag} ${r.message}`;
|
|
|
91
91
|
const isError = (r) => r.level === 'E' || r.level === 'F';
|
|
92
92
|
class DeviceLogBook {
|
|
93
93
|
constructor(opts) {
|
|
94
|
-
var _a, _b;
|
|
94
|
+
var _a, _b, _c;
|
|
95
95
|
this.kept = 0;
|
|
96
96
|
this.leftOut = 0;
|
|
97
97
|
this.limitNoted = false;
|
|
98
98
|
this.clock = opts.clock;
|
|
99
|
-
this.
|
|
100
|
-
this.
|
|
99
|
+
this.format = (_a = opts.format) !== null && _a !== void 0 ? _a : formatThreadtime;
|
|
100
|
+
this.lineLimit = (_b = opts.lineLimit) !== null && _b !== void 0 ? _b : exports.DEVICE_LOG_LINE_LIMIT;
|
|
101
|
+
this.errorLimit = (_c = opts.errorLimit) !== null && _c !== void 0 ? _c : exports.DEVICE_LOG_ERROR_LIMIT;
|
|
101
102
|
// A record at `ts` was logged at ts + zoneShift + skew by this server's
|
|
102
103
|
// clock (timeOf); it counts from WINDOW_SLACK_MS before `since`.
|
|
103
104
|
const first = opts.since - exports.WINDOW_SLACK_MS - this.clock.zoneShiftMs - this.clock.skewMs;
|
|
@@ -126,7 +127,7 @@ class DeviceLogBook {
|
|
|
126
127
|
const room = this.kept < this.lineLimit || (isError(rec) && this.kept < this.lineLimit + this.errorLimit);
|
|
127
128
|
if (room) {
|
|
128
129
|
this.kept += 1;
|
|
129
|
-
out.push({ message:
|
|
130
|
+
out.push({ message: this.format(rec), timestamp: this.timeOf(rec) });
|
|
130
131
|
}
|
|
131
132
|
else {
|
|
132
133
|
this.leftOut += 1;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.IosAppLines = exports.iosLineText = void 0;
|
|
4
|
+
exports.iosLineLevel = iosLineLevel;
|
|
5
|
+
exports.recordFromDriverLog = recordFromDriverLog;
|
|
6
|
+
exports.bufferedDriverLog = bufferedDriverLog;
|
|
7
|
+
exports.isDriverLog = isDriverLog;
|
|
8
|
+
// A real iPhone's syslog line: "Oct 5 20:31:40 iPhone SpringBoard(FrontBoard)[57] <Notice>: …".
|
|
9
|
+
const SYSLOG_LEVEL = /<(Fault|Error|Warning|Notice|Default|Info|Debug)>:/;
|
|
10
|
+
// A simulator's compact line: "2026-10-05 20:31:40.123 E SpringBoard[1234:5678] …".
|
|
11
|
+
const COMPACT_LEVEL = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+\s+(Df|Db|E|F|I|A)\s/;
|
|
12
|
+
const SYSLOG_LETTERS = {
|
|
13
|
+
Fault: 'F',
|
|
14
|
+
Error: 'E',
|
|
15
|
+
Warning: 'W',
|
|
16
|
+
Notice: 'I',
|
|
17
|
+
Default: 'I',
|
|
18
|
+
Info: 'I',
|
|
19
|
+
Debug: 'D',
|
|
20
|
+
};
|
|
21
|
+
const COMPACT_LETTERS = {
|
|
22
|
+
F: 'F',
|
|
23
|
+
E: 'E',
|
|
24
|
+
Df: 'I',
|
|
25
|
+
I: 'I',
|
|
26
|
+
A: 'I',
|
|
27
|
+
Db: 'D',
|
|
28
|
+
};
|
|
29
|
+
/** The level an iOS log line carries, as a logcat letter; `I` when it carries none. */
|
|
30
|
+
function iosLineLevel(message) {
|
|
31
|
+
var _a, _b;
|
|
32
|
+
const syslog = (_a = SYSLOG_LEVEL.exec(message)) === null || _a === void 0 ? void 0 : _a[1];
|
|
33
|
+
if (syslog)
|
|
34
|
+
return SYSLOG_LETTERS[syslog];
|
|
35
|
+
const compact = (_b = COMPACT_LEVEL.exec(message)) === null || _b === void 0 ? void 0 : _b[1];
|
|
36
|
+
if (compact)
|
|
37
|
+
return COMPACT_LETTERS[compact];
|
|
38
|
+
return 'I';
|
|
39
|
+
}
|
|
40
|
+
// What `log stream` prints before its first line: not the device's.
|
|
41
|
+
const STREAM_BANNER = /^(Filtering the log data using |Timestamp\s+Ty\s+Process)/;
|
|
42
|
+
/**
|
|
43
|
+
* A driver entry as a record for the session's book, or null for what isn't
|
|
44
|
+
* a device line. A line without a level of its own (a message's continuation)
|
|
45
|
+
* is kept, as `I`.
|
|
46
|
+
*/
|
|
47
|
+
function recordFromDriverLog(entry) {
|
|
48
|
+
if (!entry || typeof entry.message !== 'string')
|
|
49
|
+
return null;
|
|
50
|
+
const message = entry.message.replace(/\s+$/, '');
|
|
51
|
+
if (!message || STREAM_BANNER.test(message))
|
|
52
|
+
return null;
|
|
53
|
+
const ts = Number(entry.timestamp);
|
|
54
|
+
return {
|
|
55
|
+
ts: Number.isFinite(ts) ? ts : Date.now(),
|
|
56
|
+
pid: 0,
|
|
57
|
+
tid: 0,
|
|
58
|
+
level: iosLineLevel(message),
|
|
59
|
+
tag: '',
|
|
60
|
+
message,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** An iOS record's row text: the line as the phone or simulator printed it. */
|
|
64
|
+
const iosLineText = (rec) => rec.message;
|
|
65
|
+
exports.iosLineText = iosLineText;
|
|
66
|
+
/**
|
|
67
|
+
* The lines the driver's log already holds, oldest first, without taking
|
|
68
|
+
* them: the ones from the create, before Xenon could listen. Reads the
|
|
69
|
+
* driver's internals (`logs`, an LRU cache keyed by arrival, and
|
|
70
|
+
* `_deserializeEntry`, as its own `getLogs()` does), so any other shape gives
|
|
71
|
+
* nothing rather than an error.
|
|
72
|
+
*/
|
|
73
|
+
function bufferedDriverLog(driverLog) {
|
|
74
|
+
var _a;
|
|
75
|
+
const log = driverLog;
|
|
76
|
+
if (typeof ((_a = log === null || log === void 0 ? void 0 : log.logs) === null || _a === void 0 ? void 0 : _a.rvalues) !== 'function' || typeof log._deserializeEntry !== 'function') {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const out = [];
|
|
81
|
+
for (const value of log.logs.rvalues())
|
|
82
|
+
out.push(log._deserializeEntry(value));
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
catch (_b) {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Whether `value` can be listened to as a driver log. */
|
|
90
|
+
function isDriverLog(value) {
|
|
91
|
+
const v = value;
|
|
92
|
+
return typeof (v === null || v === void 0 ? void 0 : v.on) === 'function' && typeof (v === null || v === void 0 ? void 0 : v.removeListener) === 'function';
|
|
93
|
+
}
|
|
94
|
+
// A line's process, the library that logged it and its pid: an iPhone's
|
|
95
|
+
// "… Rabindras-iPhone Edge(UIKitCore)[8788] <Notice>: …" or a simulator's
|
|
96
|
+
// "2026-10-05 20:31:40.123 Df Shop[4127:8812] (UIKitCore) …".
|
|
97
|
+
const SYSLOG_HEADER = /^\w{3}\s+\d+ \d\d:\d\d:\d\d \S+ ([^\s[(]+)(?:\(([^)]*)\))?\[(\d+)\]/;
|
|
98
|
+
const COMPACT_HEADER = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+\s+\S+\s+([^\s[]+)\[(\d+)(?::\d+)?\](?:\s+\(([^)]*)\))?/;
|
|
99
|
+
function headerOf(message) {
|
|
100
|
+
const syslog = SYSLOG_HEADER.exec(message);
|
|
101
|
+
if (syslog)
|
|
102
|
+
return { name: syslog[1], sender: syslog[2] || undefined, pid: syslog[3] };
|
|
103
|
+
const compact = COMPACT_HEADER.exec(message);
|
|
104
|
+
if (compact)
|
|
105
|
+
return { name: compact[1], sender: compact[3] || undefined, pid: compact[2] };
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
109
|
+
/**
|
|
110
|
+
* Which of an iPhone's or simulator's lines a session keeps: what the app
|
|
111
|
+
* under test's own code logs, every error and fault inside its process, what
|
|
112
|
+
* the phone's app manager says about it (its launch, its state, how it
|
|
113
|
+
* ended), the crash reports and memory kills that name it, and every fault.
|
|
114
|
+
*
|
|
115
|
+
* A phone logs hundreds of lines a second. Measured on an iPhone 14 Plus
|
|
116
|
+
* (iOS 26.5): about 53,000 in a one-minute session, which filled the
|
|
117
|
+
* 10,000-line limit during the create. Keeping the app's whole process wasn't
|
|
118
|
+
* enough either: Edge's process logged about 110 lines a second, of which
|
|
119
|
+
* its own code wrote 5 in 8,839. The rest was Apple's frameworks inside it
|
|
120
|
+
* (UIKit, Network, CFNetwork, WebKit), so their ordinary lines are left out
|
|
121
|
+
* and their errors kept. Only `runningboardd` and `SpringBoard` are kept for
|
|
122
|
+
* naming the app: about twenty other daemons each log every change of its
|
|
123
|
+
* state ("Received state update for 8723 (app<…"), 3,000 lines in 78 s.
|
|
124
|
+
*
|
|
125
|
+
* The app's process is learnt from the lines: the phone announces it by
|
|
126
|
+
* bundle id and pid (`app<com.example.shop(…)>:4127`, `pid: 4127 bundleID:
|
|
127
|
+
* com.example.shop`), and the lines of that pid give its executable's name,
|
|
128
|
+
* so a crash report or a memory kill that names it (`Shop[4127]`, `[Shop]`,
|
|
129
|
+
* `corpse[4127]`) is kept too. A line with no header of its own continues
|
|
130
|
+
* the one before.
|
|
131
|
+
*/
|
|
132
|
+
// The processes whose lines about the app say how it lives and ends: its
|
|
133
|
+
// launch, assertions and exit with its reason (RunningBoard), its scenes and
|
|
134
|
+
// windows (SpringBoard).
|
|
135
|
+
const APP_MANAGERS = new Set(['runningboardd', 'SpringBoard']);
|
|
136
|
+
class IosAppLines {
|
|
137
|
+
constructor(bundleId) {
|
|
138
|
+
this.bundleId = bundleId;
|
|
139
|
+
this.pids = new Set();
|
|
140
|
+
this.names = new Set();
|
|
141
|
+
this.lastKept = false;
|
|
142
|
+
const id = escapeRegExp(bundleId);
|
|
143
|
+
this.pidAnnounced = [
|
|
144
|
+
new RegExp(`app<${id}[^>]*>:(\\d+)`, 'g'),
|
|
145
|
+
new RegExp(`pid:? (\\d+),? bundleID:? ${id}\\b`, 'g'),
|
|
146
|
+
];
|
|
147
|
+
}
|
|
148
|
+
/** Learns the app's pid and executable from a line, without deciding on it. */
|
|
149
|
+
learn(message) {
|
|
150
|
+
if (message.includes(this.bundleId)) {
|
|
151
|
+
for (const re of this.pidAnnounced) {
|
|
152
|
+
for (const m of message.matchAll(re))
|
|
153
|
+
this.pids.add(m[1]);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const header = headerOf(message);
|
|
157
|
+
if (header && this.pids.has(header.pid))
|
|
158
|
+
this.names.add(header.name);
|
|
159
|
+
}
|
|
160
|
+
/** Whether the session keeps the line. */
|
|
161
|
+
keeps(message, level) {
|
|
162
|
+
this.learn(message);
|
|
163
|
+
const header = headerOf(message);
|
|
164
|
+
if (!header)
|
|
165
|
+
return this.lastKept;
|
|
166
|
+
const { name, sender, pid } = header;
|
|
167
|
+
const inApp = this.pids.has(pid) || this.names.has(name);
|
|
168
|
+
const keep = level === 'F' ||
|
|
169
|
+
(inApp && (level === 'E' || !sender || this.names.has(sender))) ||
|
|
170
|
+
(APP_MANAGERS.has(name) && message.includes(this.bundleId)) ||
|
|
171
|
+
(!inApp &&
|
|
172
|
+
([...this.names].some((n) => message.includes(`${n}[`) || message.includes(`[${n}]`)) ||
|
|
173
|
+
[...this.pids].some((p) => message.includes(`[${p}]`))));
|
|
174
|
+
this.lastKept = keep;
|
|
175
|
+
return keep;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
exports.IosAppLines = IosAppLines;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.NodeDeviceLogsSupport = exports.UNCLAIMED_MS = exports.NODE_DEVICE_LOGS_KEEP_AFTER_END_MS = exports.NODE_DEVICE_LOG_PAGE = exports.NODE_DEVICE_LOGS_HEADER = exports.NODE_DEVICE_LOGS_ROUTE = void 0;
|
|
13
|
+
exports.readNodeDeviceLogsReply = readNodeDeviceLogsReply;
|
|
14
|
+
exports.nodeDeviceLogsSourceOf = nodeDeviceLogsSourceOf;
|
|
15
|
+
const typedi_1 = require("typedi");
|
|
16
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
17
|
+
const SessionType_1 = __importDefault(require("../../enums/SessionType"));
|
|
18
|
+
const nodeAsk_1 = require("../../gateway/nodeAsk");
|
|
19
|
+
/** Where a node serves a session's device log lines, under `/xenon/api`. */
|
|
20
|
+
exports.NODE_DEVICE_LOGS_ROUTE = '/node/sessions/:sessionId/device-logs';
|
|
21
|
+
/** On every answer of that route, so a hub can tell a node that has it. */
|
|
22
|
+
exports.NODE_DEVICE_LOGS_HEADER = 'x-xenon-node-device-logs';
|
|
23
|
+
/** Lines in one answer. */
|
|
24
|
+
exports.NODE_DEVICE_LOG_PAGE = 2000;
|
|
25
|
+
/** How long a node keeps an ended session's lines for its hub's last ask. */
|
|
26
|
+
exports.NODE_DEVICE_LOGS_KEEP_AFTER_END_MS = 10 * 60000;
|
|
27
|
+
/**
|
|
28
|
+
* A node stops recording a session no hub has asked about this long after
|
|
29
|
+
* it started: its hub doesn't collect (its dashboard is off, the default) or
|
|
30
|
+
* is older. A hub that collects asks as soon as the session is created.
|
|
31
|
+
*/
|
|
32
|
+
exports.UNCLAIMED_MS = 2 * 60000;
|
|
33
|
+
const STATES = ['recording', 'ended', 'off'];
|
|
34
|
+
/**
|
|
35
|
+
* The line, field by field, or null when it isn't one: one malformed row
|
|
36
|
+
* would fail every later write of the session's lines (a failed batch goes
|
|
37
|
+
* back to the front of the buffer).
|
|
38
|
+
*/
|
|
39
|
+
function lineOf(v) {
|
|
40
|
+
if (!v || typeof v !== 'object')
|
|
41
|
+
return null;
|
|
42
|
+
if (!Number.isSafeInteger(v.seq) || v.seq < 1)
|
|
43
|
+
return null;
|
|
44
|
+
if (typeof v.message !== 'string')
|
|
45
|
+
return null;
|
|
46
|
+
if (typeof v.timestamp !== 'number' || !Number.isFinite(v.timestamp))
|
|
47
|
+
return null;
|
|
48
|
+
return { seq: v.seq, message: v.message, timestamp: v.timestamp };
|
|
49
|
+
}
|
|
50
|
+
function deviceLogsAnswerOf(value) {
|
|
51
|
+
if (!value || !STATES.includes(value.state) || !Array.isArray(value.lines))
|
|
52
|
+
return null;
|
|
53
|
+
return {
|
|
54
|
+
state: value.state,
|
|
55
|
+
lines: value.lines.map(lineOf).filter((x) => !!x),
|
|
56
|
+
more: value.more === true,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function readNodeDeviceLogsReply(status, headers, data) {
|
|
60
|
+
return (0, nodeAsk_1.readNodeReply)(exports.NODE_DEVICE_LOGS_HEADER, status, headers, data, deviceLogsAnswerOf);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The session as a NodeDeviceLogsSource, when it runs on a node. A cloud
|
|
64
|
+
* provider's session (CloudSession) and this server's own (LocalSession)
|
|
65
|
+
* inherit the methods from RemoteSession, so the type decides.
|
|
66
|
+
*/
|
|
67
|
+
function nodeDeviceLogsSourceOf(session) {
|
|
68
|
+
var _a;
|
|
69
|
+
const s = session;
|
|
70
|
+
return s &&
|
|
71
|
+
((_a = s.getType) === null || _a === void 0 ? void 0 : _a.call(s)) === SessionType_1.default.REMOTE &&
|
|
72
|
+
typeof s.nodeDeviceLogs === 'function' &&
|
|
73
|
+
typeof s.nodeOrigin === 'function'
|
|
74
|
+
? s
|
|
75
|
+
: undefined;
|
|
76
|
+
}
|
|
77
|
+
/** Hub side: which nodes lack the route (an older Xenon). Their sessions have no Device logs. */
|
|
78
|
+
let NodeDeviceLogsSupport = class NodeDeviceLogsSupport extends nodeAsk_1.OlderNodes {
|
|
79
|
+
constructor() {
|
|
80
|
+
super(...arguments);
|
|
81
|
+
this.logger = logger_1.default.scope('DeviceLogs');
|
|
82
|
+
}
|
|
83
|
+
warning(origin, status) {
|
|
84
|
+
return (`Node ${origin} has no session device log route (answered ${status}): an older Xenon. ` +
|
|
85
|
+
'Its sessions have no device logs here. Upgrade the node.');
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
exports.NodeDeviceLogsSupport = NodeDeviceLogsSupport;
|
|
89
|
+
exports.NodeDeviceLogsSupport = NodeDeviceLogsSupport = __decorate([
|
|
90
|
+
(0, typedi_1.Service)()
|
|
91
|
+
], NodeDeviceLogsSupport);
|
|
@@ -9,12 +9,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
9
9
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.NodeMetricsSupport = exports.
|
|
12
|
+
exports.NodeMetricsSupport = exports.KEEP_AFTER_END_MS = exports.NODE_METRICS_HEADER = exports.NODE_METRICS_ROUTE = void 0;
|
|
13
13
|
exports.readNodeMetricsReply = readNodeMetricsReply;
|
|
14
14
|
exports.nodeMetricsSourceOf = nodeMetricsSourceOf;
|
|
15
15
|
const typedi_1 = require("typedi");
|
|
16
16
|
const logger_1 = __importDefault(require("../../logger"));
|
|
17
17
|
const SessionType_1 = __importDefault(require("../../enums/SessionType"));
|
|
18
|
+
const nodeAsk_1 = require("../../gateway/nodeAsk");
|
|
18
19
|
/** Where a node serves a session's figures, under `/xenon/api`. */
|
|
19
20
|
exports.NODE_METRICS_ROUTE = '/node/sessions/:sessionId/metrics';
|
|
20
21
|
/** On every answer of that route, so a hub can tell a node that has it. */
|
|
@@ -41,33 +42,20 @@ function sampleOf(v) {
|
|
|
41
42
|
return null;
|
|
42
43
|
return { at: v.at, deviceCpuPct, deviceMemMb, deviceMemTotalMb, appCpuPct, appMemMb, appId };
|
|
43
44
|
}
|
|
44
|
-
function
|
|
45
|
+
function metricsAnswerOf(value) {
|
|
45
46
|
var _a;
|
|
46
|
-
if (!
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (status === 200 && value && STATES.includes(value.state) && Array.isArray(value.samples)) {
|
|
59
|
-
return {
|
|
60
|
-
kind: 'answer',
|
|
61
|
-
answer: {
|
|
62
|
-
platform: String((_a = value.platform) !== null && _a !== void 0 ? _a : ''),
|
|
63
|
-
state: value.state,
|
|
64
|
-
samples: value.samples
|
|
65
|
-
.map(sampleOf)
|
|
66
|
-
.filter((x) => x !== null),
|
|
67
|
-
},
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
return { kind: 'unavailable', reason: `answered ${status}` };
|
|
47
|
+
if (!value || !STATES.includes(value.state) || !Array.isArray(value.samples))
|
|
48
|
+
return null;
|
|
49
|
+
return {
|
|
50
|
+
platform: String((_a = value.platform) !== null && _a !== void 0 ? _a : ''),
|
|
51
|
+
state: value.state,
|
|
52
|
+
samples: value.samples
|
|
53
|
+
.map(sampleOf)
|
|
54
|
+
.filter((x) => x !== null),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function readNodeMetricsReply(status, headers, data) {
|
|
58
|
+
return (0, nodeAsk_1.readNodeReply)(exports.NODE_METRICS_HEADER, status, headers, data, metricsAnswerOf);
|
|
71
59
|
}
|
|
72
60
|
/**
|
|
73
61
|
* The session as a NodeMetricsSource, when it runs on a node. A cloud
|
|
@@ -84,35 +72,18 @@ function nodeMetricsSourceOf(session) {
|
|
|
84
72
|
? s
|
|
85
73
|
: undefined;
|
|
86
74
|
}
|
|
87
|
-
/** How long the hub leaves a node without the route alone before asking it again. */
|
|
88
|
-
exports.NODE_METRICS_RECHECK_MS = 10 * 60000;
|
|
89
75
|
/**
|
|
90
76
|
* Hub side: which nodes lack the route (an older Xenon). Their sessions show
|
|
91
77
|
* no figures; the hub says so once per node, and asks again after
|
|
92
|
-
*
|
|
78
|
+
* OLDER_NODE_RECHECK_MS, so a node upgraded in place is picked up.
|
|
93
79
|
*/
|
|
94
|
-
let NodeMetricsSupport = class NodeMetricsSupport {
|
|
80
|
+
let NodeMetricsSupport = class NodeMetricsSupport extends nodeAsk_1.OlderNodes {
|
|
95
81
|
constructor() {
|
|
82
|
+
super(...arguments);
|
|
96
83
|
this.logger = logger_1.default.scope('NodeMetrics');
|
|
97
|
-
this.now = () => Date.now();
|
|
98
|
-
this.unsupportedUntil = new Map();
|
|
99
|
-
this.warned = new Set();
|
|
100
|
-
}
|
|
101
|
-
shouldAsk(origin) {
|
|
102
|
-
const until = this.unsupportedUntil.get(origin);
|
|
103
|
-
if (until === undefined)
|
|
104
|
-
return true;
|
|
105
|
-
if (until > this.now())
|
|
106
|
-
return false;
|
|
107
|
-
this.unsupportedUntil.delete(origin);
|
|
108
|
-
return true;
|
|
109
84
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (this.warned.has(origin))
|
|
113
|
-
return;
|
|
114
|
-
this.warned.add(origin);
|
|
115
|
-
this.logger.warn(`Node ${origin} has no session metrics route (answered ${status}): an older Xenon. ` +
|
|
85
|
+
warning(origin, status) {
|
|
86
|
+
return (`Node ${origin} has no session metrics route (answered ${status}): an older Xenon. ` +
|
|
116
87
|
'Its sessions show no CPU or memory. Upgrade the node.');
|
|
117
88
|
}
|
|
118
89
|
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
12
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
13
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
14
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
15
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
16
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
17
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
21
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
22
|
+
};
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.SelectorVisibilityResolver = exports.SELECTOR_VISIBILITY_LOOKUP_TIMEOUT_MS = exports.SELECTOR_VISIBILITY_TTL_MS = void 0;
|
|
25
|
+
const typedi_1 = require("typedi");
|
|
26
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
27
|
+
const sessionVisibility_1 = require("../device-access/sessionVisibility");
|
|
28
|
+
const access_1 = require("./access");
|
|
29
|
+
/**
|
|
30
|
+
* How long an answer is reused. A selector event is rare, but a verification
|
|
31
|
+
* run can send several at once, each to every dashboard connected.
|
|
32
|
+
*/
|
|
33
|
+
exports.SELECTOR_VISIBILITY_TTL_MS = 5000;
|
|
34
|
+
/** A lookup that takes longer answers no for that event, and isn't cached. */
|
|
35
|
+
exports.SELECTOR_VISIBILITY_LOOKUP_TIMEOUT_MS = 2000;
|
|
36
|
+
const MAX_ENTRIES = 10000;
|
|
37
|
+
/**
|
|
38
|
+
* Whether a dashboard caller may see a selector, for the `selector_*` live
|
|
39
|
+
* events: Selector Health's own rule (`access.ts`), a selector healed in at
|
|
40
|
+
* least one session the caller may see. Admins see every selector and are
|
|
41
|
+
* never asked about.
|
|
42
|
+
*
|
|
43
|
+
* Answers are cached for {@link SELECTOR_VISIBILITY_TTL_MS} per viewer (user
|
|
44
|
+
* and teams, so a member's tabs share one) and selector, and a viewer's
|
|
45
|
+
* sessions are read once for all their selectors. Concurrent questions share
|
|
46
|
+
* one lookup. A lookup that fails, or outlasts
|
|
47
|
+
* {@link SELECTOR_VISIBILITY_LOOKUP_TIMEOUT_MS}, answers no and isn't cached.
|
|
48
|
+
*/
|
|
49
|
+
let SelectorVisibilityResolver = class SelectorVisibilityResolver {
|
|
50
|
+
// An interface-typed parameter emits `Object`, which TypeDI leaves alone; a
|
|
51
|
+
// function-typed one would make Container.get throw.
|
|
52
|
+
constructor(deps = {}) {
|
|
53
|
+
this.deps = deps;
|
|
54
|
+
this.scopes = new Map();
|
|
55
|
+
this.answers = new Map();
|
|
56
|
+
}
|
|
57
|
+
/** Never rejects. */
|
|
58
|
+
canSee(key, viewer) {
|
|
59
|
+
var _a;
|
|
60
|
+
const who = JSON.stringify([(_a = viewer.userId) !== null && _a !== void 0 ? _a : null, Array.from(new Set(viewer.teamIds)).sort()]);
|
|
61
|
+
const answer = this.cached(this.answers, JSON.stringify([who, key.strategy, key.selector]), () => __awaiter(this, void 0, void 0, function* () {
|
|
62
|
+
const scope = yield this.cached(this.scopes, who, () => this.sessionScope(viewer));
|
|
63
|
+
return this.healedIn(key, scope);
|
|
64
|
+
}));
|
|
65
|
+
return answer.catch((err) => {
|
|
66
|
+
var _a;
|
|
67
|
+
logger_1.default.warn(`[SelectorVisibility] could not tell who may see ${key.strategy}=${key.selector}: ` +
|
|
68
|
+
`${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}; its event went to admins only`);
|
|
69
|
+
return false;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
cached(map, key, load) {
|
|
73
|
+
const entry = map.get(key);
|
|
74
|
+
if (entry &&
|
|
75
|
+
(entry.settledAt === undefined || this.now() - entry.settledAt < exports.SELECTOR_VISIBILITY_TTL_MS)) {
|
|
76
|
+
return entry.value;
|
|
77
|
+
}
|
|
78
|
+
const fresh = { value: Promise.resolve(undefined) };
|
|
79
|
+
fresh.value = this.bounded(load).then((value) => {
|
|
80
|
+
fresh.settledAt = this.now();
|
|
81
|
+
return value;
|
|
82
|
+
}, (err) => {
|
|
83
|
+
if (map.get(key) === fresh)
|
|
84
|
+
map.delete(key);
|
|
85
|
+
throw err;
|
|
86
|
+
});
|
|
87
|
+
if (map.size >= MAX_ENTRIES && !map.has(key))
|
|
88
|
+
map.clear();
|
|
89
|
+
map.set(key, fresh);
|
|
90
|
+
return fresh.value;
|
|
91
|
+
}
|
|
92
|
+
bounded(load) {
|
|
93
|
+
var _a;
|
|
94
|
+
const timeoutMs = (_a = this.deps.lookupTimeoutMs) !== null && _a !== void 0 ? _a : exports.SELECTOR_VISIBILITY_LOOKUP_TIMEOUT_MS;
|
|
95
|
+
let timer;
|
|
96
|
+
const timedOut = new Promise((_, reject) => {
|
|
97
|
+
var _a;
|
|
98
|
+
timer = setTimeout(() => reject(new Error(`took over ${timeoutMs} ms`)), timeoutMs);
|
|
99
|
+
(_a = timer.unref) === null || _a === void 0 ? void 0 : _a.call(timer);
|
|
100
|
+
});
|
|
101
|
+
const lookup = new Promise((resolve) => resolve(load()));
|
|
102
|
+
return Promise.race([lookup, timedOut]).finally(() => clearTimeout(timer));
|
|
103
|
+
}
|
|
104
|
+
sessionScope(viewer) {
|
|
105
|
+
if (this.deps.sessionScope)
|
|
106
|
+
return this.deps.sessionScope(viewer);
|
|
107
|
+
return (0, sessionVisibility_1.visibleSessionWhere)(viewer);
|
|
108
|
+
}
|
|
109
|
+
healedIn(key, scope) {
|
|
110
|
+
var _a;
|
|
111
|
+
return ((_a = this.deps.healedIn) !== null && _a !== void 0 ? _a : access_1.canSeeSelector)(key, scope);
|
|
112
|
+
}
|
|
113
|
+
now() {
|
|
114
|
+
return this.deps.now ? this.deps.now() : Date.now();
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
exports.SelectorVisibilityResolver = SelectorVisibilityResolver;
|
|
118
|
+
exports.SelectorVisibilityResolver = SelectorVisibilityResolver = __decorate([
|
|
119
|
+
(0, typedi_1.Service)(),
|
|
120
|
+
__metadata("design:paramtypes", [Object])
|
|
121
|
+
], SelectorVisibilityResolver);
|
|
@@ -51,7 +51,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
51
51
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
52
52
|
};
|
|
53
53
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
|
-
exports.JwtKeyService = void 0;
|
|
54
|
+
exports.JwtKeyService = exports.JWT_CLOCK_TOLERANCE_SEC = void 0;
|
|
55
55
|
const typedi_1 = require("typedi");
|
|
56
56
|
const jose = __importStar(require("jose"));
|
|
57
57
|
const crypto_1 = require("crypto");
|
|
@@ -59,6 +59,12 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
59
59
|
const path_1 = __importDefault(require("path"));
|
|
60
60
|
const KEY_FILE = 'xenon-jwt-private.pem';
|
|
61
61
|
const ISSUER = process.env.XENON_JWT_ISSUER || 'xenon-hub';
|
|
62
|
+
/**
|
|
63
|
+
* How long past `exp` verify() still takes a token (spec §7.1: ±60 s skew,
|
|
64
|
+
* mirrors appium-mcp-auth). A dashboard socket on a bearer token closes then,
|
|
65
|
+
* when REST stops taking it (SocketServer).
|
|
66
|
+
*/
|
|
67
|
+
exports.JWT_CLOCK_TOLERANCE_SEC = 60;
|
|
62
68
|
/**
|
|
63
69
|
* RS256 signing key for hub-issued JWTs (REST/MCP tokens, stream tickets).
|
|
64
70
|
* Key material lives on disk (0600), never in the database. `kid` is derived
|
|
@@ -119,7 +125,7 @@ let JwtKeyService = class JwtKeyService {
|
|
|
119
125
|
const { payload } = yield jose.jwtVerify(token, this.publicKey, {
|
|
120
126
|
issuer: ISSUER,
|
|
121
127
|
audience: opts.audience,
|
|
122
|
-
clockTolerance:
|
|
128
|
+
clockTolerance: exports.JWT_CLOCK_TOLERANCE_SEC,
|
|
123
129
|
});
|
|
124
130
|
return payload;
|
|
125
131
|
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DEFAULT_MCP_TOKEN_TTL_SEC = void 0;
|
|
7
|
+
exports.mcpTokenTtlSec = mcpTokenTtlSec;
|
|
8
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
9
|
+
exports.DEFAULT_MCP_TOKEN_TTL_SEC = 86400;
|
|
10
|
+
/**
|
|
11
|
+
* The lifetime of the xenon-mcp tokens and session tokens POST /auth/token
|
|
12
|
+
* mints, in seconds: XENON_MCP_TOKEN_TTL_SEC, a whole number above 0, else a
|
|
13
|
+
* day. Through 2.15 any other value (`abc`) gave every such token a NaN
|
|
14
|
+
* lifetime; it now warns and keeps the default.
|
|
15
|
+
*/
|
|
16
|
+
function mcpTokenTtlSec(raw = process.env.XENON_MCP_TOKEN_TTL_SEC, warn = (message) => logger_1.default.warn(message)) {
|
|
17
|
+
var _a;
|
|
18
|
+
const text = (_a = raw === null || raw === void 0 ? void 0 : raw.trim()) !== null && _a !== void 0 ? _a : '';
|
|
19
|
+
if (text === '')
|
|
20
|
+
return exports.DEFAULT_MCP_TOKEN_TTL_SEC;
|
|
21
|
+
const seconds = /^\d+$/.test(text) ? Number(text) : NaN;
|
|
22
|
+
if (Number.isSafeInteger(seconds) && seconds > 0)
|
|
23
|
+
return seconds;
|
|
24
|
+
warn(`XENON_MCP_TOKEN_TTL_SEC is "${raw}", not a whole number of seconds above 0. ` +
|
|
25
|
+
`The tokens it sets the lifetime of last ${exports.DEFAULT_MCP_TOKEN_TTL_SEC} s, the default.`);
|
|
26
|
+
return exports.DEFAULT_MCP_TOKEN_TTL_SEC;
|
|
27
|
+
}
|
|
@@ -109,6 +109,26 @@ class LocalSession extends RemoteSession_1.RemoteSession {
|
|
|
109
109
|
};
|
|
110
110
|
});
|
|
111
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* The device log the session's driver captures (XCUITest's
|
|
114
|
+
* `logs.syslog`), for the session's Device logs (SessionDeviceLogs).
|
|
115
|
+
* Undefined for a driver that keeps none, or a session that skips log
|
|
116
|
+
* capture.
|
|
117
|
+
*/
|
|
118
|
+
deviceLog() {
|
|
119
|
+
var _a, _b, _c, _d, _e, _f;
|
|
120
|
+
const sessionDriver = ((_c = (_b = (_a = this.driver) === null || _a === void 0 ? void 0 : _a.sessions) === null || _b === void 0 ? void 0 : _b[this.sessionId]) === null || _c === void 0 ? void 0 : _c.proxydriver) ||
|
|
121
|
+
((_e = (_d = this.driver) === null || _d === void 0 ? void 0 : _d.sessions) === null || _e === void 0 ? void 0 : _e[this.sessionId]);
|
|
122
|
+
return (_f = sessionDriver === null || sessionDriver === void 0 ? void 0 : sessionDriver.logs) === null || _f === void 0 ? void 0 : _f.syslog;
|
|
123
|
+
}
|
|
124
|
+
/** The bundle id of the app under test, as the session's driver has it (XCUITest's `opts.bundleId`). */
|
|
125
|
+
appUnderTest() {
|
|
126
|
+
var _a, _b, _c, _d, _e, _f;
|
|
127
|
+
const sessionDriver = ((_c = (_b = (_a = this.driver) === null || _a === void 0 ? void 0 : _a.sessions) === null || _b === void 0 ? void 0 : _b[this.sessionId]) === null || _c === void 0 ? void 0 : _c.proxydriver) ||
|
|
128
|
+
((_e = (_d = this.driver) === null || _d === void 0 ? void 0 : _d.sessions) === null || _e === void 0 ? void 0 : _e[this.sessionId]);
|
|
129
|
+
const bundleId = (_f = sessionDriver === null || sessionDriver === void 0 ? void 0 : sessionDriver.opts) === null || _f === void 0 ? void 0 : _f.bundleId;
|
|
130
|
+
return typeof bundleId === 'string' && bundleId ? bundleId : undefined;
|
|
131
|
+
}
|
|
112
132
|
/**
|
|
113
133
|
* A local session's driver is in this process, so ask it directly instead of
|
|
114
134
|
* looping back over HTTP. The loopback works, but it re-enters the Appium
|
|
@@ -246,7 +266,7 @@ class LocalSession extends RemoteSession_1.RemoteSession {
|
|
|
246
266
|
// Override to use proper Appium URL for video commands
|
|
247
267
|
startVideoRecording(options, driverOverride) {
|
|
248
268
|
return __awaiter(this, void 0, void 0, function* () {
|
|
249
|
-
var _a, _b, _c;
|
|
269
|
+
var _a, _b, _c, _d;
|
|
250
270
|
logger_1.default.info(`[LocalSession] Starting video recording for session ${this.sessionId}`);
|
|
251
271
|
// Principal Intelligence: For local sessions, try to call the driver directly.
|
|
252
272
|
// We try driverOverride first (e.g. from onUnexpectedShutdown), then search the session map, then this.driver.
|
|
@@ -291,6 +311,14 @@ class LocalSession extends RemoteSession_1.RemoteSession {
|
|
|
291
311
|
const result = yield typedi_1.Container.get(AndroidStreamService_1.default).startStream(device.udid);
|
|
292
312
|
mjpegPort = result.mjpegPort;
|
|
293
313
|
}
|
|
314
|
+
else if (device.platform === 'ios' && !device.realDevice) {
|
|
315
|
+
// A simulator's WebDriverAgent is the driver's, and serves the picture
|
|
316
|
+
// on this Mac at the session's mjpegServerPort. The stream service is
|
|
317
|
+
// an iPhone's (go-ios, iproxy over USB): on a simulator it forwarded
|
|
318
|
+
// nothing, and no video was written.
|
|
319
|
+
mjpegPort =
|
|
320
|
+
Number((_d = this.getCapabilities()) === null || _d === void 0 ? void 0 : _d.mjpegServerPort) || device.mjpegServerPort || undefined;
|
|
321
|
+
}
|
|
294
322
|
else if (device.platform === 'ios') {
|
|
295
323
|
const result = yield typedi_1.Container.get(IOSStreamService_1.default).startStream(device.udid);
|
|
296
324
|
mjpegPort = result.mjpegPort;
|