@xenon-device-management/xenon 2.10.0 → 2.11.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/lib/package.json +1 -1
- package/lib/public/assets/{AnnotationOverlay-LzfXiiZJ.js → AnnotationOverlay-BQPQ5bPF.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-BzFDp4mK.js → ApiKeyGate-Bd7Pumwv.js} +1 -1
- package/lib/public/assets/{BugReportButton-BpgWvmZo.js → BugReportButton-iC7fxPxl.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-CCB-egSH.js → DeviceMosaicView-CNdES7nm.js} +1 -1
- package/lib/public/assets/{EmptyState-DkUVmANo.js → EmptyState-wZeJbGhN.js} +1 -1
- package/lib/public/assets/{FieldGroup-mbfnfTql.js → FieldGroup-tpRfmhlE.js} +1 -1
- package/lib/public/assets/{FilterMenu-BH5usz4Y.js → FilterMenu-BZwOppId.js} +1 -1
- package/lib/public/assets/{Menu-Cbo9mdTk.js → Menu-CahoAx8L.js} +1 -1
- package/lib/public/assets/{Modal-D6XfQHPr.js → Modal-mcM5W_Ji.js} +1 -1
- package/lib/public/assets/{RecordingPage-CmFPkWqH.js → RecordingPage-B4dFBndn.js} +1 -1
- package/lib/public/assets/{RecordingsPage-CSlKk2qC.js → RecordingsPage-BOez3f_9.js} +1 -1
- package/lib/public/assets/{SegmentedControl-BFW9Dlqd.js → SegmentedControl-qMSiMN1d.js} +1 -1
- package/lib/public/assets/{SettingCard-C4DioTTi.js → SettingCard-rcPtJ7HG.js} +1 -1
- package/lib/public/assets/{Table-DrKjlL00.js → Table-J3uognu0.js} +1 -1
- package/lib/public/assets/{activity-C9bE7Jm8.js → activity-_5pNSfKT.js} +1 -1
- package/lib/public/assets/{ai-settings-DRYic4oO.js → ai-settings-DTPRbkei.js} +1 -1
- package/lib/public/assets/{api-keys-CaQU0g7c.js → api-keys-CxkIUo76.js} +1 -1
- package/lib/public/assets/apps-BQKfuN3J.css +1 -0
- package/lib/public/assets/{apps-c1gUFmmK.js → apps-Bv0pAfxz.js} +2 -2
- package/lib/public/assets/{arrow-left-DMvAWA8r.js → arrow-left-uY7F6z1K.js} +1 -1
- package/lib/public/assets/{arrow-right-BEQgxmKT.js → arrow-right-BbAm_2Wa.js} +1 -1
- package/lib/public/assets/{arrow-up-right-Ba0e2GeU.js → arrow-up-right-x4H5NuvE.js} +1 -1
- package/lib/public/assets/{auth-shell-COODQnc7.js → auth-shell-CtMFhbnC.js} +2 -2
- package/lib/public/assets/{builds-page-DZQgWBUs.js → builds-page-B_O5uBSZ.js} +1 -1
- package/lib/public/assets/{button-WW9j43wR.js → button-CQoMQEj_.js} +1 -1
- package/lib/public/assets/{calendar-COUs9VxF.js → calendar-BZkpCqlL.js} +1 -1
- package/lib/public/assets/{check-D8ifHIJR.js → check-CAB6RYuc.js} +1 -1
- package/lib/public/assets/{chevron-right-BjKVUlB_.js → chevron-right-ec0unu3u.js} +1 -1
- package/lib/public/assets/{circle-check-CfP79QE2.js → circle-check-Dfg-gpjg.js} +1 -1
- package/lib/public/assets/{circle-x-BZbFCixZ.js → circle-x-cbPq6Btp.js} +1 -1
- package/lib/public/assets/{clock-BuTm35Qg.js → clock-DU9cBLi8.js} +1 -1
- package/lib/public/assets/{copy-W00zUBwN.js → copy-VM9kWPpE.js} +1 -1
- package/lib/public/assets/{device-explorer-BEpS-Y-_.js → device-explorer-ai0usQ8m.js} +1 -1
- package/lib/public/assets/{download-CGz-MS3y.js → download-WgW4AVid.js} +1 -1
- package/lib/public/assets/{forgot-password-DG7SqVgU.js → forgot-password-BGRpjTtt.js} +1 -1
- package/lib/public/assets/{index-EkaQM1DK.js → index-C33GjwPr.js} +2 -2
- package/lib/public/assets/{index-DWHTp1YP.js → index-DNDdHt3h.js} +1 -1
- package/lib/public/assets/{input-ChaTXeCq.js → input-DwiVcO3Q.js} +1 -1
- package/lib/public/assets/{keycodes-BhbgeW_r.js → keycodes-CvCtpVJD.js} +1 -1
- package/lib/public/assets/{line-chart-9P3RxScy.js → line-chart-PE5e9PeN.js} +1 -1
- package/lib/public/assets/{list-checks-_23IIEEL.js → list-checks-Bgn9GEEO.js} +1 -1
- package/lib/public/assets/{lock-B2JaqqRI.js → lock-Cb7My6hc.js} +1 -1
- package/lib/public/assets/{login-BEWIfLfC.js → login-CDmyzSpl.js} +1 -1
- package/lib/public/assets/{maintenance-settings-BGJIJQ-k.js → maintenance-settings-Bq6pKoBt.js} +1 -1
- package/lib/public/assets/{monitor-BrwMY4G0.js → monitor-BZSwrnPV.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-DsWDMyrY.js → mouse-pointer-2-BbFj8y8i.js} +1 -1
- package/lib/public/assets/{network-DfIhNVJv.js → network-DpwYGB0-.js} +1 -1
- package/lib/public/assets/{overview-B1mePY2_.js → overview-B4_IO-X8.js} +1 -1
- package/lib/public/assets/{page-header-DoD3TOzZ.js → page-header-BpLEuo7i.js} +1 -1
- package/lib/public/assets/{plus-BGvWvpBB.js → plus-DHZS0EGU.js} +1 -1
- package/lib/public/assets/{profile-page-CvV5aWvl.js → profile-page-DsHDIq-Y.js} +1 -1
- package/lib/public/assets/{reset-password-CGVd31LR.js → reset-password-DuUjhw_l.js} +1 -1
- package/lib/public/assets/{runbook-page-Bq2cvwb5.js → runbook-page-BI_ayvLd.js} +1 -1
- package/lib/public/assets/{select-CTcD4jWR.js → select-xiba9PSx.js} +1 -1
- package/lib/public/assets/selector-detail-redirect-Emz2obK_.js +1 -0
- package/lib/public/assets/selector-health-page-zpwrgalC.js +11 -0
- package/lib/public/assets/session-detail-page-BhlTJ1Oq.js +4 -0
- package/lib/public/assets/{settings-C-EnGIqs.js → settings-B-mabJFm.js} +1 -1
- package/lib/public/assets/{stat-tile-BY0RDTRm.js → stat-tile-lT8WcPgu.js} +1 -1
- package/lib/public/assets/{tablet-B4cFM-5B.js → tablet-DM8dp75P.js} +1 -1
- package/lib/public/assets/{teams-BFe-RpYd.js → teams-CRkb5-Fe.js} +1 -1
- package/lib/public/assets/{trash-2-CJCKBOP9.js → trash-2-Dxgryn4o.js} +1 -1
- package/lib/public/assets/{upload-DidWkbld.js → upload-Ci0vz0BP.js} +1 -1
- package/lib/public/assets/{use-builds-data-fnfxatOd.js → use-builds-data-DaaqMjjL.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-iPen3tmC.js → use-password-reset-mode-BvIyZtaF.js} +1 -1
- package/lib/public/assets/{users-CrcjmRdf.js → users-BT8qx3ea.js} +1 -1
- package/lib/public/assets/{users-DBUv0CJx.js → users-CWYHk75q.js} +1 -1
- package/lib/public/assets/{video-off-eh69-FKB.js → video-off-BvY08g6I.js} +1 -1
- package/lib/public/assets/view-state-83Z1gFME.js +1 -0
- package/lib/public/assets/{webhook-settings-CQoSshBn.js → webhook-settings-aHIlSJFP.js} +1 -1
- package/lib/public/assets/{zap-CICtWiLn.js → zap-qG2Li6rI.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/src/app/index.js +2 -0
- package/lib/src/dashboard/event-manager.js +4 -6
- package/lib/src/gateway/nodeSessionMetrics.js +39 -0
- package/lib/src/gateway/nodeSessionStatus.js +32 -23
- package/lib/src/generated/client/edge.js +2 -2
- package/lib/src/generated/client/index.js +2 -2
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +6 -0
- package/lib/src/plugin.js +3 -0
- package/lib/src/services/SelectorVerificationJob.js +50 -5
- package/lib/src/services/SessionLifecycleService.js +16 -0
- package/lib/src/services/metrics/NodeMetricsCollector.js +114 -0
- package/lib/src/services/metrics/NodeMetricsStore.js +76 -0
- package/lib/src/services/metrics/SessionMetricsService.js +71 -14
- package/lib/src/services/metrics/nodeMetrics.js +92 -0
- package/lib/src/services/selector-health/selectorDetail.js +36 -20
- package/lib/src/sessions/RemoteSession.js +32 -0
- package/lib/src/sessions/SessionManager.js +63 -0
- package/lib/test/helpers/scratch-database.js +17 -3
- package/lib/test/unit/dashboard-events-metrics.spec.js +10 -0
- package/lib/test/unit/hub-node-session-metrics.spec.js +330 -0
- package/lib/test/unit/node-metrics-collector.spec.js +226 -0
- package/lib/test/unit/node-metrics-store.spec.js +70 -0
- package/lib/test/unit/node-session-metrics-route.spec.js +97 -0
- package/lib/test/unit/selector-health-detail.spec.js +57 -0
- package/lib/test/unit/selector-verification-job.spec.js +3 -0
- package/lib/test/unit/selector-verification-missed-regression.spec.js +132 -0
- package/lib/test/unit/session-log-indexes.spec.js +147 -0
- package/lib/test/unit/session-metrics-lifecycle.spec.js +55 -0
- package/lib/test/unit/session-metrics-service.spec.js +97 -0
- package/lib/test/unit/session-recovery-metrics.spec.js +94 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/prisma/migrations/20261002180000_session_log_indexes/migration.sql +5 -0
- package/prisma/migrations/20261002190000_log_profiling_session_indexes/migration.sql +5 -0
- package/prisma/schema.prisma +6 -0
- package/lib/public/assets/apps-DMhpZuKd.css +0 -1
- package/lib/public/assets/selector-detail-redirect-19odlD4B.js +0 -1
- package/lib/public/assets/selector-health-page-LqIsLrjW.js +0 -11
- package/lib/public/assets/session-detail-page-DQzb8_fq.js +0 -4
- package/lib/public/assets/view-state-CP9dE1UI.js +0 -1
|
@@ -1,10 +1,43 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
2
18
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
19
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
20
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
21
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
22
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
23
|
};
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
8
41
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
9
42
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
10
43
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
@@ -28,6 +61,7 @@ const CloudSession_1 = require("./CloudSession");
|
|
|
28
61
|
const device_store_1 = require("../data-service/device-store");
|
|
29
62
|
const nodeWebDriverUrl_1 = require("../gateway/nodeWebDriverUrl");
|
|
30
63
|
const XenonCapabilityManager_1 = require("../XenonCapabilityManager");
|
|
64
|
+
const nodeMetrics_1 = require("../services/metrics/nodeMetrics");
|
|
31
65
|
/**
|
|
32
66
|
* SessionManager with persistence and recovery capabilities.
|
|
33
67
|
*
|
|
@@ -183,6 +217,11 @@ let SessionManager = class SessionManager {
|
|
|
183
217
|
// Add to in-memory map
|
|
184
218
|
this.addSession(dbSession.id, recoveredSession);
|
|
185
219
|
yield this.adoptHeartbeat(dbSession.id);
|
|
220
|
+
// A node session whose figures the hub was collecting goes on from
|
|
221
|
+
// the newest sample stored here; the node's memory fills the gap.
|
|
222
|
+
if (!device.cloud && dbSession.is_profiling_available) {
|
|
223
|
+
yield this.resumeNodeMetrics(dbSession.id, device, recoveredSession, sessionResponse);
|
|
224
|
+
}
|
|
186
225
|
recoveredCount++;
|
|
187
226
|
}
|
|
188
227
|
catch (sessionErr) {
|
|
@@ -200,6 +239,30 @@ let SessionManager = class SessionManager {
|
|
|
200
239
|
}
|
|
201
240
|
});
|
|
202
241
|
}
|
|
242
|
+
/** Goes on collecting a node session's figures after a hub restart, from the newest stored. */
|
|
243
|
+
resumeNodeMetrics(sessionId, device, session, capabilities) {
|
|
244
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
245
|
+
var _a;
|
|
246
|
+
try {
|
|
247
|
+
const newest = yield prisma_1.prisma.sessionMetric.aggregate({
|
|
248
|
+
where: { session_id: sessionId },
|
|
249
|
+
_max: { at: true },
|
|
250
|
+
});
|
|
251
|
+
// Loaded here: SessionMetricsService imports the device managers.
|
|
252
|
+
const { SessionMetricsService } = yield Promise.resolve().then(() => __importStar(require('../services/metrics/SessionMetricsService')));
|
|
253
|
+
typedi_2.Container.get(SessionMetricsService).start({
|
|
254
|
+
sessionId,
|
|
255
|
+
device,
|
|
256
|
+
capabilities,
|
|
257
|
+
source: (0, nodeMetrics_1.nodeMetricsSourceOf)(session),
|
|
258
|
+
after: (_a = newest._max.at) !== null && _a !== void 0 ? _a : null,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
this.log.warn(`Session ${sessionId}: CPU and memory not resumed: ${err.message}`);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
203
266
|
/**
|
|
204
267
|
* A recovered session is this process's from now on: its row's heartbeat
|
|
205
268
|
* is stamped with this process, now. The row still carried the heartbeat
|
|
@@ -61,9 +61,12 @@ const MODELS = [
|
|
|
61
61
|
* migrated once per suite; the stubs are made before each test and removed
|
|
62
62
|
* after it, so a spec's own `sinon.restore()` can't leave the next test on
|
|
63
63
|
* the real database. Tests empty the tables they use.
|
|
64
|
+
*
|
|
65
|
+
* With `captureQueries`, `queries` holds the SQL each test ran, so a spec can
|
|
66
|
+
* ask SQLite how it reads a table (`EXPLAIN QUERY PLAN`).
|
|
64
67
|
*/
|
|
65
|
-
function useScratchDatabase() {
|
|
66
|
-
const ctx = {};
|
|
68
|
+
function useScratchDatabase(options = {}) {
|
|
69
|
+
const ctx = { queries: [] };
|
|
67
70
|
const sandbox = sinon_1.default.createSandbox();
|
|
68
71
|
let dbPath = '';
|
|
69
72
|
before(function () {
|
|
@@ -75,9 +78,20 @@ function useScratchDatabase() {
|
|
|
75
78
|
env: Object.assign(Object.assign({}, process.env), { DATABASE_URL: url }),
|
|
76
79
|
stdio: 'pipe',
|
|
77
80
|
});
|
|
78
|
-
|
|
81
|
+
if (options.captureQueries) {
|
|
82
|
+
const db = new client_1.PrismaClient({
|
|
83
|
+
datasources: { db: { url } },
|
|
84
|
+
log: [{ emit: 'event', level: 'query' }],
|
|
85
|
+
});
|
|
86
|
+
db.$on('query', (e) => ctx.queries.push({ query: e.query, params: e.params }));
|
|
87
|
+
ctx.db = db;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
ctx.db = new client_1.PrismaClient({ datasources: { db: { url } } });
|
|
91
|
+
}
|
|
79
92
|
});
|
|
80
93
|
beforeEach(() => {
|
|
94
|
+
ctx.queries.length = 0;
|
|
81
95
|
for (const model of MODELS) {
|
|
82
96
|
const wrapper = prisma_1.prisma[model];
|
|
83
97
|
const delegate = ctx.db[model];
|
|
@@ -143,4 +143,14 @@ describe('EventManager: CPU and memory sampling', () => {
|
|
|
143
143
|
yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStopped('s-metrics-2');
|
|
144
144
|
(0, chai_1.expect)(metrics.stop.calledOnceWithExactly('s-metrics-2')).to.equal(true);
|
|
145
145
|
}));
|
|
146
|
+
it("hands the service a node's session, to collect its figures from the node", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
147
|
+
const { session, device } = startSession();
|
|
148
|
+
const remote = Object.assign(session, {
|
|
149
|
+
nodeOrigin: () => 'http://node:4723',
|
|
150
|
+
nodeMetrics: () => __awaiter(void 0, void 0, void 0, function* () { return ({ kind: 'refused' }); }),
|
|
151
|
+
});
|
|
152
|
+
yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStarted({}, remote, device);
|
|
153
|
+
(0, chai_1.expect)(metrics.appliesTo.firstCall.args).to.deep.equal([device, remote]);
|
|
154
|
+
(0, chai_1.expect)(metrics.start.firstCall.args[0].source).to.equal(remote);
|
|
155
|
+
}));
|
|
146
156
|
});
|
|
@@ -0,0 +1,330 @@
|
|
|
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 express_1 = __importDefault(require("express"));
|
|
18
|
+
const fs_1 = __importDefault(require("fs"));
|
|
19
|
+
const http_1 = __importDefault(require("http"));
|
|
20
|
+
const os_1 = __importDefault(require("os"));
|
|
21
|
+
const path_1 = __importDefault(require("path"));
|
|
22
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
23
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
24
|
+
const typedi_1 = require("typedi");
|
|
25
|
+
const plugin_1 = require("../../src/plugin");
|
|
26
|
+
const registerCommandAuth_1 = require("../../src/app/registerCommandAuth");
|
|
27
|
+
const defaultGateway_1 = require("../../src/gateway/defaultGateway");
|
|
28
|
+
const hubSessionToken_1 = require("../../src/gateway/hubSessionToken");
|
|
29
|
+
const nodeSessionStatus_1 = require("../../src/gateway/nodeSessionStatus");
|
|
30
|
+
const nodeSessionMetrics_1 = require("../../src/gateway/nodeSessionMetrics");
|
|
31
|
+
const SessionMetricsService_1 = require("../../src/services/metrics/SessionMetricsService");
|
|
32
|
+
const NodeMetricsStore_1 = require("../../src/services/metrics/NodeMetricsStore");
|
|
33
|
+
const NodeMetricsCollector_1 = require("../../src/services/metrics/NodeMetricsCollector");
|
|
34
|
+
const nodeMetrics_1 = require("../../src/services/metrics/nodeMetrics");
|
|
35
|
+
const JwtKeyService_1 = require("../../src/services/token/JwtKeyService");
|
|
36
|
+
const PluginContext_1 = require("../../src/PluginContext");
|
|
37
|
+
const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
|
|
38
|
+
const device_managers_1 = require("../../src/device-managers");
|
|
39
|
+
const device_store_1 = require("../../src/data-service/device-store");
|
|
40
|
+
const prisma_store_1 = require("../../src/data-service/prisma-store");
|
|
41
|
+
const SessionManager_1 = require("../../src/sessions/SessionManager");
|
|
42
|
+
const RemoteSession_1 = require("../../src/sessions/RemoteSession");
|
|
43
|
+
const appiumUmbrella_1 = require("../../src/sessions/appiumUmbrella");
|
|
44
|
+
const config_1 = require("../../src/config");
|
|
45
|
+
const container_registration_1 = require("../helpers/container-registration");
|
|
46
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
47
|
+
const appium_umbrella_1 = require("../helpers/appium-umbrella");
|
|
48
|
+
/**
|
|
49
|
+
* A hub collects CPU and memory for a session on a node's phone from the
|
|
50
|
+
* node: against a node in Appium 3's own server() (Appium's umbrella with
|
|
51
|
+
* Xenon's plugin, the production node gateway, and the node's session-status
|
|
52
|
+
* and session-metrics routes), with per-command auth on, so every ask carries
|
|
53
|
+
* the hub's session token. The node's phone sampler is replaced by one the
|
|
54
|
+
* test drives; everything between it and the hub's writes is the real thing.
|
|
55
|
+
*/
|
|
56
|
+
const quiet = { info: () => undefined, warn: () => undefined, error: () => undefined };
|
|
57
|
+
const NODE_ID = 'node-1';
|
|
58
|
+
/** The node's sampler service, its phone sampler replaced by one the test drives. */
|
|
59
|
+
class NodeSideMetrics extends SessionMetricsService_1.SessionMetricsService {
|
|
60
|
+
constructor() {
|
|
61
|
+
super(...arguments);
|
|
62
|
+
this.hooks = [];
|
|
63
|
+
this.stopped = 0;
|
|
64
|
+
}
|
|
65
|
+
samplerFor(_id, _d, _c, hooks) {
|
|
66
|
+
this.hooks.push(hooks);
|
|
67
|
+
return { start: () => undefined, stop: () => __awaiter(this, void 0, void 0, function* () { return void (this.stopped += 1); }) };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** The hub's sampler service: this hub's own phones are none, it asks the node every 50 ms. */
|
|
71
|
+
class HubSideMetrics extends SessionMetricsService_1.SessionMetricsService {
|
|
72
|
+
constructor() {
|
|
73
|
+
super(...arguments);
|
|
74
|
+
this.written = [];
|
|
75
|
+
}
|
|
76
|
+
context() {
|
|
77
|
+
return {
|
|
78
|
+
pluginArgs: Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { bindHostOrIp: '127.0.0.1' }),
|
|
79
|
+
port: 4799,
|
|
80
|
+
nodeId: 'hub-1',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
writeSamples(_id, samples) {
|
|
84
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
85
|
+
this.written.push(...samples.map((s) => s.at));
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
collectorFor(source, hooks, after) {
|
|
89
|
+
return new NodeMetricsCollector_1.NodeMetricsCollector({
|
|
90
|
+
source,
|
|
91
|
+
hooks,
|
|
92
|
+
after,
|
|
93
|
+
support: typedi_1.Container.get(nodeMetrics_1.NodeMetricsSupport),
|
|
94
|
+
logger: quiet,
|
|
95
|
+
intervalMs: 50,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const until = (ok_1, ...args_1) => __awaiter(void 0, [ok_1, ...args_1], void 0, function* (ok, ms = 5000) {
|
|
100
|
+
const end = Date.now() + ms;
|
|
101
|
+
while (!ok()) {
|
|
102
|
+
if (Date.now() > end)
|
|
103
|
+
throw new Error('timed out');
|
|
104
|
+
yield new Promise((r) => setTimeout(r, 20));
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
const at = (t) => ({
|
|
108
|
+
at: t,
|
|
109
|
+
deviceCpuPct: 12,
|
|
110
|
+
deviceMemMb: 900,
|
|
111
|
+
deviceMemTotalMb: 4000,
|
|
112
|
+
appCpuPct: 3,
|
|
113
|
+
appMemMb: 150,
|
|
114
|
+
appId: 'com.android.settings',
|
|
115
|
+
});
|
|
116
|
+
describe('a hub collects a node session’s CPU and memory', function () {
|
|
117
|
+
this.timeout(60000);
|
|
118
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
119
|
+
const servers = [];
|
|
120
|
+
let dirs;
|
|
121
|
+
let hubKeys;
|
|
122
|
+
let appiumLogs;
|
|
123
|
+
let restore;
|
|
124
|
+
let saved;
|
|
125
|
+
let authDisabledBefore;
|
|
126
|
+
let commandAuthBefore;
|
|
127
|
+
let umbrella;
|
|
128
|
+
let hubUrl;
|
|
129
|
+
let nodeOrigin;
|
|
130
|
+
let nodeMetrics;
|
|
131
|
+
before(() => __awaiter(this, void 0, void 0, function* () {
|
|
132
|
+
appiumLogs = (0, appium_umbrella_1.quietAppiumLogs)();
|
|
133
|
+
dirs = [fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-heartbeat-keys-'))];
|
|
134
|
+
hubKeys = new JwtKeyService_1.JwtKeyService();
|
|
135
|
+
yield hubKeys.init(dirs[0]);
|
|
136
|
+
}));
|
|
137
|
+
after(() => {
|
|
138
|
+
appiumLogs.restore();
|
|
139
|
+
for (const dir of dirs)
|
|
140
|
+
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
141
|
+
});
|
|
142
|
+
beforeEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
143
|
+
restore = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, hubSessionToken_1.HubSessionTokenIssuer, device_managers_1.XenonManager, appiumUmbrella_1.AppiumUmbrella, nodeSessionStatus_1.NodeSessionProbeSupport, SessionMetricsService_1.SessionMetricsService, NodeMetricsStore_1.NodeMetricsStore, nodeMetrics_1.NodeMetricsSupport);
|
|
144
|
+
nodeMetrics = new NodeSideMetrics();
|
|
145
|
+
typedi_1.Container.set(SessionMetricsService_1.SessionMetricsService, nodeMetrics);
|
|
146
|
+
typedi_1.Container.set(NodeMetricsStore_1.NodeMetricsStore, new NodeMetricsStore_1.NodeMetricsStore());
|
|
147
|
+
typedi_1.Container.set(nodeMetrics_1.NodeMetricsSupport, new nodeMetrics_1.NodeMetricsSupport());
|
|
148
|
+
// One process plays both sides: the hub's keys sign, the node checks them.
|
|
149
|
+
typedi_1.Container.set(JwtKeyService_1.JwtKeyService, hubKeys);
|
|
150
|
+
typedi_1.Container.set(hubSessionToken_1.HubSessionTokenIssuer, new hubSessionToken_1.HubSessionTokenIssuer());
|
|
151
|
+
typedi_1.Container.set(appiumUmbrella_1.AppiumUmbrella, new appiumUmbrella_1.AppiumUmbrella());
|
|
152
|
+
typedi_1.Container.set(nodeSessionStatus_1.NodeSessionProbeSupport, new nodeSessionStatus_1.NodeSessionProbeSupport());
|
|
153
|
+
typedi_1.Container.set(device_managers_1.XenonManager, {
|
|
154
|
+
getMaxSessionCount: () => undefined,
|
|
155
|
+
deviceInstances: () => __awaiter(this, void 0, void 0, function* () { return []; }),
|
|
156
|
+
});
|
|
157
|
+
const context = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
158
|
+
saved = {
|
|
159
|
+
store: device_store_1.DeviceStoreFactory._deviceStore,
|
|
160
|
+
pending: device_store_1.DeviceStoreFactory._pendingSessionStore,
|
|
161
|
+
context: Object.assign({}, context),
|
|
162
|
+
};
|
|
163
|
+
device_store_1.DeviceStoreFactory._deviceStore = new prisma_store_1.PrismaDeviceStore();
|
|
164
|
+
device_store_1.DeviceStoreFactory._pendingSessionStore = new prisma_store_1.PrismaPendingSessionStore();
|
|
165
|
+
authDisabledBefore = config_1.config.authDisabled;
|
|
166
|
+
config_1.config.authDisabled = false;
|
|
167
|
+
// Per-command auth on: the node then takes the hub's token and nothing else.
|
|
168
|
+
commandAuthBefore = process.env.XENON_REQUIRE_COMMAND_AUTH;
|
|
169
|
+
process.env.XENON_REQUIRE_COMMAND_AUTH = '1';
|
|
170
|
+
yield scratch.db.pendingSession.deleteMany({});
|
|
171
|
+
yield scratch.db.device.deleteMany({});
|
|
172
|
+
}));
|
|
173
|
+
afterEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
174
|
+
for (const s of SessionManager_1.SESSION_MANAGER.getAllSessions())
|
|
175
|
+
SessionManager_1.SESSION_MANAGER.removeSession(s.getId());
|
|
176
|
+
device_store_1.DeviceStoreFactory._deviceStore = saved.store;
|
|
177
|
+
device_store_1.DeviceStoreFactory._pendingSessionStore = saved.pending;
|
|
178
|
+
Object.assign(typedi_1.Container.get(PluginContext_1.PluginContext), saved.context);
|
|
179
|
+
config_1.config.authDisabled = authDisabledBefore;
|
|
180
|
+
if (commandAuthBefore === undefined)
|
|
181
|
+
delete process.env.XENON_REQUIRE_COMMAND_AUTH;
|
|
182
|
+
else
|
|
183
|
+
process.env.XENON_REQUIRE_COMMAND_AUTH = commandAuthBefore;
|
|
184
|
+
sinon_1.default.restore();
|
|
185
|
+
restore();
|
|
186
|
+
for (const s of servers.splice(0)) {
|
|
187
|
+
s.closeAllConnections();
|
|
188
|
+
yield new Promise((resolve) => http_1.default.Server.prototype.close.call(s, () => resolve()));
|
|
189
|
+
}
|
|
190
|
+
}));
|
|
191
|
+
const port = (s) => s.address().port;
|
|
192
|
+
/** A hub (its public keys) and a node. */
|
|
193
|
+
function boot() {
|
|
194
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
195
|
+
const baseDriver = (0, appium_umbrella_1.appiumBaseDriver)();
|
|
196
|
+
const hubServer = yield baseDriver.server({
|
|
197
|
+
routeConfiguringFunction: baseDriver.routeConfiguringFunction((0, appium_umbrella_1.umbrellaWith)([])),
|
|
198
|
+
port: 0,
|
|
199
|
+
hostname: '127.0.0.1',
|
|
200
|
+
basePath: '/wd/hub',
|
|
201
|
+
cliArgs: { basePath: '/wd/hub' },
|
|
202
|
+
serverUpdaters: [
|
|
203
|
+
(app) => {
|
|
204
|
+
app.get('/xenon/api/auth/jwks.json', (_req, res) => res.json(hubKeys.jwks()));
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
});
|
|
208
|
+
servers.push(hubServer);
|
|
209
|
+
hubUrl = `http://127.0.0.1:${port(hubServer)}`;
|
|
210
|
+
const pluginArgs = Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { hub: hubUrl, bindHostOrIp: '127.0.0.1', enableDashboard: false, deviceAvailabilityTimeoutMs: 1000, deviceAvailabilityQueryIntervalMs: 100 });
|
|
211
|
+
umbrella = (0, appium_umbrella_1.umbrellaWith)([[plugin_1.XenonPlugin, 'xenon']]);
|
|
212
|
+
const nodeServer = yield baseDriver.server({
|
|
213
|
+
routeConfiguringFunction: baseDriver.routeConfiguringFunction(umbrella),
|
|
214
|
+
port: 0,
|
|
215
|
+
hostname: '127.0.0.1',
|
|
216
|
+
basePath: '/node',
|
|
217
|
+
cliArgs: { basePath: '/node' },
|
|
218
|
+
serverUpdaters: [
|
|
219
|
+
(app) => {
|
|
220
|
+
(0, registerCommandAuth_1.registerSessionGateway)(app, { basePath: '/node' }, (0, registerCommandAuth_1.commandAuthDeps)({ enabled: () => true, authDisabled: () => false, logger: quiet }), (0, defaultGateway_1.nodeGatewayOptions)(hubUrl));
|
|
221
|
+
// What ServerManager mounts under /xenon/api on a node.
|
|
222
|
+
const api = express_1.default.Router();
|
|
223
|
+
(0, nodeSessionStatus_1.registerNodeSessionStatus)(api, pluginArgs);
|
|
224
|
+
(0, nodeSessionMetrics_1.registerNodeSessionMetrics)(api, pluginArgs);
|
|
225
|
+
app.use('/xenon/api', api);
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
});
|
|
229
|
+
servers.push(nodeServer);
|
|
230
|
+
nodeOrigin = `http://127.0.0.1:${port(nodeServer)}`;
|
|
231
|
+
typedi_1.Container.get(PluginContext_1.PluginContext).setContext(pluginArgs, port(nodeServer), NODE_ID, '/node');
|
|
232
|
+
yield scratch.db.device.create({
|
|
233
|
+
data: {
|
|
234
|
+
udid: 'phone-1',
|
|
235
|
+
host: nodeOrigin,
|
|
236
|
+
nodeId: NODE_ID,
|
|
237
|
+
platform: 'android',
|
|
238
|
+
name: 'phone-1',
|
|
239
|
+
sdk: '14',
|
|
240
|
+
busy: false,
|
|
241
|
+
userBlocked: false,
|
|
242
|
+
offline: false,
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
/** A session on the node, created as the hub creates it. */
|
|
248
|
+
function nodeSession() {
|
|
249
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
250
|
+
const token = yield typedi_1.Container.get(hubSessionToken_1.HubSessionTokenIssuer).createTokenFor({
|
|
251
|
+
userId: 'alice',
|
|
252
|
+
udid: 'phone-1',
|
|
253
|
+
host: nodeOrigin,
|
|
254
|
+
});
|
|
255
|
+
const res = yield (0, loopbackRequest_1.default)(nodeOrigin)
|
|
256
|
+
.post('/node/session')
|
|
257
|
+
.set(hubSessionToken_1.HUB_TOKEN_HEADER, token)
|
|
258
|
+
.send({
|
|
259
|
+
capabilities: {
|
|
260
|
+
alwaysMatch: {
|
|
261
|
+
platformName: 'Android',
|
|
262
|
+
'appium:automationName': appium_umbrella_1.FAKE_AUTOMATION,
|
|
263
|
+
'appium:newCommandTimeout': 0,
|
|
264
|
+
'appium:udid': 'phone-1',
|
|
265
|
+
'xe:options': { recordVideo: false },
|
|
266
|
+
},
|
|
267
|
+
firstMatch: [{}],
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
(0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
|
|
271
|
+
return res.body.value.sessionId;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
/** The hub's object for that session. */
|
|
275
|
+
const hubSide = (sessionId) => new RemoteSession_1.RemoteSession({
|
|
276
|
+
sessionId,
|
|
277
|
+
device: { udid: 'phone-1', host: nodeOrigin, nodeId: NODE_ID, platform: 'android' },
|
|
278
|
+
sessionResponse: {},
|
|
279
|
+
xenonOption: {},
|
|
280
|
+
baseUrl: `${nodeOrigin}/node`,
|
|
281
|
+
});
|
|
282
|
+
it("the hub collects a node session's figures as it runs, and the last ones when it ends", () => __awaiter(this, void 0, void 0, function* () {
|
|
283
|
+
yield boot();
|
|
284
|
+
const sessionId = yield nodeSession();
|
|
285
|
+
// The node samples the session the hub created, its dashboard off.
|
|
286
|
+
(0, chai_1.expect)(nodeMetrics.hooks).to.have.length(1);
|
|
287
|
+
nodeMetrics.hooks[0].onSample(at(1000));
|
|
288
|
+
nodeMetrics.hooks[0].onSample(at(2000));
|
|
289
|
+
const hub = new HubSideMetrics();
|
|
290
|
+
const device = {
|
|
291
|
+
udid: 'phone-1',
|
|
292
|
+
host: nodeOrigin,
|
|
293
|
+
nodeId: NODE_ID,
|
|
294
|
+
platform: 'android',
|
|
295
|
+
realDevice: true,
|
|
296
|
+
};
|
|
297
|
+
hub.start({ sessionId, device, capabilities: {}, source: hubSide(sessionId) });
|
|
298
|
+
(0, chai_1.expect)(hub.recordingState(sessionId)).to.equal('sampling');
|
|
299
|
+
// Live: asked with the hub's token (per-command auth is on), every 50 ms here.
|
|
300
|
+
yield until(() => hub.written.length === 0 &&
|
|
301
|
+
typedi_1.Container.get(NodeMetricsStore_1.NodeMetricsStore).read(sessionId, null).samples.length === 0);
|
|
302
|
+
nodeMetrics.hooks[0].onSample(at(3000));
|
|
303
|
+
const token = yield typedi_1.Container.get(hubSessionToken_1.HubSessionTokenIssuer).tokenFor(sessionId);
|
|
304
|
+
const del = yield (0, loopbackRequest_1.default)(nodeOrigin)
|
|
305
|
+
.delete(`/node/session/${sessionId}`)
|
|
306
|
+
.set(hubSessionToken_1.HUB_TOKEN_HEADER, token);
|
|
307
|
+
(0, chai_1.expect)(del.status, JSON.stringify(del.body)).to.equal(200);
|
|
308
|
+
(0, chai_1.expect)(nodeMetrics.stopped).to.equal(1);
|
|
309
|
+
(0, chai_1.expect)(typedi_1.Container.get(NodeMetricsStore_1.NodeMetricsStore).read(sessionId, 2000).state).to.equal('ended');
|
|
310
|
+
yield hub.stop(sessionId);
|
|
311
|
+
(0, chai_1.expect)(hub.written).to.deep.equal([1000, 2000, 3000]);
|
|
312
|
+
}));
|
|
313
|
+
it("an older node's sessions say they aren't recorded", () => __awaiter(this, void 0, void 0, function* () {
|
|
314
|
+
yield boot();
|
|
315
|
+
const sessionId = yield nodeSession();
|
|
316
|
+
const hub = new HubSideMetrics();
|
|
317
|
+
const device = {
|
|
318
|
+
udid: 'phone-1',
|
|
319
|
+
host: nodeOrigin,
|
|
320
|
+
nodeId: NODE_ID,
|
|
321
|
+
platform: 'android',
|
|
322
|
+
realDevice: true,
|
|
323
|
+
};
|
|
324
|
+
const older = Object.assign(hubSide(sessionId), {
|
|
325
|
+
nodeMetrics: () => __awaiter(this, void 0, void 0, function* () { return ({ kind: 'unsupported', status: 404 }); }),
|
|
326
|
+
});
|
|
327
|
+
hub.start({ sessionId, device, capabilities: {}, source: older });
|
|
328
|
+
yield until(() => hub.recordingState(sessionId) === 'off');
|
|
329
|
+
}));
|
|
330
|
+
});
|