@xenon-device-management/xenon 2.16.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 +2 -2
- package/lib/package.json +1 -1
- package/lib/public/assets/{AnnotationOverlay-2W4JvEyZ.js → AnnotationOverlay-CjmMCS0S.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-GmckX_7r.js → ApiKeyGate-B4Ob0v3C.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-DqODzkyo.js → DeviceMosaicView-DTVOB_NO.js} +1 -1
- package/lib/public/assets/{EmptyState-7hETKEWb.js → EmptyState-DcEG-oAi.js} +1 -1
- package/lib/public/assets/{FieldGroup-GOgssFEP.js → FieldGroup-CtnCDshE.js} +1 -1
- package/lib/public/assets/{FilterMenu-Yvf6RGMd.js → FilterMenu-BK0dE9cc.js} +1 -1
- package/lib/public/assets/{Menu-KrPnfdsf.js → Menu-eKsmLpKS.js} +1 -1
- package/lib/public/assets/{Modal-C6WjZFeI.js → Modal-DPwop3Lz.js} +1 -1
- package/lib/public/assets/{RecordingPage-CSRIhZEh.js → RecordingPage-CfgSkZkB.js} +1 -1
- package/lib/public/assets/{RecordingsPage-CB8ST3nE.js → RecordingsPage-Bzl9pTUY.js} +1 -1
- package/lib/public/assets/{SegmentedControl-C9o90KId.js → SegmentedControl-B_TZTrPm.js} +1 -1
- package/lib/public/assets/{SettingCard-CPm3w9l2.js → SettingCard-DlmvZJPM.js} +1 -1
- package/lib/public/assets/{Table-DwIgERRt.js → Table-BigPWKXi.js} +1 -1
- package/lib/public/assets/{activity-DrLbHifn.js → activity-CvuZdQYu.js} +1 -1
- package/lib/public/assets/{ai-settings-DU_Ls06P.js → ai-settings-lI2UJ2n7.js} +1 -1
- package/lib/public/assets/{api-keys-CWYdEzJ_.js → api-keys-CPgf9XJM.js} +1 -1
- package/lib/public/assets/{apps-BqNVdln7.js → apps-DIzOvtUp.js} +1 -1
- package/lib/public/assets/{arrow-left-BtB0_Jg_.js → arrow-left-DiP5ywBT.js} +1 -1
- package/lib/public/assets/{arrow-right-C8NhdtyN.js → arrow-right-DtW_8hm5.js} +1 -1
- package/lib/public/assets/{arrow-up-right-gMg0ffvv.js → arrow-up-right-C89FbNU0.js} +1 -1
- package/lib/public/assets/{auth-shell-CiYZ3N4M.js → auth-shell-B7sUDb--.js} +2 -2
- package/lib/public/assets/{builds-page-EaaqDmLN.js → builds-page-BBhvrMfA.js} +1 -1
- package/lib/public/assets/{button-DDau4Axv.js → button-DugsdUw1.js} +1 -1
- package/lib/public/assets/{calendar-DQ83B1ed.js → calendar-grKd08ZH.js} +1 -1
- package/lib/public/assets/{check-K1ovTQam.js → check-ORjyNfnX.js} +1 -1
- package/lib/public/assets/{chevron-right-Biq00pMt.js → chevron-right-Ei2FxUXq.js} +1 -1
- package/lib/public/assets/{circle-check-CBv88cH7.js → circle-check-Day0HjW4.js} +1 -1
- package/lib/public/assets/{circle-x-Bmltailn.js → circle-x-CHxSRLHp.js} +1 -1
- package/lib/public/assets/{clock-BPrld-3h.js → clock-BDHewWTx.js} +1 -1
- package/lib/public/assets/{copy-t0QB14JI.js → copy-CTwT4I3D.js} +1 -1
- package/lib/public/assets/{device-explorer-BgGZ8AvV.js → device-explorer-DlhrKJ-g.js} +1 -1
- package/lib/public/assets/{download-BroHxNOh.js → download-D3bjz_vr.js} +1 -1
- package/lib/public/assets/{forgot-password-Bjmf49i6.js → forgot-password-BNBLi023.js} +1 -1
- package/lib/public/assets/{h264Stream-DeD__6wa.js → h264Stream-C51Au2i_.js} +1 -1
- package/lib/public/assets/{index-B0gQkEg0.js → index-Dk5J6HB1.js} +2 -2
- package/lib/public/assets/{index-B0JeuZbg.js → index-DncSz5LG.js} +1 -1
- package/lib/public/assets/{index-72oF89LJ.js → index-Z7_BY3pJ.js} +1 -1
- package/lib/public/assets/{input-DIc4MSC7.js → input-bTXM4l09.js} +1 -1
- package/lib/public/assets/{line-chart-BcWm2vW8.js → line-chart-B3YmVYyr.js} +1 -1
- package/lib/public/assets/{list-checks-B06IJJad.js → list-checks-BClKEj1-.js} +1 -1
- package/lib/public/assets/{lock-pVLvnN30.js → lock-CZsenX3D.js} +1 -1
- package/lib/public/assets/{login-CN77vy--.js → login-B6A6tOp9.js} +1 -1
- package/lib/public/assets/{maintenance-settings-CSdzQSkY.js → maintenance-settings-CmCJGy7O.js} +1 -1
- package/lib/public/assets/{monitor-BtrN5IkI.js → monitor-BdTNEglU.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CNsQfo6w.js → mouse-pointer-2-BM8CaLtt.js} +1 -1
- package/lib/public/assets/{network-BSfA8ZxG.js → network-BS6vUc1x.js} +1 -1
- package/lib/public/assets/{overview-BfTebCK5.js → overview-Yfk_Y7g3.js} +1 -1
- package/lib/public/assets/{page-header-D8KUX6Qg.js → page-header-DhXCwpml.js} +1 -1
- package/lib/public/assets/{play-Cvc8xzic.js → play-D08gP1td.js} +1 -1
- package/lib/public/assets/{plus-C9Zj84UF.js → plus-BKboorfa.js} +1 -1
- package/lib/public/assets/{profile-page-Bi27STgs.js → profile-page-Cd9NodrA.js} +1 -1
- package/lib/public/assets/{reset-password-CPsOuwSu.js → reset-password-acLDyyDW.js} +1 -1
- package/lib/public/assets/{runbook-page-DEQ-DBOX.js → runbook-page-CWLkC1Fd.js} +1 -1
- package/lib/public/assets/{select-BWpnhWXj.js → select-DZoRKNti.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BefSCSpe.js → selector-detail-redirect-DS4qrt84.js} +1 -1
- package/lib/public/assets/{selector-health-page-MSk96Zz7.js → selector-health-page-TpmN61G3.js} +1 -1
- package/lib/public/assets/{session-detail-page-DPyBNef_.js → session-detail-page-C0wYaXqy.js} +1 -1
- package/lib/public/assets/{settings-V6x-cXwy.js → settings-CxPGEY2j.js} +1 -1
- package/lib/public/assets/{stat-tile-Duex-GZD.js → stat-tile-DUpyYgl-.js} +1 -1
- package/lib/public/assets/{tablet-BBcoqBmL.js → tablet-SA02BJNQ.js} +1 -1
- package/lib/public/assets/{teams-ts8p5rEh.js → teams-jamD0Kxr.js} +1 -1
- package/lib/public/assets/{trash-2-CZ2ew6AZ.js → trash-2-DO8fv6w4.js} +1 -1
- package/lib/public/assets/{upload-Dv99y_le.js → upload-CaiGCStb.js} +1 -1
- package/lib/public/assets/{use-builds-data-DrYC8JCF.js → use-builds-data-jS-fcvVf.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-C7QkBNkP.js → use-password-reset-mode-BzoenegM.js} +1 -1
- package/lib/public/assets/{users-gJ5Y01yo.js → users-Bnw7Vs_B.js} +1 -1
- package/lib/public/assets/{users-D7tKMESH.js → users-DHvDrriG.js} +1 -1
- package/lib/public/assets/{video-off-CqZVuZGs.js → video-off-FEvy0w5U.js} +1 -1
- package/lib/public/assets/{webhook-settings-DI5DSW99.js → webhook-settings-CBVB5lxi.js} +1 -1
- package/lib/public/assets/{zap-ClcHFxfo.js → zap-BMrTJO6M.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +1 -1
- package/lib/src/app/index.js +2 -0
- package/lib/src/app/openapi/grid.yaml +102 -0
- package/lib/src/dashboard/event-manager.js +5 -8
- package/lib/src/device-utils.js +11 -2
- package/lib/src/gateway/nodeAsk.js +57 -0
- package/lib/src/gateway/nodeSessionDeviceLogs.js +35 -0
- package/lib/src/plugin.js +4 -1
- package/lib/src/scripts/initialize-database.js +0 -6
- package/lib/src/scripts/run-migrations.js +123 -880
- package/lib/src/services/SessionLifecycleService.js +20 -5
- 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 +199 -44
- package/lib/src/services/logcat/nodeDeviceLogs.js +91 -0
- package/lib/src/services/metrics/nodeMetrics.js +20 -49
- package/lib/src/sessions/RemoteSession.js +11 -2
- package/lib/src/sessions/SessionManager.js +28 -1
- package/lib/test/helpers/fake-logcat.js +80 -0
- package/lib/test/helpers/hub-node-pair.js +227 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +15 -0
- 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/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/run-migrations-database.spec.js +57 -424
- package/lib/test/unit/run-migrations.test.js +74 -674
- package/lib/test/unit/session-device-logs-hub-node.spec.js +340 -0
- package/lib/test/unit/session-device-logs-lifecycle.spec.js +137 -0
- package/lib/test/unit/session-device-logs.spec.js +32 -94
- package/lib/test/unit/session-log-indexes.spec.js +15 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/schema.json +1 -1
|
@@ -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
|
+
}
|
|
@@ -175,6 +175,21 @@ describe('EventManager: an Android session’s device log', () => {
|
|
|
175
175
|
(0, chai_1.expect)(deviceLogs.start.firstCall.args[0].driverLog).to.equal(driverLog);
|
|
176
176
|
(0, chai_1.expect)(deviceLogs.start.firstCall.args[0].appUnderTest).to.equal('com.example.shop');
|
|
177
177
|
}));
|
|
178
|
+
it("collects a node session's from the node, through the session", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
179
|
+
const { device } = stubSessionStart();
|
|
180
|
+
const session = {
|
|
181
|
+
getId: () => 's-logs-node',
|
|
182
|
+
getType: () => 'remote',
|
|
183
|
+
getCapabilities: () => ({ platformName: 'Android' }),
|
|
184
|
+
getLiveVideoUrl: () => null,
|
|
185
|
+
nodeOrigin: () => 'http://node:4723',
|
|
186
|
+
nodeDeviceLogs: () => __awaiter(void 0, void 0, void 0, function* () { return ({ kind: 'refused' }); }),
|
|
187
|
+
apiKeyId: null,
|
|
188
|
+
userId: null,
|
|
189
|
+
};
|
|
190
|
+
yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStarted(parsed({}), session, Object.assign(Object.assign({}, device), { host: 'http://node:4723' }));
|
|
191
|
+
(0, chai_1.expect)(deviceLogs.start.firstCall.args[0].source).to.equal(session);
|
|
192
|
+
}));
|
|
178
193
|
it('writes it to the end when the session stops, even one no longer in memory', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
179
194
|
sinon_1.default.stub(SessionManager_1.SESSION_MANAGER, 'getSession').returns(undefined);
|
|
180
195
|
sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').returns({ getDevices: () => __awaiter(void 0, void 0, void 0, function* () { return []; }) });
|
|
@@ -0,0 +1,134 @@
|
|
|
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 sinon_1 = __importDefault(require("sinon"));
|
|
18
|
+
const device_utils_1 = require("../../src/device-utils");
|
|
19
|
+
const device_service_1 = require("../../src/data-service/device-service");
|
|
20
|
+
const device_store_1 = require("../../src/data-service/device-store");
|
|
21
|
+
const localDeviceHosts_1 = require("../../src/device-managers/localDeviceHosts");
|
|
22
|
+
const IOSStreamService_1 = __importDefault(require("../../src/device-managers/ios/IOSStreamService"));
|
|
23
|
+
const WDAClient_1 = require("../../src/device-managers/ios/WDAClient");
|
|
24
|
+
const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
|
|
25
|
+
const test_container_1 = require("../helpers/test-container");
|
|
26
|
+
const loki_stores_1 = require("../helpers/loki-stores");
|
|
27
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
28
|
+
/**
|
|
29
|
+
* Before a session, allocation asks the phone's manager whether it is ready.
|
|
30
|
+
* For an iPhone that asks WebDriverAgent on 127.0.0.1, at the port the phone's
|
|
31
|
+
* row names, and on no answer starts a go-ios stream for the phone. Both are
|
|
32
|
+
* this machine's: they say nothing about a phone another server drives.
|
|
33
|
+
*
|
|
34
|
+
* A hub ran it for its nodes' iPhones too. With the node on another machine
|
|
35
|
+
* nothing answered on the hub, its stream found no phone of its own to start,
|
|
36
|
+
* and every session on the node's iPhone was refused as "unhealthy". With both
|
|
37
|
+
* on one Mac, the hub started its own WebDriverAgent on the phone the node
|
|
38
|
+
* drives. The node checks the phone itself when it allocates it.
|
|
39
|
+
*/
|
|
40
|
+
describe("Readiness before a session, on a hub allocating a node's iPhone", function () {
|
|
41
|
+
this.timeout(60000);
|
|
42
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
43
|
+
(0, loki_stores_1.usePrismaStores)();
|
|
44
|
+
const NODE_ID = 'node-process';
|
|
45
|
+
const UDID = '00008110-000A1B2C3D4E5F60';
|
|
46
|
+
const pluginArgs = Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { platform: 'ios', iosDeviceType: 'both' });
|
|
47
|
+
let hubNodeId;
|
|
48
|
+
let hubOrigin;
|
|
49
|
+
let verifyWda;
|
|
50
|
+
let startStream;
|
|
51
|
+
const iPhone = (over) => (Object.assign({ udid: UDID, name: 'iPhone 14 Plus', platform: 'ios', sdk: '18.0', deviceType: 'real', realDevice: true, state: 'device', busy: false, offline: false, userBlocked: false, wdaLocalPort: 8100, mjpegServerPort: 9100, totalUtilizationTimeMilliSec: 0, sessionStartTime: 0 }, over));
|
|
52
|
+
const caps = (extra = {}) => ({
|
|
53
|
+
alwaysMatch: Object.assign({ platformName: 'iOS', 'appium:udid': UDID }, extra),
|
|
54
|
+
firstMatch: [{}],
|
|
55
|
+
});
|
|
56
|
+
const allocate = (extra = {}) => (0, device_utils_1.allocateDeviceForSession)(caps(extra), 1000, 50, pluginArgs);
|
|
57
|
+
const row = (host) => device_store_1.DeviceStoreFactory.getStore().findDevice({ udid: UDID, host });
|
|
58
|
+
beforeEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
59
|
+
const { context, xenonManager } = (0, test_container_1.setupTestContainer)(pluginArgs);
|
|
60
|
+
xenonManager.init();
|
|
61
|
+
hubNodeId = context.nodeId;
|
|
62
|
+
hubOrigin = (0, localDeviceHosts_1.serverOrigin)(context.pluginArgs, context.port);
|
|
63
|
+
yield scratch.db.device.deleteMany({});
|
|
64
|
+
yield scratch.db.deviceSetting.deleteMany({});
|
|
65
|
+
verifyWda = sinon_1.default.stub(WDAClient_1.WDAClient.prototype, 'verifyWDAStatus');
|
|
66
|
+
startStream = sinon_1.default.stub(IOSStreamService_1.default.prototype, 'startStream');
|
|
67
|
+
}));
|
|
68
|
+
afterEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
69
|
+
sinon_1.default.restore();
|
|
70
|
+
yield (0, test_container_1.resetTestContainer)();
|
|
71
|
+
}));
|
|
72
|
+
describe('a node on another machine', () => {
|
|
73
|
+
const NODE = 'http://203.0.113.7:4723';
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
// What the hub's own machine answers: no WebDriverAgent on its
|
|
76
|
+
// 127.0.0.1, and no row of its own to start a stream from.
|
|
77
|
+
verifyWda.resolves(false);
|
|
78
|
+
startStream.rejects(new Error(`Device ${UDID} not found`));
|
|
79
|
+
});
|
|
80
|
+
it("allocates the node's iPhone, which the node checks itself", () => __awaiter(this, void 0, void 0, function* () {
|
|
81
|
+
var _a;
|
|
82
|
+
yield (0, device_service_1.addNewDevice)([iPhone({ host: NODE, nodeId: NODE_ID })]);
|
|
83
|
+
const device = yield allocate();
|
|
84
|
+
(0, chai_1.expect)(device).to.include({ udid: UDID, host: NODE });
|
|
85
|
+
(0, chai_1.expect)((_a = (yield row(NODE))) === null || _a === void 0 ? void 0 : _a.busy).to.equal(true);
|
|
86
|
+
}));
|
|
87
|
+
it("never asks the hub's own WebDriverAgent or starts a stream for it", () => __awaiter(this, void 0, void 0, function* () {
|
|
88
|
+
yield (0, device_service_1.addNewDevice)([iPhone({ host: NODE, nodeId: NODE_ID })]);
|
|
89
|
+
yield allocate().catch(() => undefined);
|
|
90
|
+
(0, chai_1.expect)(verifyWda.called, 'verifyWDAStatus called').to.equal(false);
|
|
91
|
+
(0, chai_1.expect)(startStream.called, 'startStream called').to.equal(false);
|
|
92
|
+
}));
|
|
93
|
+
});
|
|
94
|
+
describe('a node on the same Mac', () => {
|
|
95
|
+
it("never starts the hub's own stream for the iPhone the node drives", () => __awaiter(this, void 0, void 0, function* () {
|
|
96
|
+
var _a;
|
|
97
|
+
const node = `http://${pluginArgs.bindHostOrIp}:4724`;
|
|
98
|
+
// The hub sees the same iPhone and files it under its own host; the
|
|
99
|
+
// client asks for the node's.
|
|
100
|
+
yield (0, device_service_1.addNewDevice)([
|
|
101
|
+
iPhone({ host: node, nodeId: NODE_ID }),
|
|
102
|
+
iPhone({ host: hubOrigin, nodeId: hubNodeId, wdaLocalPort: 8101 }),
|
|
103
|
+
]);
|
|
104
|
+
// The hub's stream starts for the udid, and its WebDriverAgent then
|
|
105
|
+
// answers: a second one beside the node's.
|
|
106
|
+
verifyWda.callsFake(() => __awaiter(this, void 0, void 0, function* () { return startStream.called; }));
|
|
107
|
+
startStream.resolves({ wdaPort: 8102, mjpegPort: 9102 });
|
|
108
|
+
const device = yield allocate({ 'appium:filterByHost': node });
|
|
109
|
+
(0, chai_1.expect)(device).to.include({ udid: UDID, host: node });
|
|
110
|
+
(0, chai_1.expect)(startStream.called, 'startStream called').to.equal(false);
|
|
111
|
+
(0, chai_1.expect)(verifyWda.called, 'verifyWDAStatus called').to.equal(false);
|
|
112
|
+
(0, chai_1.expect)((_a = (yield row(hubOrigin))) === null || _a === void 0 ? void 0 : _a.busy).to.equal(false);
|
|
113
|
+
}));
|
|
114
|
+
});
|
|
115
|
+
describe("the hub's own iPhone", () => {
|
|
116
|
+
it('is still checked, and allocated when its WebDriverAgent answers', () => __awaiter(this, void 0, void 0, function* () {
|
|
117
|
+
yield (0, device_service_1.addNewDevice)([iPhone({ host: hubOrigin, nodeId: hubNodeId })]);
|
|
118
|
+
verifyWda.resolves(true);
|
|
119
|
+
const device = yield allocate();
|
|
120
|
+
(0, chai_1.expect)(device).to.include({ udid: UDID, host: hubOrigin });
|
|
121
|
+
(0, chai_1.expect)(verifyWda.calledWith(UDID)).to.equal(true);
|
|
122
|
+
}));
|
|
123
|
+
it('is still refused, and released, when it is not ready and cannot be recovered', () => __awaiter(this, void 0, void 0, function* () {
|
|
124
|
+
var _a;
|
|
125
|
+
yield (0, device_service_1.addNewDevice)([iPhone({ host: hubOrigin, nodeId: hubNodeId })]);
|
|
126
|
+
verifyWda.resolves(false);
|
|
127
|
+
startStream.rejects(new Error('go-ios could not start'));
|
|
128
|
+
const error = yield allocate().then(() => null, (e) => e);
|
|
129
|
+
(0, chai_1.expect)(error === null || error === void 0 ? void 0 : error.message).to.match(/is unhealthy and could not be autonomously recovered/);
|
|
130
|
+
(0, chai_1.expect)(startStream.calledWith(UDID)).to.equal(true);
|
|
131
|
+
(0, chai_1.expect)((_a = (yield row(hubOrigin))) === null || _a === void 0 ? void 0 : _a.busy).to.equal(false);
|
|
132
|
+
}));
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -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
|
+
});
|