@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
|
@@ -18,7 +18,9 @@ const typedi_1 = require("typedi");
|
|
|
18
18
|
const logger_1 = __importDefault(require("../logger"));
|
|
19
19
|
const SessionType_1 = __importDefault(require("../enums/SessionType"));
|
|
20
20
|
const hubSessionToken_1 = require("../gateway/hubSessionToken");
|
|
21
|
+
const outboundProxy_1 = require("../helpers/outboundProxy");
|
|
21
22
|
const nodeMetrics_1 = require("../services/metrics/nodeMetrics");
|
|
23
|
+
const nodeDeviceLogs_1 = require("../services/logcat/nodeDeviceLogs");
|
|
22
24
|
const nodeSessionStatus_1 = require("../gateway/nodeSessionStatus");
|
|
23
25
|
const XenonSession_1 = require("./XenonSession");
|
|
24
26
|
class RemoteSession extends XenonSession_1.XenonSession {
|
|
@@ -52,7 +54,8 @@ class RemoteSession extends XenonSession_1.XenonSession {
|
|
|
52
54
|
return __awaiter(this, void 0, void 0, function* () {
|
|
53
55
|
var _a, _b;
|
|
54
56
|
const options = yield this.callOptions();
|
|
55
|
-
|
|
57
|
+
const request = Object.assign(Object.assign({}, config), options);
|
|
58
|
+
return (0, axios_1.default)(Object.assign(Object.assign(Object.assign({}, (request.proxy === undefined ? proxyFor(config.url) : {})), request), { headers: Object.assign(Object.assign({}, ((_a = config.headers) !== null && _a !== void 0 ? _a : {})), ((_b = options.headers) !== null && _b !== void 0 ? _b : {})) }));
|
|
56
59
|
});
|
|
57
60
|
}
|
|
58
61
|
getScreenShot() {
|
|
@@ -233,6 +236,14 @@ class RemoteSession extends XenonSession_1.XenonSession {
|
|
|
233
236
|
}
|
|
234
237
|
/** The node's CPU and memory for this session newer than `after` (NodeMetricsCollector). */
|
|
235
238
|
nodeMetrics(after) {
|
|
239
|
+
return this.askNode('metrics', after, nodeMetrics_1.readNodeMetricsReply);
|
|
240
|
+
}
|
|
241
|
+
/** The node's device log lines for this session after line `after` (NodeDeviceLogsCollector). */
|
|
242
|
+
nodeDeviceLogs(after) {
|
|
243
|
+
return this.askNode('device-logs', after, nodeDeviceLogs_1.readNodeDeviceLogsReply);
|
|
244
|
+
}
|
|
245
|
+
/** One of the node's routes about this session, `/xenon/api/node/sessions/<id>/<what>?after=`. */
|
|
246
|
+
askNode(what, after, read) {
|
|
236
247
|
return __awaiter(this, void 0, void 0, function* () {
|
|
237
248
|
var _a, _b, _c;
|
|
238
249
|
const origin = this.nodeOrigin();
|
|
@@ -241,12 +252,12 @@ class RemoteSession extends XenonSession_1.XenonSession {
|
|
|
241
252
|
try {
|
|
242
253
|
const response = yield this.call({
|
|
243
254
|
method: 'get',
|
|
244
|
-
url: `${origin}${nodeSessionStatus_1.NODE_SESSION_STATUS_PATH}/${encodeURIComponent(this.sessionId)}
|
|
255
|
+
url: `${origin}${nodeSessionStatus_1.NODE_SESSION_STATUS_PATH}/${encodeURIComponent(this.sessionId)}/${what}`,
|
|
245
256
|
params: after === null ? undefined : { after },
|
|
246
257
|
timeout: 5000,
|
|
247
258
|
validateStatus: () => true,
|
|
248
259
|
});
|
|
249
|
-
return (
|
|
260
|
+
return read(response.status, (_a = response.headers) !== null && _a !== void 0 ? _a : {}, response.data);
|
|
250
261
|
}
|
|
251
262
|
catch (err) {
|
|
252
263
|
return { kind: 'unavailable', reason: String((_c = (_b = err === null || err === void 0 ? void 0 : err.code) !== null && _b !== void 0 ? _b : err === null || err === void 0 ? void 0 : err.message) !== null && _c !== void 0 ? _c : err) };
|
|
@@ -392,7 +403,7 @@ class RemoteSession extends XenonSession_1.XenonSession {
|
|
|
392
403
|
logger_1.default.warn(`[RemoteSession] Session ${this.sessionId} probe returned 404. Performing server status fallback...`);
|
|
393
404
|
// Fallback: Verify Appium server is alive
|
|
394
405
|
try {
|
|
395
|
-
const statusRes = yield axios_1.default.get(`${appiumUrl}/status`, { timeout: 3000 });
|
|
406
|
+
const statusRes = yield axios_1.default.get(`${appiumUrl}/status`, Object.assign({ timeout: 3000 }, proxyFor(`${appiumUrl}/status`)));
|
|
396
407
|
if (statusRes.status === 200) {
|
|
397
408
|
// If server is alive but we got a raw 404 with no specific error,
|
|
398
409
|
// it's likely the session is gone in Appium 2 (where /sessions list check is unsupported).
|
|
@@ -438,3 +449,11 @@ class RemoteSession extends XenonSession_1.XenonSession {
|
|
|
438
449
|
}
|
|
439
450
|
}
|
|
440
451
|
exports.RemoteSession = RemoteSession;
|
|
452
|
+
function proxyFor(url) {
|
|
453
|
+
try {
|
|
454
|
+
return url ? (0, outboundProxy_1.axiosProxyConfig)(url) : {};
|
|
455
|
+
}
|
|
456
|
+
catch (_a) {
|
|
457
|
+
return {};
|
|
458
|
+
}
|
|
459
|
+
}
|
|
@@ -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.SESSION_MANAGER = exports.SessionManager =
|
|
54
|
+
exports.SESSION_MANAGER = exports.SessionManager = void 0;
|
|
55
55
|
const os_1 = __importDefault(require("os"));
|
|
56
56
|
const typedi_1 = require("typedi");
|
|
57
57
|
const prisma_1 = require("../prisma");
|
|
@@ -59,15 +59,14 @@ const logger_1 = __importDefault(require("../logger"));
|
|
|
59
59
|
const RemoteSession_1 = require("./RemoteSession");
|
|
60
60
|
const CloudSession_1 = require("./CloudSession");
|
|
61
61
|
const device_store_1 = require("../data-service/device-store");
|
|
62
|
+
const PluginContext_1 = require("../PluginContext");
|
|
62
63
|
const nodeWebDriverUrl_1 = require("../gateway/nodeWebDriverUrl");
|
|
63
64
|
const XenonCapabilityManager_1 = require("../XenonCapabilityManager");
|
|
64
65
|
const nodeMetrics_1 = require("../services/metrics/nodeMetrics");
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
*/
|
|
70
|
-
exports.HUB_RESTART_CATEGORY = 'HUB_RESTART';
|
|
66
|
+
const nodeDeviceLogs_1 = require("../services/logcat/nodeDeviceLogs");
|
|
67
|
+
// A session still running at boot that can't be picked up again is filed
|
|
68
|
+
// under it; the failure analysis also writes it for a shutdown's drain.
|
|
69
|
+
const failureCategories_1 = require("../dashboard/services/failureCategories");
|
|
71
70
|
/**
|
|
72
71
|
* SessionManager with persistence and recovery capabilities.
|
|
73
72
|
*
|
|
@@ -124,7 +123,7 @@ let SessionManager = class SessionManager {
|
|
|
124
123
|
*/
|
|
125
124
|
recoverActiveSessions(currentNodeId_1, nodeBasePath_1) {
|
|
126
125
|
return __awaiter(this, arguments, void 0, function* (currentNodeId, nodeBasePath, isLocalHost = () => false) {
|
|
127
|
-
var _a, _b;
|
|
126
|
+
var _a, _b, _c;
|
|
128
127
|
this.log.info('🔄 Attempting to recover active sessions from database...');
|
|
129
128
|
try {
|
|
130
129
|
// Find all sessions that are still "running" (not ended)
|
|
@@ -228,6 +227,13 @@ let SessionManager = class SessionManager {
|
|
|
228
227
|
if (!device.cloud && dbSession.is_profiling_available) {
|
|
229
228
|
yield this.resumeNodeMetrics(dbSession.id, device, recoveredSession, sessionResponse);
|
|
230
229
|
}
|
|
230
|
+
// Its device log too, where the hub records sessions (the dashboard
|
|
231
|
+
// on) and the session didn't turn it off, after the newest line stored.
|
|
232
|
+
if (!device.cloud &&
|
|
233
|
+
((_c = typedi_2.Container.get(PluginContext_1.PluginContext).pluginArgs) === null || _c === void 0 ? void 0 : _c.enableDashboard) &&
|
|
234
|
+
xenonCapabilities[XenonCapabilityManager_1.XENON_CAPABILITIES.SAVE_DEVICE_LOGS] !== false) {
|
|
235
|
+
yield this.resumeNodeDeviceLogs(dbSession.id, device, recoveredSession);
|
|
236
|
+
}
|
|
231
237
|
recoveredCount++;
|
|
232
238
|
}
|
|
233
239
|
catch (sessionErr) {
|
|
@@ -269,6 +275,24 @@ let SessionManager = class SessionManager {
|
|
|
269
275
|
}
|
|
270
276
|
});
|
|
271
277
|
}
|
|
278
|
+
/** Goes on collecting a node session's device log after a hub restart. */
|
|
279
|
+
resumeNodeDeviceLogs(sessionId, device, session) {
|
|
280
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
281
|
+
try {
|
|
282
|
+
// Loaded here: SessionDeviceLogs imports the device managers.
|
|
283
|
+
const { SessionDeviceLogs } = yield Promise.resolve().then(() => __importStar(require('../services/logcat/SessionDeviceLogs')));
|
|
284
|
+
void typedi_2.Container.get(SessionDeviceLogs).start({
|
|
285
|
+
sessionId,
|
|
286
|
+
device,
|
|
287
|
+
source: (0, nodeDeviceLogs_1.nodeDeviceLogsSourceOf)(session),
|
|
288
|
+
resume: true,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
this.log.warn(`Session ${sessionId}: device log not resumed: ${err.message}`);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
}
|
|
272
296
|
/**
|
|
273
297
|
* A recovered session is this process's from now on: its row's heartbeat
|
|
274
298
|
* is stamped with this process, now. The row still carried the heartbeat
|
|
@@ -312,7 +336,7 @@ let SessionManager = class SessionManager {
|
|
|
312
336
|
status: 'failed',
|
|
313
337
|
endTime: new Date(),
|
|
314
338
|
failure_reason: reason,
|
|
315
|
-
failure_category:
|
|
339
|
+
failure_category: failureCategories_1.HUB_RESTART_CATEGORY,
|
|
316
340
|
},
|
|
317
341
|
});
|
|
318
342
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.TestDriverLog = void 0;
|
|
13
|
+
const line_consuming_log_1 = require("appium-xcuitest-driver/build/lib/device/log/line-consuming-log");
|
|
14
|
+
const quietLog = { debug() { }, info() { }, warn() { }, error() { } };
|
|
15
|
+
/**
|
|
16
|
+
* The XCUITest driver's own log class, as an iPhone's (`IOSDeviceLog`) and a
|
|
17
|
+
* simulator's (`IOSSimulatorLog`) `logs.syslog` extend it, with `line()` in
|
|
18
|
+
* place of a device: what Xenon reads is what the driver keeps.
|
|
19
|
+
*/
|
|
20
|
+
class TestDriverLog extends line_consuming_log_1.LineConsumingLog {
|
|
21
|
+
constructor() {
|
|
22
|
+
super({ log: quietLog });
|
|
23
|
+
}
|
|
24
|
+
startCapture() {
|
|
25
|
+
return __awaiter(this, void 0, void 0, function* () { });
|
|
26
|
+
}
|
|
27
|
+
stopCapture() {
|
|
28
|
+
return __awaiter(this, void 0, void 0, function* () { });
|
|
29
|
+
}
|
|
30
|
+
get isCapturing() {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
/** A line from the device, as the driver's capture hands it on. */
|
|
34
|
+
line(message) {
|
|
35
|
+
this.broadcast(message);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
exports.TestDriverLog = TestDriverLog;
|
|
@@ -0,0 +1,106 @@
|
|
|
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
|
+
exports.startFakeProxy = startFakeProxy;
|
|
16
|
+
const http_1 = __importDefault(require("http"));
|
|
17
|
+
const net_1 = __importDefault(require("net"));
|
|
18
|
+
/**
|
|
19
|
+
* An HTTP proxy on 127.0.0.1 that forwards what it is sent and notes it: a
|
|
20
|
+
* request in absolute form (http-proxy-agent, axios) is passed on to its
|
|
21
|
+
* target, a CONNECT (https-proxy-agent, so WebSockets) becomes a tunnel, or
|
|
22
|
+
* is refused with `refuseConnect`. A spec can then tell a call that went
|
|
23
|
+
* through the proxy from one that went straight to the target.
|
|
24
|
+
*/
|
|
25
|
+
function startFakeProxy() {
|
|
26
|
+
return __awaiter(this, arguments, void 0, function* (opts = {}) {
|
|
27
|
+
const seen = [];
|
|
28
|
+
const authorizations = [];
|
|
29
|
+
const sockets = new Set();
|
|
30
|
+
const resolve = (host) => { var _a, _b; return (_b = (_a = opts.hosts) === null || _a === void 0 ? void 0 : _a[host]) !== null && _b !== void 0 ? _b : host; };
|
|
31
|
+
const server = http_1.default.createServer((req, res) => {
|
|
32
|
+
var _a, _b;
|
|
33
|
+
seen.push(`${req.method} ${req.url}`);
|
|
34
|
+
authorizations.push(String((_a = req.headers['proxy-authorization']) !== null && _a !== void 0 ? _a : ''));
|
|
35
|
+
let target;
|
|
36
|
+
try {
|
|
37
|
+
target = new URL((_b = req.url) !== null && _b !== void 0 ? _b : '');
|
|
38
|
+
}
|
|
39
|
+
catch (_c) {
|
|
40
|
+
res.writeHead(400).end();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const upstream = http_1.default.request(Object.assign(Object.assign({}, urlParts(target)), { hostname: resolve(target.hostname), method: req.method, headers: req.headers }), (answer) => {
|
|
44
|
+
var _a;
|
|
45
|
+
res.writeHead((_a = answer.statusCode) !== null && _a !== void 0 ? _a : 502, answer.headers);
|
|
46
|
+
answer.pipe(res);
|
|
47
|
+
});
|
|
48
|
+
upstream.on('error', () => res.writeHead(502).end());
|
|
49
|
+
req.pipe(upstream);
|
|
50
|
+
});
|
|
51
|
+
server.on('connect', (req, client, head) => {
|
|
52
|
+
var _a;
|
|
53
|
+
seen.push(`CONNECT ${req.url}`);
|
|
54
|
+
authorizations.push(String((_a = req.headers['proxy-authorization']) !== null && _a !== void 0 ? _a : ''));
|
|
55
|
+
if (opts.ignoreConnect) {
|
|
56
|
+
sockets.add(client);
|
|
57
|
+
client.on('error', () => client.destroy());
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (opts.refuseConnect) {
|
|
61
|
+
client.end('HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\n\r\n');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const [host, port] = String(req.url).split(':');
|
|
65
|
+
const upstream = net_1.default.connect(Number(port), resolve(host), () => {
|
|
66
|
+
client.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
|
67
|
+
if (head.length)
|
|
68
|
+
upstream.write(head);
|
|
69
|
+
upstream.pipe(client);
|
|
70
|
+
client.pipe(upstream);
|
|
71
|
+
});
|
|
72
|
+
sockets.add(upstream).add(client);
|
|
73
|
+
const drop = () => {
|
|
74
|
+
upstream.destroy();
|
|
75
|
+
client.destroy();
|
|
76
|
+
};
|
|
77
|
+
upstream.on('error', drop);
|
|
78
|
+
client.on('error', drop);
|
|
79
|
+
});
|
|
80
|
+
yield new Promise((resolve, reject) => {
|
|
81
|
+
server.once('error', reject);
|
|
82
|
+
server.listen(0, '127.0.0.1', () => resolve());
|
|
83
|
+
});
|
|
84
|
+
const { port } = server.address();
|
|
85
|
+
return {
|
|
86
|
+
url: `http://127.0.0.1:${port}`,
|
|
87
|
+
seen,
|
|
88
|
+
authorizations,
|
|
89
|
+
close: () => new Promise((resolve) => {
|
|
90
|
+
var _a;
|
|
91
|
+
for (const socket of sockets)
|
|
92
|
+
socket.destroy();
|
|
93
|
+
(_a = server.closeAllConnections) === null || _a === void 0 ? void 0 : _a.call(server);
|
|
94
|
+
server.close(() => resolve());
|
|
95
|
+
}),
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function urlParts(url) {
|
|
100
|
+
return {
|
|
101
|
+
protocol: url.protocol,
|
|
102
|
+
hostname: url.hostname,
|
|
103
|
+
port: url.port,
|
|
104
|
+
path: `${url.pathname}${url.search}`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.until = exports.settle = exports.FakeLogcatStreams = void 0;
|
|
13
|
+
exports.lineAt = lineAt;
|
|
14
|
+
exports.fakeProc = fakeProc;
|
|
15
|
+
const stream_1 = require("stream");
|
|
16
|
+
const LogcatStreamService_1 = require("../../src/device-managers/android/LogcatStreamService");
|
|
17
|
+
const PackageResolver_1 = require("../../src/services/logcat/PackageResolver");
|
|
18
|
+
const pad = (n, w = 2) => String(n).padStart(w, '0');
|
|
19
|
+
/** A threadtime line logged at `at` by this machine's local clock. */
|
|
20
|
+
function lineAt(at, msg, level = 'I', tag = 'Tag', pid = 4127) {
|
|
21
|
+
const t = new Date(at);
|
|
22
|
+
return (`${pad(t.getMonth() + 1)}-${pad(t.getDate())} ${pad(t.getHours())}:${pad(t.getMinutes())}:` +
|
|
23
|
+
`${pad(t.getSeconds())}.${pad(t.getMilliseconds(), 3)} ${pid} ${pid} ${level} ${tag}: ${msg}\n`);
|
|
24
|
+
}
|
|
25
|
+
/** An `adb logcat` child whose output the test writes. */
|
|
26
|
+
function fakeProc() {
|
|
27
|
+
const stdout = new stream_1.PassThrough();
|
|
28
|
+
const handlers = {};
|
|
29
|
+
return {
|
|
30
|
+
stdout,
|
|
31
|
+
killed: false,
|
|
32
|
+
on(ev, cb) {
|
|
33
|
+
(handlers[ev] || (handlers[ev] = [])).push(cb);
|
|
34
|
+
},
|
|
35
|
+
/** The process ends, as when the phone restarts or is unplugged. */
|
|
36
|
+
exit() {
|
|
37
|
+
stdout.end();
|
|
38
|
+
(handlers.close || []).forEach((h) => h());
|
|
39
|
+
},
|
|
40
|
+
kill() {
|
|
41
|
+
this.killed = true;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** The real stream service, its `adb logcat` children fake. */
|
|
46
|
+
class FakeLogcatStreams extends LogcatStreamService_1.LogcatStreamService {
|
|
47
|
+
constructor() {
|
|
48
|
+
super(...arguments);
|
|
49
|
+
this.procs = [];
|
|
50
|
+
}
|
|
51
|
+
spawnLogcat() {
|
|
52
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
53
|
+
const proc = fakeProc();
|
|
54
|
+
this.procs.push(proc);
|
|
55
|
+
return proc;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
makeResolver() {
|
|
59
|
+
return new PackageResolver_1.PackageResolver(() => __awaiter(this, void 0, void 0, function* () { return ' PID NAME\n 4127 com.example.app\n'; }));
|
|
60
|
+
}
|
|
61
|
+
get proc() {
|
|
62
|
+
return this.procs[this.procs.length - 1];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
exports.FakeLogcatStreams = FakeLogcatStreams;
|
|
66
|
+
/** Lets readline, the stream's package lookups and the mux deliver what was written. */
|
|
67
|
+
const settle = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
68
|
+
for (let i = 0; i < 5; i++)
|
|
69
|
+
yield new Promise((r) => setImmediate(r));
|
|
70
|
+
});
|
|
71
|
+
exports.settle = settle;
|
|
72
|
+
const until = (check_1, ...args_1) => __awaiter(void 0, [check_1, ...args_1], void 0, function* (check, ms = 2000) {
|
|
73
|
+
const end = Date.now() + ms;
|
|
74
|
+
while (!check()) {
|
|
75
|
+
if (Date.now() > end)
|
|
76
|
+
throw new Error('timed out');
|
|
77
|
+
yield new Promise((r) => setTimeout(r, 5));
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
exports.until = until;
|
|
@@ -0,0 +1,227 @@
|
|
|
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
|
+
exports.NODE_ID = void 0;
|
|
16
|
+
exports.useHubAndNode = useHubAndNode;
|
|
17
|
+
const chai_1 = require("chai");
|
|
18
|
+
const express_1 = __importDefault(require("express"));
|
|
19
|
+
const fs_1 = __importDefault(require("fs"));
|
|
20
|
+
const http_1 = __importDefault(require("http"));
|
|
21
|
+
const os_1 = __importDefault(require("os"));
|
|
22
|
+
const path_1 = __importDefault(require("path"));
|
|
23
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
24
|
+
const typedi_1 = require("typedi");
|
|
25
|
+
const loopbackRequest_1 = __importDefault(require("./loopbackRequest"));
|
|
26
|
+
const plugin_1 = require("../../src/plugin");
|
|
27
|
+
const registerCommandAuth_1 = require("../../src/app/registerCommandAuth");
|
|
28
|
+
const defaultGateway_1 = require("../../src/gateway/defaultGateway");
|
|
29
|
+
const hubSessionToken_1 = require("../../src/gateway/hubSessionToken");
|
|
30
|
+
const nodeSessionStatus_1 = require("../../src/gateway/nodeSessionStatus");
|
|
31
|
+
const nodeSessionMetrics_1 = require("../../src/gateway/nodeSessionMetrics");
|
|
32
|
+
const nodeSessionDeviceLogs_1 = require("../../src/gateway/nodeSessionDeviceLogs");
|
|
33
|
+
const JwtKeyService_1 = require("../../src/services/token/JwtKeyService");
|
|
34
|
+
const PluginContext_1 = require("../../src/PluginContext");
|
|
35
|
+
const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
|
|
36
|
+
const device_managers_1 = require("../../src/device-managers");
|
|
37
|
+
const device_store_1 = require("../../src/data-service/device-store");
|
|
38
|
+
const prisma_store_1 = require("../../src/data-service/prisma-store");
|
|
39
|
+
const SessionManager_1 = require("../../src/sessions/SessionManager");
|
|
40
|
+
const RemoteSession_1 = require("../../src/sessions/RemoteSession");
|
|
41
|
+
const appiumUmbrella_1 = require("../../src/sessions/appiumUmbrella");
|
|
42
|
+
const config_1 = require("../../src/config");
|
|
43
|
+
const container_registration_1 = require("./container-registration");
|
|
44
|
+
const appium_umbrella_1 = require("./appium-umbrella");
|
|
45
|
+
exports.NODE_ID = 'node-1';
|
|
46
|
+
const quiet = { info: () => undefined, warn: () => undefined, error: () => undefined };
|
|
47
|
+
/**
|
|
48
|
+
* A hub and a node in one process, for what a hub collects from a node about
|
|
49
|
+
* a session it runs: the node in Appium 3's own server() (Appium's umbrella
|
|
50
|
+
* with Xenon's plugin, the production node gateway, and the node's
|
|
51
|
+
* session-status, metrics and device-log routes), with per-command auth on,
|
|
52
|
+
* so every ask carries the hub's session token. The hub serves only its
|
|
53
|
+
* public keys: the spec plays the hub's side through the session's
|
|
54
|
+
* RemoteSession.
|
|
55
|
+
*
|
|
56
|
+
* Call it inside a `describe`, after useScratchDatabase(). `services` are the
|
|
57
|
+
* container ids the spec replaces in its own `beforeEach`; they are put back
|
|
58
|
+
* after each test.
|
|
59
|
+
*/
|
|
60
|
+
function useHubAndNode(scratch, services = []) {
|
|
61
|
+
const servers = [];
|
|
62
|
+
let dirs;
|
|
63
|
+
let hubKeys;
|
|
64
|
+
let appiumLogs;
|
|
65
|
+
let restore;
|
|
66
|
+
let saved;
|
|
67
|
+
let authDisabledBefore;
|
|
68
|
+
let commandAuthBefore;
|
|
69
|
+
let nodeOrigin = '';
|
|
70
|
+
before(() => __awaiter(this, void 0, void 0, function* () {
|
|
71
|
+
appiumLogs = (0, appium_umbrella_1.quietAppiumLogs)();
|
|
72
|
+
dirs = [fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-hub-node-keys-'))];
|
|
73
|
+
hubKeys = new JwtKeyService_1.JwtKeyService();
|
|
74
|
+
yield hubKeys.init(dirs[0]);
|
|
75
|
+
}));
|
|
76
|
+
after(() => {
|
|
77
|
+
appiumLogs.restore();
|
|
78
|
+
for (const dir of dirs)
|
|
79
|
+
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
80
|
+
});
|
|
81
|
+
beforeEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
82
|
+
restore = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, hubSessionToken_1.HubSessionTokenIssuer, device_managers_1.XenonManager, appiumUmbrella_1.AppiumUmbrella, nodeSessionStatus_1.NodeSessionProbeSupport, ...services);
|
|
83
|
+
// One process plays both sides: the hub's keys sign, the node checks them.
|
|
84
|
+
typedi_1.Container.set(JwtKeyService_1.JwtKeyService, hubKeys);
|
|
85
|
+
typedi_1.Container.set(hubSessionToken_1.HubSessionTokenIssuer, new hubSessionToken_1.HubSessionTokenIssuer());
|
|
86
|
+
typedi_1.Container.set(appiumUmbrella_1.AppiumUmbrella, new appiumUmbrella_1.AppiumUmbrella());
|
|
87
|
+
typedi_1.Container.set(nodeSessionStatus_1.NodeSessionProbeSupport, new nodeSessionStatus_1.NodeSessionProbeSupport());
|
|
88
|
+
typedi_1.Container.set(device_managers_1.XenonManager, {
|
|
89
|
+
getMaxSessionCount: () => undefined,
|
|
90
|
+
deviceInstances: () => __awaiter(this, void 0, void 0, function* () { return []; }),
|
|
91
|
+
});
|
|
92
|
+
const context = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
93
|
+
saved = {
|
|
94
|
+
store: device_store_1.DeviceStoreFactory._deviceStore,
|
|
95
|
+
pending: device_store_1.DeviceStoreFactory._pendingSessionStore,
|
|
96
|
+
context: Object.assign({}, context),
|
|
97
|
+
};
|
|
98
|
+
device_store_1.DeviceStoreFactory._deviceStore = new prisma_store_1.PrismaDeviceStore();
|
|
99
|
+
device_store_1.DeviceStoreFactory._pendingSessionStore = new prisma_store_1.PrismaPendingSessionStore();
|
|
100
|
+
authDisabledBefore = config_1.config.authDisabled;
|
|
101
|
+
config_1.config.authDisabled = false;
|
|
102
|
+
// Per-command auth on: the node then takes the hub's token and nothing else.
|
|
103
|
+
commandAuthBefore = process.env.XENON_REQUIRE_COMMAND_AUTH;
|
|
104
|
+
process.env.XENON_REQUIRE_COMMAND_AUTH = '1';
|
|
105
|
+
yield scratch.db.pendingSession.deleteMany({});
|
|
106
|
+
yield scratch.db.device.deleteMany({});
|
|
107
|
+
}));
|
|
108
|
+
afterEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
109
|
+
for (const s of SessionManager_1.SESSION_MANAGER.getAllSessions())
|
|
110
|
+
SessionManager_1.SESSION_MANAGER.removeSession(s.getId());
|
|
111
|
+
device_store_1.DeviceStoreFactory._deviceStore = saved.store;
|
|
112
|
+
device_store_1.DeviceStoreFactory._pendingSessionStore = saved.pending;
|
|
113
|
+
Object.assign(typedi_1.Container.get(PluginContext_1.PluginContext), saved.context);
|
|
114
|
+
config_1.config.authDisabled = authDisabledBefore;
|
|
115
|
+
if (commandAuthBefore === undefined)
|
|
116
|
+
delete process.env.XENON_REQUIRE_COMMAND_AUTH;
|
|
117
|
+
else
|
|
118
|
+
process.env.XENON_REQUIRE_COMMAND_AUTH = commandAuthBefore;
|
|
119
|
+
sinon_1.default.restore();
|
|
120
|
+
restore();
|
|
121
|
+
for (const s of servers.splice(0)) {
|
|
122
|
+
s.closeAllConnections();
|
|
123
|
+
yield new Promise((resolve) => http_1.default.Server.prototype.close.call(s, () => resolve()));
|
|
124
|
+
}
|
|
125
|
+
}));
|
|
126
|
+
const port = (s) => s.address().port;
|
|
127
|
+
function boot() {
|
|
128
|
+
return __awaiter(this, arguments, void 0, function* (nodeArgs = {}) {
|
|
129
|
+
const baseDriver = (0, appium_umbrella_1.appiumBaseDriver)();
|
|
130
|
+
const hubServer = yield baseDriver.server({
|
|
131
|
+
routeConfiguringFunction: baseDriver.routeConfiguringFunction((0, appium_umbrella_1.umbrellaWith)([])),
|
|
132
|
+
port: 0,
|
|
133
|
+
hostname: '127.0.0.1',
|
|
134
|
+
basePath: '/wd/hub',
|
|
135
|
+
cliArgs: { basePath: '/wd/hub' },
|
|
136
|
+
serverUpdaters: [
|
|
137
|
+
(app) => {
|
|
138
|
+
app.get('/xenon/api/auth/jwks.json', (_req, res) => res.json(hubKeys.jwks()));
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
});
|
|
142
|
+
servers.push(hubServer);
|
|
143
|
+
const hubUrl = `http://127.0.0.1:${port(hubServer)}`;
|
|
144
|
+
const pluginArgs = Object.assign(Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { hub: hubUrl, bindHostOrIp: '127.0.0.1', enableDashboard: false, deviceAvailabilityTimeoutMs: 1000, deviceAvailabilityQueryIntervalMs: 100 }), nodeArgs);
|
|
145
|
+
const umbrella = (0, appium_umbrella_1.umbrellaWith)([[plugin_1.XenonPlugin, 'xenon']]);
|
|
146
|
+
const nodeServer = yield baseDriver.server({
|
|
147
|
+
routeConfiguringFunction: baseDriver.routeConfiguringFunction(umbrella),
|
|
148
|
+
port: 0,
|
|
149
|
+
hostname: '127.0.0.1',
|
|
150
|
+
basePath: '/node',
|
|
151
|
+
cliArgs: { basePath: '/node' },
|
|
152
|
+
serverUpdaters: [
|
|
153
|
+
(app) => {
|
|
154
|
+
(0, registerCommandAuth_1.registerSessionGateway)(app, { basePath: '/node' }, (0, registerCommandAuth_1.commandAuthDeps)({ enabled: () => true, authDisabled: () => false, logger: quiet }), (0, defaultGateway_1.nodeGatewayOptions)(hubUrl));
|
|
155
|
+
// What ServerManager mounts under /xenon/api on a node.
|
|
156
|
+
const api = express_1.default.Router();
|
|
157
|
+
(0, nodeSessionStatus_1.registerNodeSessionStatus)(api, pluginArgs);
|
|
158
|
+
(0, nodeSessionMetrics_1.registerNodeSessionMetrics)(api, pluginArgs);
|
|
159
|
+
(0, nodeSessionDeviceLogs_1.registerNodeSessionDeviceLogs)(api, pluginArgs);
|
|
160
|
+
app.use('/xenon/api', api);
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
});
|
|
164
|
+
servers.push(nodeServer);
|
|
165
|
+
nodeOrigin = `http://127.0.0.1:${port(nodeServer)}`;
|
|
166
|
+
typedi_1.Container.get(PluginContext_1.PluginContext).setContext(pluginArgs, port(nodeServer), exports.NODE_ID, '/node');
|
|
167
|
+
yield scratch.db.device.create({
|
|
168
|
+
data: {
|
|
169
|
+
udid: 'phone-1',
|
|
170
|
+
host: nodeOrigin,
|
|
171
|
+
nodeId: exports.NODE_ID,
|
|
172
|
+
platform: 'android',
|
|
173
|
+
name: 'phone-1',
|
|
174
|
+
sdk: '14',
|
|
175
|
+
busy: false,
|
|
176
|
+
userBlocked: false,
|
|
177
|
+
offline: false,
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
function nodeSession() {
|
|
183
|
+
return __awaiter(this, arguments, void 0, function* (caps = {}) {
|
|
184
|
+
const token = yield typedi_1.Container.get(hubSessionToken_1.HubSessionTokenIssuer).createTokenFor({
|
|
185
|
+
userId: 'alice',
|
|
186
|
+
udid: 'phone-1',
|
|
187
|
+
host: nodeOrigin,
|
|
188
|
+
});
|
|
189
|
+
const res = yield (0, loopbackRequest_1.default)(nodeOrigin)
|
|
190
|
+
.post('/node/session')
|
|
191
|
+
.set(hubSessionToken_1.HUB_TOKEN_HEADER, token)
|
|
192
|
+
.send({
|
|
193
|
+
capabilities: {
|
|
194
|
+
alwaysMatch: Object.assign({ platformName: 'Android', 'appium:automationName': appium_umbrella_1.FAKE_AUTOMATION, 'appium:newCommandTimeout': 0, 'appium:udid': 'phone-1', 'xe:options': { recordVideo: false } }, caps),
|
|
195
|
+
firstMatch: [{}],
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
(0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
|
|
199
|
+
return res.body.value.sessionId;
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
const hubSide = (sessionId) => new RemoteSession_1.RemoteSession({
|
|
203
|
+
sessionId,
|
|
204
|
+
device: { udid: 'phone-1', host: nodeOrigin, nodeId: exports.NODE_ID, platform: 'android' },
|
|
205
|
+
sessionResponse: {},
|
|
206
|
+
xenonOption: {},
|
|
207
|
+
baseUrl: `${nodeOrigin}/node`,
|
|
208
|
+
});
|
|
209
|
+
function deleteOnNode(sessionId) {
|
|
210
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
211
|
+
const token = yield typedi_1.Container.get(hubSessionToken_1.HubSessionTokenIssuer).tokenFor(sessionId);
|
|
212
|
+
const res = yield (0, loopbackRequest_1.default)(nodeOrigin)
|
|
213
|
+
.delete(`/node/session/${sessionId}`)
|
|
214
|
+
.set(hubSessionToken_1.HUB_TOKEN_HEADER, token);
|
|
215
|
+
(0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
get nodeOrigin() {
|
|
220
|
+
return nodeOrigin;
|
|
221
|
+
},
|
|
222
|
+
boot,
|
|
223
|
+
nodeSession,
|
|
224
|
+
hubSide,
|
|
225
|
+
deleteOnNode,
|
|
226
|
+
};
|
|
227
|
+
}
|