@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
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const typedi_1 = require("typedi");
|
|
18
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
19
|
+
const SessionDeviceLogs_1 = require("../../src/services/logcat/SessionDeviceLogs");
|
|
20
|
+
const NodeDeviceLogStore_1 = require("../../src/services/logcat/NodeDeviceLogStore");
|
|
21
|
+
const NodeDeviceLogsCollector_1 = require("../../src/services/logcat/NodeDeviceLogsCollector");
|
|
22
|
+
const nodeDeviceLogs_1 = require("../../src/services/logcat/nodeDeviceLogs");
|
|
23
|
+
const deviceLogBook_1 = require("../../src/services/logcat/deviceLogBook");
|
|
24
|
+
const hubSessionToken_1 = require("../../src/gateway/hubSessionToken");
|
|
25
|
+
const commandAuth_1 = require("../../src/middleware/commandAuth");
|
|
26
|
+
const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
|
|
27
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
28
|
+
const selector_health_fixture_1 = require("../helpers/selector-health-fixture");
|
|
29
|
+
const hub_node_pair_1 = require("../helpers/hub-node-pair");
|
|
30
|
+
const fake_logcat_1 = require("../helpers/fake-logcat");
|
|
31
|
+
/**
|
|
32
|
+
* A hub writes the Device logs of a session on a node's phone, collected from
|
|
33
|
+
* the node: the node records its phone's log for the session the hub created
|
|
34
|
+
* (its dashboard off) and holds the lines; the hub asks for them with its
|
|
35
|
+
* session token (per-command auth is on) and writes them as the session page
|
|
36
|
+
* reads them. The node's `adb logcat` is fake; everything from it to the
|
|
37
|
+
* hub's rows is the real thing.
|
|
38
|
+
*/
|
|
39
|
+
const quiet = { info: () => undefined, warn: () => undefined, debug: () => undefined };
|
|
40
|
+
/** The node's service, its phone's log stream fake. */
|
|
41
|
+
class NodeSideDeviceLogs extends SessionDeviceLogs_1.SessionDeviceLogs {
|
|
42
|
+
constructor(logcat, unclaimed = 60000) {
|
|
43
|
+
super();
|
|
44
|
+
this.logcat = logcat;
|
|
45
|
+
this.unclaimed = unclaimed;
|
|
46
|
+
}
|
|
47
|
+
streams() {
|
|
48
|
+
return this.logcat;
|
|
49
|
+
}
|
|
50
|
+
readClock() {
|
|
51
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
52
|
+
return deviceLogBook_1.UNKNOWN_CLOCK;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
unclaimedMs() {
|
|
56
|
+
return this.unclaimed;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** The hub's service: none of the phones are its own; it asks the node every 50 ms. */
|
|
60
|
+
class HubSideDeviceLogs extends SessionDeviceLogs_1.SessionDeviceLogs {
|
|
61
|
+
context() {
|
|
62
|
+
return {
|
|
63
|
+
pluginArgs: Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { bindHostOrIp: '127.0.0.1' }),
|
|
64
|
+
port: 4799,
|
|
65
|
+
nodeId: 'hub-1',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
collectorFor(source, onLines, storedTail) {
|
|
69
|
+
return new NodeDeviceLogsCollector_1.NodeDeviceLogsCollector({
|
|
70
|
+
source,
|
|
71
|
+
onLines,
|
|
72
|
+
storedTail,
|
|
73
|
+
support: typedi_1.Container.get(nodeDeviceLogs_1.NodeDeviceLogsSupport),
|
|
74
|
+
logger: quiet,
|
|
75
|
+
intervalMs: 50,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
describe('a hub writes a node session’s device log', function () {
|
|
80
|
+
this.timeout(60000);
|
|
81
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
82
|
+
const pair = (0, hub_node_pair_1.useHubAndNode)(scratch, [
|
|
83
|
+
SessionDeviceLogs_1.SessionDeviceLogs,
|
|
84
|
+
NodeDeviceLogStore_1.NodeDeviceLogStore,
|
|
85
|
+
nodeDeviceLogs_1.NodeDeviceLogsSupport,
|
|
86
|
+
]);
|
|
87
|
+
let logcat;
|
|
88
|
+
beforeEach(() => {
|
|
89
|
+
logcat = new fake_logcat_1.FakeLogcatStreams();
|
|
90
|
+
typedi_1.Container.set(NodeDeviceLogStore_1.NodeDeviceLogStore, new NodeDeviceLogStore_1.NodeDeviceLogStore());
|
|
91
|
+
typedi_1.Container.set(nodeDeviceLogs_1.NodeDeviceLogsSupport, new nodeDeviceLogs_1.NodeDeviceLogsSupport());
|
|
92
|
+
});
|
|
93
|
+
afterEach(() => logcat.cleanup());
|
|
94
|
+
/** Boots the pair, the node's device log service `nodeLogs`; no CPU and memory sampling. */
|
|
95
|
+
function boot(nodeLogs) {
|
|
96
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
97
|
+
typedi_1.Container.set(SessionDeviceLogs_1.SessionDeviceLogs, nodeLogs);
|
|
98
|
+
yield pair.boot({ sessionMetrics: false });
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/** The hub's row for the session, as onSessionStarted writes it. */
|
|
102
|
+
const hubRow = (sessionId) => scratch.db.session.create({
|
|
103
|
+
data: {
|
|
104
|
+
id: sessionId,
|
|
105
|
+
device_udid: 'phone-1',
|
|
106
|
+
device_platform: 'android',
|
|
107
|
+
device_version: '14',
|
|
108
|
+
desired_capabilities: '{}',
|
|
109
|
+
session_capabilities: '{}',
|
|
110
|
+
node_id: hub_node_pair_1.NODE_ID,
|
|
111
|
+
has_live_video: false,
|
|
112
|
+
status: 'running',
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
/** The session's Device logs as the hub's session page gets them. */
|
|
116
|
+
const page = (sessionId) => __awaiter(this, void 0, void 0, function* () {
|
|
117
|
+
const res = yield (0, loopbackRequest_1.default)((0, selector_health_fixture_1.selectorHealthApp)(selector_health_fixture_1.ADMIN)).get(`/session/${sessionId}/logs/device`);
|
|
118
|
+
(0, chai_1.expect)(res.status, res.text).to.equal(200);
|
|
119
|
+
return res.body.map((r) => r.message.replace(/^.*?Tag {5}: /, ''));
|
|
120
|
+
});
|
|
121
|
+
/** The page, once `ok` holds for it. */
|
|
122
|
+
const pageOnce = (sessionId, ok) => __awaiter(this, void 0, void 0, function* () {
|
|
123
|
+
const end = Date.now() + 5000;
|
|
124
|
+
for (;;) {
|
|
125
|
+
const rows = yield page(sessionId);
|
|
126
|
+
if (ok(rows))
|
|
127
|
+
return rows;
|
|
128
|
+
if (Date.now() > end)
|
|
129
|
+
throw new Error(`timed out at ${JSON.stringify(rows)}`);
|
|
130
|
+
yield new Promise((r) => setTimeout(r, 50));
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
const device = () => ({ udid: 'phone-1', host: pair.nodeOrigin, nodeId: hub_node_pair_1.NODE_ID, platform: 'android' });
|
|
134
|
+
it('writes the lines as the session runs, and the last ones when it ends', () => __awaiter(this, void 0, void 0, function* () {
|
|
135
|
+
const nodeLogs = new NodeSideDeviceLogs(logcat);
|
|
136
|
+
yield boot(nodeLogs);
|
|
137
|
+
const sessionId = yield pair.nodeSession();
|
|
138
|
+
// The node records the session the hub created, its dashboard off.
|
|
139
|
+
(0, chai_1.expect)(nodeLogs.isRecording(sessionId)).to.equal(true);
|
|
140
|
+
yield (0, fake_logcat_1.until)(() => logcat.procs.length === 1);
|
|
141
|
+
logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(Date.now() - 600000, 'before the session'));
|
|
142
|
+
logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(Date.now(), 'app started'));
|
|
143
|
+
yield hubRow(sessionId);
|
|
144
|
+
const hub = new HubSideDeviceLogs();
|
|
145
|
+
yield hub.start({ sessionId, device: device(), source: pair.hubSide(sessionId) });
|
|
146
|
+
// Live: written while the session runs.
|
|
147
|
+
(0, chai_1.expect)(yield pageOnce(sessionId, (rows) => rows.length > 0)).to.deep.equal(['app started']);
|
|
148
|
+
// The test's last moments, then the hub's DELETE reaches the node.
|
|
149
|
+
logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(Date.now(), 'FATAL EXCEPTION: main'));
|
|
150
|
+
yield (0, fake_logcat_1.until)(() => typedi_1.Container.get(NodeDeviceLogStore_1.NodeDeviceLogStore).read(sessionId, null).lines.length > 0);
|
|
151
|
+
yield pair.deleteOnNode(sessionId);
|
|
152
|
+
(0, chai_1.expect)(nodeLogs.isRecording(sessionId)).to.equal(false);
|
|
153
|
+
yield hub.stop(sessionId);
|
|
154
|
+
(0, chai_1.expect)(yield page(sessionId)).to.deep.equal(['app started', 'FATAL EXCEPTION: main']);
|
|
155
|
+
}));
|
|
156
|
+
it('asks only with the hub’s token for the session', () => __awaiter(this, void 0, void 0, function* () {
|
|
157
|
+
yield boot(new NodeSideDeviceLogs(logcat));
|
|
158
|
+
const sessionId = yield pair.nodeSession();
|
|
159
|
+
const route = `/xenon/api/node/sessions/${sessionId}/device-logs`;
|
|
160
|
+
const without = yield (0, loopbackRequest_1.default)(pair.nodeOrigin).get(route);
|
|
161
|
+
const forged = yield (0, loopbackRequest_1.default)(pair.nodeOrigin).get(route).set(hubSessionToken_1.HUB_TOKEN_HEADER, 'forged');
|
|
162
|
+
(0, chai_1.expect)([without.status, without.body]).to.deep.equal([404, commandAuth_1.UNKNOWN_SESSION_BODY]);
|
|
163
|
+
(0, chai_1.expect)([forged.status, forged.body]).to.deep.equal([404, commandAuth_1.UNKNOWN_SESSION_BODY]);
|
|
164
|
+
const asked = yield pair.hubSide(sessionId).nodeDeviceLogs(null);
|
|
165
|
+
(0, chai_1.expect)(asked).to.deep.equal({
|
|
166
|
+
kind: 'answer',
|
|
167
|
+
answer: { state: 'recording', lines: [], more: false },
|
|
168
|
+
});
|
|
169
|
+
yield pair.deleteOnNode(sessionId);
|
|
170
|
+
}));
|
|
171
|
+
it('records nothing for a session that turned its device log off', () => __awaiter(this, void 0, void 0, function* () {
|
|
172
|
+
const nodeLogs = new NodeSideDeviceLogs(logcat);
|
|
173
|
+
yield boot(nodeLogs);
|
|
174
|
+
const sessionId = yield pair.nodeSession({ 'xe:save_device_logs': false });
|
|
175
|
+
(0, chai_1.expect)(nodeLogs.isRecording(sessionId)).to.equal(false);
|
|
176
|
+
(0, chai_1.expect)(logcat.procs).to.have.length(0);
|
|
177
|
+
(0, chai_1.expect)(yield pair.hubSide(sessionId).nodeDeviceLogs(null)).to.deep.equal({
|
|
178
|
+
kind: 'answer',
|
|
179
|
+
answer: { state: 'off', lines: [], more: false },
|
|
180
|
+
});
|
|
181
|
+
yield pair.deleteOnNode(sessionId);
|
|
182
|
+
}));
|
|
183
|
+
it('stops recording a session its hub doesn’t ask about', () => __awaiter(this, void 0, void 0, function* () {
|
|
184
|
+
const nodeLogs = new NodeSideDeviceLogs(logcat, 50);
|
|
185
|
+
yield boot(nodeLogs);
|
|
186
|
+
const sessionId = yield pair.nodeSession();
|
|
187
|
+
yield (0, fake_logcat_1.until)(() => !nodeLogs.isRecording(sessionId));
|
|
188
|
+
(0, chai_1.expect)(yield pair.hubSide(sessionId).nodeDeviceLogs(null)).to.deep.equal({
|
|
189
|
+
kind: 'answer',
|
|
190
|
+
answer: { state: 'off', lines: [], more: false },
|
|
191
|
+
});
|
|
192
|
+
yield pair.deleteOnNode(sessionId);
|
|
193
|
+
}));
|
|
194
|
+
});
|
|
@@ -14,38 +14,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
require("reflect-metadata");
|
|
16
16
|
const chai_1 = require("chai");
|
|
17
|
-
const express_1 = __importDefault(require("express"));
|
|
18
|
-
const fs_1 = __importDefault(require("fs"));
|
|
19
|
-
const http_1 = __importDefault(require("http"));
|
|
20
|
-
const os_1 = __importDefault(require("os"));
|
|
21
|
-
const path_1 = __importDefault(require("path"));
|
|
22
|
-
const sinon_1 = __importDefault(require("sinon"));
|
|
23
17
|
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
24
18
|
const typedi_1 = require("typedi");
|
|
25
|
-
const plugin_1 = require("../../src/plugin");
|
|
26
|
-
const registerCommandAuth_1 = require("../../src/app/registerCommandAuth");
|
|
27
|
-
const defaultGateway_1 = require("../../src/gateway/defaultGateway");
|
|
28
|
-
const hubSessionToken_1 = require("../../src/gateway/hubSessionToken");
|
|
29
|
-
const nodeSessionStatus_1 = require("../../src/gateway/nodeSessionStatus");
|
|
30
|
-
const nodeSessionMetrics_1 = require("../../src/gateway/nodeSessionMetrics");
|
|
31
19
|
const SessionMetricsService_1 = require("../../src/services/metrics/SessionMetricsService");
|
|
32
20
|
const NodeMetricsStore_1 = require("../../src/services/metrics/NodeMetricsStore");
|
|
33
21
|
const NodeMetricsCollector_1 = require("../../src/services/metrics/NodeMetricsCollector");
|
|
34
22
|
const nodeMetrics_1 = require("../../src/services/metrics/nodeMetrics");
|
|
35
|
-
const
|
|
36
|
-
const PluginContext_1 = require("../../src/PluginContext");
|
|
23
|
+
const SessionDeviceLogs_1 = require("../../src/services/logcat/SessionDeviceLogs");
|
|
37
24
|
const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
|
|
38
|
-
const device_managers_1 = require("../../src/device-managers");
|
|
39
|
-
const device_store_1 = require("../../src/data-service/device-store");
|
|
40
|
-
const prisma_store_1 = require("../../src/data-service/prisma-store");
|
|
41
|
-
const SessionManager_1 = require("../../src/sessions/SessionManager");
|
|
42
|
-
const RemoteSession_1 = require("../../src/sessions/RemoteSession");
|
|
43
|
-
const appiumUmbrella_1 = require("../../src/sessions/appiumUmbrella");
|
|
44
|
-
const config_1 = require("../../src/config");
|
|
45
|
-
const container_registration_1 = require("../helpers/container-registration");
|
|
46
25
|
const scratch_database_1 = require("../helpers/scratch-database");
|
|
47
26
|
const selector_health_fixture_1 = require("../helpers/selector-health-fixture");
|
|
48
|
-
const
|
|
27
|
+
const hub_node_pair_1 = require("../helpers/hub-node-pair");
|
|
49
28
|
/**
|
|
50
29
|
* A hub collects CPU and memory for a session on a node's phone from the
|
|
51
30
|
* node: against a node in Appium 3's own server() (Appium's umbrella with
|
|
@@ -55,7 +34,6 @@ const appium_umbrella_1 = require("../helpers/appium-umbrella");
|
|
|
55
34
|
* test drives; everything between it and the hub's writes is the real thing.
|
|
56
35
|
*/
|
|
57
36
|
const quiet = { info: () => undefined, warn: () => undefined, error: () => undefined };
|
|
58
|
-
const NODE_ID = 'node-1';
|
|
59
37
|
/** The node's sampler service, its phone sampler replaced by one the test drives. */
|
|
60
38
|
class NodeSideMetrics extends SessionMetricsService_1.SessionMetricsService {
|
|
61
39
|
constructor() {
|
|
@@ -139,169 +117,27 @@ const at = (t) => ({
|
|
|
139
117
|
describe('a hub collects a node session’s CPU and memory', function () {
|
|
140
118
|
this.timeout(60000);
|
|
141
119
|
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
let authDisabledBefore;
|
|
149
|
-
let commandAuthBefore;
|
|
150
|
-
let umbrella;
|
|
151
|
-
let hubUrl;
|
|
152
|
-
let nodeOrigin;
|
|
120
|
+
const pair = (0, hub_node_pair_1.useHubAndNode)(scratch, [
|
|
121
|
+
SessionMetricsService_1.SessionMetricsService,
|
|
122
|
+
NodeMetricsStore_1.NodeMetricsStore,
|
|
123
|
+
nodeMetrics_1.NodeMetricsSupport,
|
|
124
|
+
SessionDeviceLogs_1.SessionDeviceLogs,
|
|
125
|
+
]);
|
|
153
126
|
let nodeMetrics;
|
|
154
|
-
|
|
155
|
-
appiumLogs = (0, appium_umbrella_1.quietAppiumLogs)();
|
|
156
|
-
dirs = [fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-heartbeat-keys-'))];
|
|
157
|
-
hubKeys = new JwtKeyService_1.JwtKeyService();
|
|
158
|
-
yield hubKeys.init(dirs[0]);
|
|
159
|
-
}));
|
|
160
|
-
after(() => {
|
|
161
|
-
appiumLogs.restore();
|
|
162
|
-
for (const dir of dirs)
|
|
163
|
-
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
164
|
-
});
|
|
165
|
-
beforeEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
166
|
-
restore = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, hubSessionToken_1.HubSessionTokenIssuer, device_managers_1.XenonManager, appiumUmbrella_1.AppiumUmbrella, nodeSessionStatus_1.NodeSessionProbeSupport, SessionMetricsService_1.SessionMetricsService, NodeMetricsStore_1.NodeMetricsStore, nodeMetrics_1.NodeMetricsSupport);
|
|
127
|
+
beforeEach(() => {
|
|
167
128
|
nodeMetrics = new NodeSideMetrics();
|
|
168
129
|
typedi_1.Container.set(SessionMetricsService_1.SessionMetricsService, nodeMetrics);
|
|
169
130
|
typedi_1.Container.set(NodeMetricsStore_1.NodeMetricsStore, new NodeMetricsStore_1.NodeMetricsStore());
|
|
170
131
|
typedi_1.Container.set(nodeMetrics_1.NodeMetricsSupport, new nodeMetrics_1.NodeMetricsSupport());
|
|
171
|
-
//
|
|
172
|
-
typedi_1.Container.set(
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
typedi_1.Container.set(nodeSessionStatus_1.NodeSessionProbeSupport, new nodeSessionStatus_1.NodeSessionProbeSupport());
|
|
176
|
-
typedi_1.Container.set(device_managers_1.XenonManager, {
|
|
177
|
-
getMaxSessionCount: () => undefined,
|
|
178
|
-
deviceInstances: () => __awaiter(this, void 0, void 0, function* () { return []; }),
|
|
179
|
-
});
|
|
180
|
-
const context = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
181
|
-
saved = {
|
|
182
|
-
store: device_store_1.DeviceStoreFactory._deviceStore,
|
|
183
|
-
pending: device_store_1.DeviceStoreFactory._pendingSessionStore,
|
|
184
|
-
context: Object.assign({}, context),
|
|
185
|
-
};
|
|
186
|
-
device_store_1.DeviceStoreFactory._deviceStore = new prisma_store_1.PrismaDeviceStore();
|
|
187
|
-
device_store_1.DeviceStoreFactory._pendingSessionStore = new prisma_store_1.PrismaPendingSessionStore();
|
|
188
|
-
authDisabledBefore = config_1.config.authDisabled;
|
|
189
|
-
config_1.config.authDisabled = false;
|
|
190
|
-
// Per-command auth on: the node then takes the hub's token and nothing else.
|
|
191
|
-
commandAuthBefore = process.env.XENON_REQUIRE_COMMAND_AUTH;
|
|
192
|
-
process.env.XENON_REQUIRE_COMMAND_AUTH = '1';
|
|
193
|
-
yield scratch.db.pendingSession.deleteMany({});
|
|
194
|
-
yield scratch.db.device.deleteMany({});
|
|
195
|
-
}));
|
|
196
|
-
afterEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
197
|
-
for (const s of SessionManager_1.SESSION_MANAGER.getAllSessions())
|
|
198
|
-
SessionManager_1.SESSION_MANAGER.removeSession(s.getId());
|
|
199
|
-
device_store_1.DeviceStoreFactory._deviceStore = saved.store;
|
|
200
|
-
device_store_1.DeviceStoreFactory._pendingSessionStore = saved.pending;
|
|
201
|
-
Object.assign(typedi_1.Container.get(PluginContext_1.PluginContext), saved.context);
|
|
202
|
-
config_1.config.authDisabled = authDisabledBefore;
|
|
203
|
-
if (commandAuthBefore === undefined)
|
|
204
|
-
delete process.env.XENON_REQUIRE_COMMAND_AUTH;
|
|
205
|
-
else
|
|
206
|
-
process.env.XENON_REQUIRE_COMMAND_AUTH = commandAuthBefore;
|
|
207
|
-
sinon_1.default.restore();
|
|
208
|
-
restore();
|
|
209
|
-
for (const s of servers.splice(0)) {
|
|
210
|
-
s.closeAllConnections();
|
|
211
|
-
yield new Promise((resolve) => http_1.default.Server.prototype.close.call(s, () => resolve()));
|
|
212
|
-
}
|
|
213
|
-
}));
|
|
214
|
-
const port = (s) => s.address().port;
|
|
215
|
-
/** A hub (its public keys) and a node. */
|
|
216
|
-
function boot() {
|
|
217
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
218
|
-
const baseDriver = (0, appium_umbrella_1.appiumBaseDriver)();
|
|
219
|
-
const hubServer = yield baseDriver.server({
|
|
220
|
-
routeConfiguringFunction: baseDriver.routeConfiguringFunction((0, appium_umbrella_1.umbrellaWith)([])),
|
|
221
|
-
port: 0,
|
|
222
|
-
hostname: '127.0.0.1',
|
|
223
|
-
basePath: '/wd/hub',
|
|
224
|
-
cliArgs: { basePath: '/wd/hub' },
|
|
225
|
-
serverUpdaters: [
|
|
226
|
-
(app) => {
|
|
227
|
-
app.get('/xenon/api/auth/jwks.json', (_req, res) => res.json(hubKeys.jwks()));
|
|
228
|
-
},
|
|
229
|
-
],
|
|
230
|
-
});
|
|
231
|
-
servers.push(hubServer);
|
|
232
|
-
hubUrl = `http://127.0.0.1:${port(hubServer)}`;
|
|
233
|
-
const pluginArgs = Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { hub: hubUrl, bindHostOrIp: '127.0.0.1', enableDashboard: false, deviceAvailabilityTimeoutMs: 1000, deviceAvailabilityQueryIntervalMs: 100 });
|
|
234
|
-
umbrella = (0, appium_umbrella_1.umbrellaWith)([[plugin_1.XenonPlugin, 'xenon']]);
|
|
235
|
-
const nodeServer = yield baseDriver.server({
|
|
236
|
-
routeConfiguringFunction: baseDriver.routeConfiguringFunction(umbrella),
|
|
237
|
-
port: 0,
|
|
238
|
-
hostname: '127.0.0.1',
|
|
239
|
-
basePath: '/node',
|
|
240
|
-
cliArgs: { basePath: '/node' },
|
|
241
|
-
serverUpdaters: [
|
|
242
|
-
(app) => {
|
|
243
|
-
(0, registerCommandAuth_1.registerSessionGateway)(app, { basePath: '/node' }, (0, registerCommandAuth_1.commandAuthDeps)({ enabled: () => true, authDisabled: () => false, logger: quiet }), (0, defaultGateway_1.nodeGatewayOptions)(hubUrl));
|
|
244
|
-
// What ServerManager mounts under /xenon/api on a node.
|
|
245
|
-
const api = express_1.default.Router();
|
|
246
|
-
(0, nodeSessionStatus_1.registerNodeSessionStatus)(api, pluginArgs);
|
|
247
|
-
(0, nodeSessionMetrics_1.registerNodeSessionMetrics)(api, pluginArgs);
|
|
248
|
-
app.use('/xenon/api', api);
|
|
249
|
-
},
|
|
250
|
-
],
|
|
251
|
-
});
|
|
252
|
-
servers.push(nodeServer);
|
|
253
|
-
nodeOrigin = `http://127.0.0.1:${port(nodeServer)}`;
|
|
254
|
-
typedi_1.Container.get(PluginContext_1.PluginContext).setContext(pluginArgs, port(nodeServer), NODE_ID, '/node');
|
|
255
|
-
yield scratch.db.device.create({
|
|
256
|
-
data: {
|
|
257
|
-
udid: 'phone-1',
|
|
258
|
-
host: nodeOrigin,
|
|
259
|
-
nodeId: NODE_ID,
|
|
260
|
-
platform: 'android',
|
|
261
|
-
name: 'phone-1',
|
|
262
|
-
sdk: '14',
|
|
263
|
-
busy: false,
|
|
264
|
-
userBlocked: false,
|
|
265
|
-
offline: false,
|
|
266
|
-
},
|
|
267
|
-
});
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
/** A session on the node, created as the hub creates it. */
|
|
271
|
-
function nodeSession() {
|
|
272
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
273
|
-
const token = yield typedi_1.Container.get(hubSessionToken_1.HubSessionTokenIssuer).createTokenFor({
|
|
274
|
-
userId: 'alice',
|
|
275
|
-
udid: 'phone-1',
|
|
276
|
-
host: nodeOrigin,
|
|
277
|
-
});
|
|
278
|
-
const res = yield (0, loopbackRequest_1.default)(nodeOrigin)
|
|
279
|
-
.post('/node/session')
|
|
280
|
-
.set(hubSessionToken_1.HUB_TOKEN_HEADER, token)
|
|
281
|
-
.send({
|
|
282
|
-
capabilities: {
|
|
283
|
-
alwaysMatch: {
|
|
284
|
-
platformName: 'Android',
|
|
285
|
-
'appium:automationName': appium_umbrella_1.FAKE_AUTOMATION,
|
|
286
|
-
'appium:newCommandTimeout': 0,
|
|
287
|
-
'appium:udid': 'phone-1',
|
|
288
|
-
'xe:options': { recordVideo: false },
|
|
289
|
-
},
|
|
290
|
-
firstMatch: [{}],
|
|
291
|
-
},
|
|
292
|
-
});
|
|
293
|
-
(0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
|
|
294
|
-
return res.body.value.sessionId;
|
|
132
|
+
// The node records the session's device log too; not here.
|
|
133
|
+
typedi_1.Container.set(SessionDeviceLogs_1.SessionDeviceLogs, {
|
|
134
|
+
start: () => __awaiter(this, void 0, void 0, function* () { return undefined; }),
|
|
135
|
+
stop: () => __awaiter(this, void 0, void 0, function* () { return undefined; }),
|
|
295
136
|
});
|
|
296
|
-
}
|
|
297
|
-
/** The hub's object for that session. */
|
|
298
|
-
const hubSide = (sessionId) => new RemoteSession_1.RemoteSession({
|
|
299
|
-
sessionId,
|
|
300
|
-
device: { udid: 'phone-1', host: nodeOrigin, nodeId: NODE_ID, platform: 'android' },
|
|
301
|
-
sessionResponse: {},
|
|
302
|
-
xenonOption: {},
|
|
303
|
-
baseUrl: `${nodeOrigin}/node`,
|
|
304
137
|
});
|
|
138
|
+
const boot = () => pair.boot();
|
|
139
|
+
const nodeSession = () => pair.nodeSession();
|
|
140
|
+
const hubSide = (sessionId) => pair.hubSide(sessionId);
|
|
305
141
|
it("the hub collects a node session's figures as it runs, and the last ones when it ends", () => __awaiter(this, void 0, void 0, function* () {
|
|
306
142
|
yield boot();
|
|
307
143
|
const sessionId = yield nodeSession();
|
|
@@ -312,8 +148,8 @@ describe('a hub collects a node session’s CPU and memory', function () {
|
|
|
312
148
|
const hub = new HubSideMetrics();
|
|
313
149
|
const device = {
|
|
314
150
|
udid: 'phone-1',
|
|
315
|
-
host: nodeOrigin,
|
|
316
|
-
nodeId: NODE_ID,
|
|
151
|
+
host: pair.nodeOrigin,
|
|
152
|
+
nodeId: hub_node_pair_1.NODE_ID,
|
|
317
153
|
platform: 'android',
|
|
318
154
|
realDevice: true,
|
|
319
155
|
};
|
|
@@ -323,11 +159,7 @@ describe('a hub collects a node session’s CPU and memory', function () {
|
|
|
323
159
|
yield until(() => hub.written.length === 0 &&
|
|
324
160
|
typedi_1.Container.get(NodeMetricsStore_1.NodeMetricsStore).read(sessionId, null).samples.length === 0);
|
|
325
161
|
nodeMetrics.hooks[0].onSample(at(3000));
|
|
326
|
-
|
|
327
|
-
const del = yield (0, loopbackRequest_1.default)(nodeOrigin)
|
|
328
|
-
.delete(`/node/session/${sessionId}`)
|
|
329
|
-
.set(hubSessionToken_1.HUB_TOKEN_HEADER, token);
|
|
330
|
-
(0, chai_1.expect)(del.status, JSON.stringify(del.body)).to.equal(200);
|
|
162
|
+
yield pair.deleteOnNode(sessionId);
|
|
331
163
|
(0, chai_1.expect)(nodeMetrics.stopped).to.equal(1);
|
|
332
164
|
(0, chai_1.expect)(typedi_1.Container.get(NodeMetricsStore_1.NodeMetricsStore).read(sessionId, 2000).state).to.equal('ended');
|
|
333
165
|
yield hub.stop(sessionId);
|
|
@@ -347,7 +179,7 @@ describe('a hub collects a node session’s CPU and memory', function () {
|
|
|
347
179
|
device_version: '14',
|
|
348
180
|
desired_capabilities: '{}',
|
|
349
181
|
session_capabilities: '{}',
|
|
350
|
-
node_id: NODE_ID,
|
|
182
|
+
node_id: hub_node_pair_1.NODE_ID,
|
|
351
183
|
has_live_video: false,
|
|
352
184
|
status: 'running',
|
|
353
185
|
},
|
|
@@ -355,8 +187,8 @@ describe('a hub collects a node session’s CPU and memory', function () {
|
|
|
355
187
|
const hub = new HubStoringMetrics();
|
|
356
188
|
const device = {
|
|
357
189
|
udid: 'phone-1',
|
|
358
|
-
host: nodeOrigin,
|
|
359
|
-
nodeId: NODE_ID,
|
|
190
|
+
host: pair.nodeOrigin,
|
|
191
|
+
nodeId: hub_node_pair_1.NODE_ID,
|
|
360
192
|
platform: 'android',
|
|
361
193
|
realDevice: true,
|
|
362
194
|
};
|
|
@@ -386,8 +218,8 @@ describe('a hub collects a node session’s CPU and memory', function () {
|
|
|
386
218
|
const hub = new HubSideMetrics();
|
|
387
219
|
const device = {
|
|
388
220
|
udid: 'phone-1',
|
|
389
|
-
host: nodeOrigin,
|
|
390
|
-
nodeId: NODE_ID,
|
|
221
|
+
host: pair.nodeOrigin,
|
|
222
|
+
nodeId: hub_node_pair_1.NODE_ID,
|
|
391
223
|
platform: 'android',
|
|
392
224
|
realDevice: true,
|
|
393
225
|
};
|
|
@@ -38,6 +38,7 @@ const RemoteSession_1 = require("../../src/sessions/RemoteSession");
|
|
|
38
38
|
const appiumUmbrella_1 = require("../../src/sessions/appiumUmbrella");
|
|
39
39
|
const XenonSession_1 = require("../../src/sessions/XenonSession");
|
|
40
40
|
const config_1 = require("../../src/config");
|
|
41
|
+
const SessionDeviceLogs_1 = require("../../src/services/logcat/SessionDeviceLogs");
|
|
41
42
|
const container_registration_1 = require("../helpers/container-registration");
|
|
42
43
|
const scratch_database_1 = require("../helpers/scratch-database");
|
|
43
44
|
const appium_umbrella_1 = require("../helpers/appium-umbrella");
|
|
@@ -80,12 +81,18 @@ describe('the hub’s heartbeat on a node’s session', function () {
|
|
|
80
81
|
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
81
82
|
});
|
|
82
83
|
beforeEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
83
|
-
restore = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, hubSessionToken_1.HubSessionTokenIssuer, device_managers_1.XenonManager, appiumUmbrella_1.AppiumUmbrella, nodeSessionStatus_1.NodeSessionProbeSupport);
|
|
84
|
+
restore = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, hubSessionToken_1.HubSessionTokenIssuer, device_managers_1.XenonManager, appiumUmbrella_1.AppiumUmbrella, nodeSessionStatus_1.NodeSessionProbeSupport, SessionDeviceLogs_1.SessionDeviceLogs);
|
|
84
85
|
// One process plays both sides: the hub's keys sign, the node checks them.
|
|
85
86
|
typedi_1.Container.set(JwtKeyService_1.JwtKeyService, hubKeys);
|
|
86
87
|
typedi_1.Container.set(hubSessionToken_1.HubSessionTokenIssuer, new hubSessionToken_1.HubSessionTokenIssuer());
|
|
87
88
|
typedi_1.Container.set(appiumUmbrella_1.AppiumUmbrella, new appiumUmbrella_1.AppiumUmbrella());
|
|
88
89
|
typedi_1.Container.set(nodeSessionStatus_1.NodeSessionProbeSupport, new nodeSessionStatus_1.NodeSessionProbeSupport());
|
|
90
|
+
// A node records the device log of each session its hub creates; its
|
|
91
|
+
// phone here is a row, so nothing may reach this machine's adb.
|
|
92
|
+
typedi_1.Container.set(SessionDeviceLogs_1.SessionDeviceLogs, {
|
|
93
|
+
start: () => __awaiter(this, void 0, void 0, function* () { return undefined; }),
|
|
94
|
+
stop: () => __awaiter(this, void 0, void 0, function* () { return undefined; }),
|
|
95
|
+
});
|
|
89
96
|
typedi_1.Container.set(device_managers_1.XenonManager, {
|
|
90
97
|
getMaxSessionCount: () => undefined,
|
|
91
98
|
deviceInstances: () => __awaiter(this, void 0, void 0, function* () { return []; }),
|
|
@@ -46,6 +46,9 @@ const chai_1 = require("chai");
|
|
|
46
46
|
const fs = __importStar(require("fs"));
|
|
47
47
|
const path = __importStar(require("path"));
|
|
48
48
|
const os = __importStar(require("os"));
|
|
49
|
+
const child_process_1 = require("child_process");
|
|
50
|
+
const crypto_1 = require("crypto");
|
|
51
|
+
const sinon = __importStar(require("sinon"));
|
|
49
52
|
const node_forge_1 = require("node-forge");
|
|
50
53
|
const CertManager_1 = require("../../../src/services/interceptor/CertManager");
|
|
51
54
|
describe('CertManager', () => {
|
|
@@ -98,4 +101,90 @@ describe('CertManager', () => {
|
|
|
98
101
|
(0, chai_1.expect)(fname).to.match(/^[0-9a-f]{8}\.0$/);
|
|
99
102
|
(0, chai_1.expect)(typeof ca.subjectHash).to.equal('string');
|
|
100
103
|
}));
|
|
104
|
+
describe('serial number', () => {
|
|
105
|
+
// The serial was Date.now() in hex padded to 16 digits: always 00 00 01 ...,
|
|
106
|
+
// an INTEGER with illegal leading zeros. OpenSSL 3 (Node's included), Go
|
|
107
|
+
// and BoringSSL refuse such a certificate outright.
|
|
108
|
+
it('is a CA OpenSSL can load', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
109
|
+
const ca = yield new CertManager_1.CertManager(dir).ensure();
|
|
110
|
+
(0, chai_1.expect)(() => new crypto_1.X509Certificate(fs.readFileSync(ca.certPath))).to.not.throw();
|
|
111
|
+
}));
|
|
112
|
+
it('is positive, minimally encoded and at least 64 random bits', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
113
|
+
const ca = yield new CertManager_1.CertManager(dir).ensure();
|
|
114
|
+
const serial = new crypto_1.X509Certificate(fs.readFileSync(ca.certPath)).serialNumber;
|
|
115
|
+
const first = parseInt(serial.slice(0, 2), 16);
|
|
116
|
+
(0, chai_1.expect)(first, 'no leading zero byte').to.be.greaterThan(0);
|
|
117
|
+
(0, chai_1.expect)(first, 'not negative').to.be.lessThan(0x80);
|
|
118
|
+
(0, chai_1.expect)(serial.length / 2)
|
|
119
|
+
.to.be.at.least(8)
|
|
120
|
+
.and.at.most(20);
|
|
121
|
+
}));
|
|
122
|
+
it('differs between two CAs generated in the same millisecond', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
123
|
+
const other = fs.mkdtempSync(path.join(os.tmpdir(), 'xenon-cert-'));
|
|
124
|
+
const clock = sinon.useFakeTimers({ now: 1760000000000, toFake: ['Date'] });
|
|
125
|
+
try {
|
|
126
|
+
const a = yield new CertManager_1.CertManager(dir).ensure();
|
|
127
|
+
const b = yield new CertManager_1.CertManager(other).ensure();
|
|
128
|
+
(0, chai_1.expect)(new crypto_1.X509Certificate(fs.readFileSync(a.certPath)).serialNumber).to.not.equal(new crypto_1.X509Certificate(fs.readFileSync(b.certPath)).serialNumber);
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
clock.restore();
|
|
132
|
+
fs.rmSync(other, { recursive: true, force: true });
|
|
133
|
+
}
|
|
134
|
+
}));
|
|
135
|
+
it('replaces a CA an earlier version wrote with the bad serial', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
136
|
+
writeCa(dir, '000001a10d75af19');
|
|
137
|
+
const ca = yield new CertManager_1.CertManager(dir).ensure();
|
|
138
|
+
(0, chai_1.expect)(() => new crypto_1.X509Certificate(fs.readFileSync(ca.certPath))).to.not.throw();
|
|
139
|
+
}));
|
|
140
|
+
it('keeps a CA OpenSSL can load', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
141
|
+
writeCa(dir, '1a10d75af19a2b3c');
|
|
142
|
+
const before = fs.readFileSync(path.join(dir, 'certs', 'ca.pem'), 'utf8');
|
|
143
|
+
yield new CertManager_1.CertManager(dir).ensure();
|
|
144
|
+
(0, chai_1.expect)(fs.readFileSync(path.join(dir, 'certs', 'ca.pem'), 'utf8')).to.equal(before);
|
|
145
|
+
}));
|
|
146
|
+
});
|
|
147
|
+
describe('Android file name', () => {
|
|
148
|
+
// Android looks a system CA up by <subject_hash_old>.0. The name was
|
|
149
|
+
// openssl's, else a SHA-1 of the PEM, which names no certificate: with the
|
|
150
|
+
// bad serial openssl couldn't load the CA, so the fallback always ran.
|
|
151
|
+
it("is OpenSSL's subject_hash_old, with no openssl binary on the PATH", function () {
|
|
152
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
153
|
+
try {
|
|
154
|
+
(0, child_process_1.execFileSync)('openssl', ['version'], { stdio: 'ignore' });
|
|
155
|
+
}
|
|
156
|
+
catch (_a) {
|
|
157
|
+
this.skip();
|
|
158
|
+
}
|
|
159
|
+
const ca = yield new CertManager_1.CertManager(dir).ensure();
|
|
160
|
+
const expected = (0, child_process_1.execFileSync)('openssl', ['x509', '-inform', 'PEM', '-subject_hash_old', '-noout', '-in', ca.certPath], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
161
|
+
(0, chai_1.expect)(ca.subjectHash).to.equal(expected);
|
|
162
|
+
const pathBefore = process.env.PATH;
|
|
163
|
+
process.env.PATH = '';
|
|
164
|
+
try {
|
|
165
|
+
(0, chai_1.expect)(new CertManager_1.CertManager(dir).androidCertFilename()).to.equal(`${expected}.0`);
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
process.env.PATH = pathBefore;
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
});
|
|
101
173
|
});
|
|
174
|
+
function writeCa(dir, serialHex) {
|
|
175
|
+
fs.mkdirSync(path.join(dir, 'certs'), { recursive: true });
|
|
176
|
+
fs.mkdirSync(path.join(dir, 'keys'), { recursive: true });
|
|
177
|
+
const keys = node_forge_1.pki.rsa.generateKeyPair(1024);
|
|
178
|
+
const cert = node_forge_1.pki.createCertificate();
|
|
179
|
+
cert.publicKey = keys.publicKey;
|
|
180
|
+
cert.serialNumber = serialHex;
|
|
181
|
+
cert.validity.notBefore = new Date(Date.now() - 60000);
|
|
182
|
+
cert.validity.notAfter = new Date(Date.now() + 365 * 24 * 3600000);
|
|
183
|
+
const attrs = [{ name: 'commonName', value: 'Xenon MITM Root CA' }];
|
|
184
|
+
cert.setSubject(attrs);
|
|
185
|
+
cert.setIssuer(attrs);
|
|
186
|
+
cert.setExtensions([{ name: 'basicConstraints', cA: true, critical: true }]);
|
|
187
|
+
cert.sign(keys.privateKey, node_forge_1.md.sha256.create());
|
|
188
|
+
fs.writeFileSync(path.join(dir, 'certs', 'ca.pem'), node_forge_1.pki.certificateToPem(cert), 'utf8');
|
|
189
|
+
fs.writeFileSync(path.join(dir, 'keys', 'ca.private.key'), node_forge_1.pki.privateKeyToPem(keys.privateKey));
|
|
190
|
+
}
|