@xenon-device-management/xenon 2.10.1 → 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-defSfi9B.js → AnnotationOverlay-BQPQ5bPF.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-DS7dJ2bU.js → ApiKeyGate-Bd7Pumwv.js} +1 -1
- package/lib/public/assets/{BugReportButton-CeD8SUnQ.js → BugReportButton-iC7fxPxl.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-Dgue7t7s.js → DeviceMosaicView-CNdES7nm.js} +1 -1
- package/lib/public/assets/{EmptyState-Bo-vvxiy.js → EmptyState-wZeJbGhN.js} +1 -1
- package/lib/public/assets/{FieldGroup-DdhTNAXZ.js → FieldGroup-tpRfmhlE.js} +1 -1
- package/lib/public/assets/{FilterMenu-J3F_3fDw.js → FilterMenu-BZwOppId.js} +1 -1
- package/lib/public/assets/{Menu-Bxluaw2m.js → Menu-CahoAx8L.js} +1 -1
- package/lib/public/assets/{Modal-qgMVFrqV.js → Modal-mcM5W_Ji.js} +1 -1
- package/lib/public/assets/{RecordingPage-BUT7WgJB.js → RecordingPage-B4dFBndn.js} +1 -1
- package/lib/public/assets/{RecordingsPage-CY0OIgCo.js → RecordingsPage-BOez3f_9.js} +1 -1
- package/lib/public/assets/{SegmentedControl-Cq-fybZ2.js → SegmentedControl-qMSiMN1d.js} +1 -1
- package/lib/public/assets/{SettingCard-CASWJr7h.js → SettingCard-rcPtJ7HG.js} +1 -1
- package/lib/public/assets/{Table-BSO_dZzu.js → Table-J3uognu0.js} +1 -1
- package/lib/public/assets/{activity-B_-MNwwR.js → activity-_5pNSfKT.js} +1 -1
- package/lib/public/assets/{ai-settings-DFhemyo8.js → ai-settings-DTPRbkei.js} +1 -1
- package/lib/public/assets/{api-keys-DV1oB8Tf.js → api-keys-CxkIUo76.js} +1 -1
- package/lib/public/assets/apps-BQKfuN3J.css +1 -0
- package/lib/public/assets/{apps-B5f-VuLN.js → apps-Bv0pAfxz.js} +2 -2
- package/lib/public/assets/{arrow-left-D_xIcVqg.js → arrow-left-uY7F6z1K.js} +1 -1
- package/lib/public/assets/{arrow-right-CLGlL_Un.js → arrow-right-BbAm_2Wa.js} +1 -1
- package/lib/public/assets/{arrow-up-right-CuIgWFj2.js → arrow-up-right-x4H5NuvE.js} +1 -1
- package/lib/public/assets/{auth-shell-SgAkrFQg.js → auth-shell-CtMFhbnC.js} +2 -2
- package/lib/public/assets/{builds-page-h9ZJ8n74.js → builds-page-B_O5uBSZ.js} +1 -1
- package/lib/public/assets/{button-DKl94fzV.js → button-CQoMQEj_.js} +1 -1
- package/lib/public/assets/{calendar-BNLTjBsT.js → calendar-BZkpCqlL.js} +1 -1
- package/lib/public/assets/{check-1u_3Zsx5.js → check-CAB6RYuc.js} +1 -1
- package/lib/public/assets/{chevron-right-Cn2BixQX.js → chevron-right-ec0unu3u.js} +1 -1
- package/lib/public/assets/{circle-check-OymVCeGU.js → circle-check-Dfg-gpjg.js} +1 -1
- package/lib/public/assets/{circle-x-D3ZyFWOd.js → circle-x-cbPq6Btp.js} +1 -1
- package/lib/public/assets/{clock-DwzGN-aa.js → clock-DU9cBLi8.js} +1 -1
- package/lib/public/assets/{copy-De43WY6T.js → copy-VM9kWPpE.js} +1 -1
- package/lib/public/assets/{device-explorer-CtvEmCC5.js → device-explorer-ai0usQ8m.js} +1 -1
- package/lib/public/assets/{download-DcSdKSvR.js → download-WgW4AVid.js} +1 -1
- package/lib/public/assets/{forgot-password-DDjM8J7Z.js → forgot-password-BGRpjTtt.js} +1 -1
- package/lib/public/assets/{index-Bqt1dmMA.js → index-C33GjwPr.js} +2 -2
- package/lib/public/assets/{index-CSNs0EE4.js → index-DNDdHt3h.js} +1 -1
- package/lib/public/assets/{input-fRGFwm1K.js → input-DwiVcO3Q.js} +1 -1
- package/lib/public/assets/{keycodes-Dj4m3JV7.js → keycodes-CvCtpVJD.js} +1 -1
- package/lib/public/assets/{line-chart-DHSZ8Sm4.js → line-chart-PE5e9PeN.js} +1 -1
- package/lib/public/assets/{list-checks-Co1gqqDn.js → list-checks-Bgn9GEEO.js} +1 -1
- package/lib/public/assets/{lock-BzutP2Yf.js → lock-Cb7My6hc.js} +1 -1
- package/lib/public/assets/{login-DSVZLgW2.js → login-CDmyzSpl.js} +1 -1
- package/lib/public/assets/{maintenance-settings-PYFNp_tc.js → maintenance-settings-Bq6pKoBt.js} +1 -1
- package/lib/public/assets/{monitor-DdKVA3ze.js → monitor-BZSwrnPV.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-C8UpXByt.js → mouse-pointer-2-BbFj8y8i.js} +1 -1
- package/lib/public/assets/{network-BkzHrr2L.js → network-DpwYGB0-.js} +1 -1
- package/lib/public/assets/{overview-0YFo6f97.js → overview-B4_IO-X8.js} +1 -1
- package/lib/public/assets/{page-header-DkmHEtLJ.js → page-header-BpLEuo7i.js} +1 -1
- package/lib/public/assets/{plus-BH6Y7G7y.js → plus-DHZS0EGU.js} +1 -1
- package/lib/public/assets/{profile-page-C1IDBP4l.js → profile-page-DsHDIq-Y.js} +1 -1
- package/lib/public/assets/{reset-password-yNMsl5fZ.js → reset-password-DuUjhw_l.js} +1 -1
- package/lib/public/assets/{runbook-page--fH5nToN.js → runbook-page-BI_ayvLd.js} +1 -1
- package/lib/public/assets/{select-DqjhZ6h2.js → select-xiba9PSx.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-CrYIFrUj.js → selector-detail-redirect-Emz2obK_.js} +1 -1
- package/lib/public/assets/{selector-health-page-BnRUmiS2.js → selector-health-page-zpwrgalC.js} +1 -1
- package/lib/public/assets/session-detail-page-BhlTJ1Oq.js +4 -0
- package/lib/public/assets/{settings-BcxYFxnE.js → settings-B-mabJFm.js} +1 -1
- package/lib/public/assets/{stat-tile-DxVLBgA7.js → stat-tile-lT8WcPgu.js} +1 -1
- package/lib/public/assets/{tablet-C_uKWfUK.js → tablet-DM8dp75P.js} +1 -1
- package/lib/public/assets/{teams-C9G2WisD.js → teams-CRkb5-Fe.js} +1 -1
- package/lib/public/assets/{trash-2-C4cJO0CG.js → trash-2-Dxgryn4o.js} +1 -1
- package/lib/public/assets/{upload-BN3PMMvP.js → upload-Ci0vz0BP.js} +1 -1
- package/lib/public/assets/{use-builds-data-CtelVuo_.js → use-builds-data-DaaqMjjL.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-CClcmiRh.js → use-password-reset-mode-BvIyZtaF.js} +1 -1
- package/lib/public/assets/{users-OdnOb63Y.js → users-BT8qx3ea.js} +1 -1
- package/lib/public/assets/{users-BK7fwGGA.js → users-CWYHk75q.js} +1 -1
- package/lib/public/assets/{video-off-DUAYoXTQ.js → video-off-BvY08g6I.js} +1 -1
- package/lib/public/assets/{webhook-settings-BRyvDWHI.js → webhook-settings-aHIlSJFP.js} +1 -1
- package/lib/public/assets/{zap-BiceX7DC.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/plugin.js +3 -0
- 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/sessions/RemoteSession.js +32 -0
- package/lib/src/sessions/SessionManager.js +63 -0
- 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/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/lib/public/assets/apps-DMhpZuKd.css +0 -1
- package/lib/public/assets/session-detail-page-BNh_QfHv.js +0 -4
|
@@ -10,6 +10,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.NodeSessionProbeSupport = exports.UNSUPPORTED_RECHECK_MS = exports.NODE_SESSION_STATUS_HEADER = exports.NODE_SESSION_STATUS_PATH = exports.NODE_SESSION_STATUS_ROUTE = void 0;
|
|
13
|
+
exports.answerForHub = answerForHub;
|
|
13
14
|
exports.nodeSessionStatusHandler = nodeSessionStatusHandler;
|
|
14
15
|
exports.registerNodeSessionStatus = registerNodeSessionStatus;
|
|
15
16
|
const typedi_1 = require("typedi");
|
|
@@ -54,34 +55,42 @@ exports.NODE_SESSION_STATUS_PATH = '/xenon/api/node/sessions';
|
|
|
54
55
|
exports.NODE_SESSION_STATUS_HEADER = 'x-xenon-node-sessions';
|
|
55
56
|
/** How long a hub keeps using the old probe for a node without the route before asking again. */
|
|
56
57
|
exports.UNSUPPORTED_RECHECK_MS = 10 * 60000;
|
|
58
|
+
/**
|
|
59
|
+
* Runs `answer` when the caller may know this session's state on the node:
|
|
60
|
+
* always with per-command auth off, else only with the hub's session token
|
|
61
|
+
* for it. Otherwise the unknown-session answer, or 503 when the token can't
|
|
62
|
+
* be checked. `what` names the route in the log.
|
|
63
|
+
*/
|
|
64
|
+
function answerForHub(deps, req, res, sessionId, what, answer) {
|
|
65
|
+
if (!deps.enforced())
|
|
66
|
+
return answer();
|
|
67
|
+
const presented = req.headers[hubSessionToken_1.HUB_TOKEN_HEADER];
|
|
68
|
+
const token = typeof presented === 'string' ? presented : undefined;
|
|
69
|
+
if (token === undefined) {
|
|
70
|
+
res.status(commandAuth_1.UNKNOWN_SESSION_STATUS).json(commandAuth_1.UNKNOWN_SESSION_BODY);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
deps.hubTokens
|
|
74
|
+
.verify(token, sessionId)
|
|
75
|
+
.then((valid) => {
|
|
76
|
+
if (valid)
|
|
77
|
+
return answer();
|
|
78
|
+
deps.logger.warn(`${what} for ${sessionId} refused: the hub token is not valid for it`);
|
|
79
|
+
res.status(commandAuth_1.UNKNOWN_SESSION_STATUS).json(commandAuth_1.UNKNOWN_SESSION_BODY);
|
|
80
|
+
})
|
|
81
|
+
.catch((error) => {
|
|
82
|
+
deps.logger.error(`${what} for ${sessionId} unavailable: hub token check failed: ${(0, commandAuth_1.summarize)(error)}`);
|
|
83
|
+
if (!res.headersSent) {
|
|
84
|
+
res.status(commandAuth_1.COMMAND_AUTH_UNAVAILABLE_STATUS).json(commandAuth_1.COMMAND_AUTH_UNAVAILABLE_BODY);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
57
88
|
function nodeSessionStatusHandler(deps) {
|
|
58
89
|
return (req, res) => {
|
|
59
90
|
var _a, _b;
|
|
60
91
|
res.setHeader(exports.NODE_SESSION_STATUS_HEADER, '1');
|
|
61
92
|
const sessionId = String((_b = (_a = req.params) === null || _a === void 0 ? void 0 : _a.sessionId) !== null && _b !== void 0 ? _b : '');
|
|
62
|
-
|
|
63
|
-
if (!deps.enforced())
|
|
64
|
-
return answer();
|
|
65
|
-
const presented = req.headers[hubSessionToken_1.HUB_TOKEN_HEADER];
|
|
66
|
-
const token = typeof presented === 'string' ? presented : undefined;
|
|
67
|
-
if (token === undefined) {
|
|
68
|
-
res.status(commandAuth_1.UNKNOWN_SESSION_STATUS).json(commandAuth_1.UNKNOWN_SESSION_BODY);
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
deps.hubTokens
|
|
72
|
-
.verify(token, sessionId)
|
|
73
|
-
.then((valid) => {
|
|
74
|
-
if (valid)
|
|
75
|
-
return answer();
|
|
76
|
-
deps.logger.warn(`Session status for ${sessionId} refused: the hub token is not valid for it`);
|
|
77
|
-
res.status(commandAuth_1.UNKNOWN_SESSION_STATUS).json(commandAuth_1.UNKNOWN_SESSION_BODY);
|
|
78
|
-
})
|
|
79
|
-
.catch((error) => {
|
|
80
|
-
deps.logger.error(`Session status for ${sessionId} unavailable: hub token check failed: ${(0, commandAuth_1.summarize)(error)}`);
|
|
81
|
-
if (!res.headersSent) {
|
|
82
|
-
res.status(commandAuth_1.COMMAND_AUTH_UNAVAILABLE_STATUS).json(commandAuth_1.COMMAND_AUTH_UNAVAILABLE_BODY);
|
|
83
|
-
}
|
|
84
|
-
});
|
|
93
|
+
answerForHub(deps, req, res, sessionId, 'Session status', () => res.status(200).json({ value: { sessionId, exists: deps.hasSession(sessionId) } }));
|
|
85
94
|
};
|
|
86
95
|
}
|
|
87
96
|
/**
|
package/lib/src/plugin.js
CHANGED
|
@@ -66,6 +66,7 @@ const IPluginArgs_1 = require("./interfaces/IPluginArgs");
|
|
|
66
66
|
const NodeDevices_1 = __importDefault(require("./device-managers/NodeDevices"));
|
|
67
67
|
const appiumUmbrella_1 = require("./sessions/appiumUmbrella");
|
|
68
68
|
const LiveSessionOwners_1 = require("./services/device-access/LiveSessionOwners");
|
|
69
|
+
const SessionMetricsService_1 = require("./services/metrics/SessionMetricsService");
|
|
69
70
|
const config_1 = require("./config");
|
|
70
71
|
const SessionManager_1 = require("./sessions/SessionManager");
|
|
71
72
|
const event_manager_1 = require("./dashboard/event-manager");
|
|
@@ -98,6 +99,8 @@ class XenonPlugin extends base_plugin_1.default {
|
|
|
98
99
|
var _a;
|
|
99
100
|
const sessionId = driver.sessionId;
|
|
100
101
|
typedi_1.Container.get(LiveSessionOwners_1.LiveSessionOwners).forget(sessionId);
|
|
102
|
+
if (sessionId)
|
|
103
|
+
yield typedi_1.Container.get(SessionMetricsService_1.SessionMetricsService).stop(sessionId);
|
|
101
104
|
const deviceFilter = {
|
|
102
105
|
session_id: sessionId ? sessionId : undefined,
|
|
103
106
|
udid: driver.caps && driver.caps.udid ? driver.caps.udid : undefined,
|
|
@@ -80,6 +80,7 @@ const callerTeamIds_1 = require("./device-access/callerTeamIds");
|
|
|
80
80
|
const queueVisibility_1 = require("./device-access/queueVisibility");
|
|
81
81
|
const appVisibility_1 = require("./device-access/appVisibility");
|
|
82
82
|
const LiveSessionOwners_1 = require("./device-access/LiveSessionOwners");
|
|
83
|
+
const SessionMetricsService_1 = require("./metrics/SessionMetricsService");
|
|
83
84
|
const appCapability_1 = require("./session/appCapability");
|
|
84
85
|
const CircuitBreaker_1 = require("../data-service/CircuitBreaker");
|
|
85
86
|
const nodeWebDriverUrl_1 = require("../gateway/nodeWebDriverUrl");
|
|
@@ -816,6 +817,16 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
816
817
|
if (sessionInstance instanceof LocalSession_1.LocalSession) {
|
|
817
818
|
typedi_1.Container.get(LiveSessionOwners_1.LiveSessionOwners).record(sessionId, userId);
|
|
818
819
|
}
|
|
820
|
+
// A node samples its own phones for its hub, whatever its dashboard
|
|
821
|
+
// setting (the hub collects the figures). A hub and a standalone server
|
|
822
|
+
// start sampling in EventManager.onSessionStarted, once the row exists.
|
|
823
|
+
if (!this.isHub(context.pluginArgs) && sessionInstance instanceof LocalSession_1.LocalSession) {
|
|
824
|
+
typedi_1.Container.get(SessionMetricsService_1.SessionMetricsService).start({
|
|
825
|
+
sessionId,
|
|
826
|
+
device: freshDevice,
|
|
827
|
+
capabilities: sessionResponse,
|
|
828
|
+
});
|
|
829
|
+
}
|
|
819
830
|
yield this.applyPostSessionLogic(sessionInstance, xenonCapabilities, freshDevice);
|
|
820
831
|
});
|
|
821
832
|
}
|
|
@@ -1104,6 +1115,10 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
1104
1115
|
finally {
|
|
1105
1116
|
if (sessionId) {
|
|
1106
1117
|
typedi_1.Container.get(LiveSessionOwners_1.LiveSessionOwners).forget(sessionId);
|
|
1118
|
+
// Before the lock's "still in memory?" check, which a dashboard-off
|
|
1119
|
+
// node fails. On a node this ends the figures it holds for its hub;
|
|
1120
|
+
// on a hub it collects a node session's last ones. Idempotent.
|
|
1121
|
+
yield typedi_1.Container.get(SessionMetricsService_1.SessionMetricsService).stop(sessionId);
|
|
1107
1122
|
yield sessionCleanupLock.acquire(sessionId, () => __awaiter(this, void 0, void 0, function* () {
|
|
1108
1123
|
const session = SessionManager_1.SESSION_MANAGER.getSession(sessionId);
|
|
1109
1124
|
if (!session) {
|
|
@@ -1238,6 +1253,7 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
1238
1253
|
}
|
|
1239
1254
|
SessionManager_1.SESSION_MANAGER.removeSession(sessionId);
|
|
1240
1255
|
typedi_1.Container.get(LiveSessionOwners_1.LiveSessionOwners).forget(sessionId);
|
|
1256
|
+
yield typedi_1.Container.get(SessionMetricsService_1.SessionMetricsService).stop(sessionId);
|
|
1241
1257
|
}));
|
|
1242
1258
|
});
|
|
1243
1259
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
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.NodeMetricsCollector = exports.COLLECT_INTERVAL_MS = void 0;
|
|
13
|
+
/** How often the hub asks a node for a session's new figures. */
|
|
14
|
+
exports.COLLECT_INTERVAL_MS = 10000;
|
|
15
|
+
/**
|
|
16
|
+
* On a hub: a session on a node's phone, sampled by asking the node
|
|
17
|
+
* (GET /node/sessions/:id/metrics) every COLLECT_INTERVAL_MS instead of
|
|
18
|
+
* reading the phone. Its samples go through the same hooks as a phone
|
|
19
|
+
* sampler's, so SessionMetricsService buffers and writes them as its own.
|
|
20
|
+
* Asks never overlap. An unreachable node is asked again; whether the
|
|
21
|
+
* session is alive is the heartbeat's call. When the session ends it asks
|
|
22
|
+
* once more, for the last seconds.
|
|
23
|
+
*/
|
|
24
|
+
class NodeMetricsCollector {
|
|
25
|
+
constructor(o) {
|
|
26
|
+
var _a;
|
|
27
|
+
this.o = o;
|
|
28
|
+
this.current = 'sampling';
|
|
29
|
+
this.done = false;
|
|
30
|
+
this.asking = null;
|
|
31
|
+
this.outage = false;
|
|
32
|
+
this.after = (_a = o.after) !== null && _a !== void 0 ? _a : null;
|
|
33
|
+
}
|
|
34
|
+
state() {
|
|
35
|
+
return this.current;
|
|
36
|
+
}
|
|
37
|
+
start() {
|
|
38
|
+
var _a, _b, _c;
|
|
39
|
+
const origin = this.o.source.nodeOrigin();
|
|
40
|
+
if (!origin || !this.o.support.shouldAsk(origin)) {
|
|
41
|
+
this.finish('off');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
this.timer = setInterval(() => void this.tick(), (_a = this.o.intervalMs) !== null && _a !== void 0 ? _a : exports.COLLECT_INTERVAL_MS);
|
|
45
|
+
(_c = (_b = this.timer).unref) === null || _c === void 0 ? void 0 : _c.call(_b);
|
|
46
|
+
}
|
|
47
|
+
stop() {
|
|
48
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
49
|
+
if (this.timer)
|
|
50
|
+
clearInterval(this.timer);
|
|
51
|
+
if (this.asking)
|
|
52
|
+
yield this.asking;
|
|
53
|
+
if (!this.done)
|
|
54
|
+
yield this.ask();
|
|
55
|
+
this.done = true;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
tick() {
|
|
59
|
+
if (this.done || this.asking)
|
|
60
|
+
return undefined;
|
|
61
|
+
this.asking = this.ask().finally(() => {
|
|
62
|
+
this.asking = null;
|
|
63
|
+
});
|
|
64
|
+
return this.asking;
|
|
65
|
+
}
|
|
66
|
+
ask() {
|
|
67
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
68
|
+
var _a;
|
|
69
|
+
const reply = yield this.o.source.nodeMetrics(this.after);
|
|
70
|
+
switch (reply.kind) {
|
|
71
|
+
case 'answer': {
|
|
72
|
+
if (this.outage) {
|
|
73
|
+
this.outage = false;
|
|
74
|
+
this.o.logger.info(`${this.o.source.nodeOrigin()} answers again`);
|
|
75
|
+
}
|
|
76
|
+
for (const s of reply.answer.samples) {
|
|
77
|
+
if (this.after !== null && s.at <= this.after)
|
|
78
|
+
continue;
|
|
79
|
+
this.o.hooks.onSample(s);
|
|
80
|
+
this.after = s.at;
|
|
81
|
+
}
|
|
82
|
+
const state = reply.answer.state;
|
|
83
|
+
if (state === 'sampling')
|
|
84
|
+
this.current = 'sampling';
|
|
85
|
+
else
|
|
86
|
+
this.finish(state === 'off' ? 'off' : 'stopped');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
case 'unsupported':
|
|
90
|
+
this.o.support.unsupported((_a = this.o.source.nodeOrigin()) !== null && _a !== void 0 ? _a : '', reply.status);
|
|
91
|
+
this.finish('off');
|
|
92
|
+
return;
|
|
93
|
+
case 'refused':
|
|
94
|
+
this.finish('off');
|
|
95
|
+
return;
|
|
96
|
+
case 'unavailable':
|
|
97
|
+
if (!this.outage) {
|
|
98
|
+
this.outage = true;
|
|
99
|
+
this.o.logger.warn(`Can't collect CPU and memory from ${this.o.source.nodeOrigin()}: ${reply.reason}. Asking again.`);
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
finish(state) {
|
|
106
|
+
// Figures already collected stay on the page: "stopped", never "off",
|
|
107
|
+
// which the panel shows as "isn't recorded" in their place.
|
|
108
|
+
this.current = state === 'off' && this.after !== null ? 'stopped' : state;
|
|
109
|
+
this.done = true;
|
|
110
|
+
if (this.timer)
|
|
111
|
+
clearInterval(this.timer);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
exports.NodeMetricsCollector = NodeMetricsCollector;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.NodeMetricsStore = void 0;
|
|
10
|
+
const typedi_1 = require("typedi");
|
|
11
|
+
const nodeMetrics_1 = require("./nodeMetrics");
|
|
12
|
+
const types_1 = require("./types");
|
|
13
|
+
/**
|
|
14
|
+
* On a node: each sampled session's CPU and memory, held in memory until the
|
|
15
|
+
* hub collects them. A node has no Session row for a session its hub created,
|
|
16
|
+
* so it can't write them to SessionMetric. The newest MAX_BUFFERED_SAMPLES per
|
|
17
|
+
* session; what the hub has collected is dropped; an ended session is kept
|
|
18
|
+
* KEEP_AFTER_END_MS, then forgotten, whether or not a hub asked.
|
|
19
|
+
*/
|
|
20
|
+
let NodeMetricsStore = class NodeMetricsStore {
|
|
21
|
+
constructor() {
|
|
22
|
+
this.now = () => Date.now();
|
|
23
|
+
this.entries = new Map();
|
|
24
|
+
}
|
|
25
|
+
begin(sessionId, platform) {
|
|
26
|
+
this.prune();
|
|
27
|
+
this.entries.set(sessionId, {
|
|
28
|
+
platform: platform.toLowerCase(),
|
|
29
|
+
state: 'sampling',
|
|
30
|
+
samples: [],
|
|
31
|
+
endedAt: null,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
add(sessionId, sample) {
|
|
35
|
+
const entry = this.entries.get(sessionId);
|
|
36
|
+
if (!entry)
|
|
37
|
+
return;
|
|
38
|
+
entry.samples.push(sample);
|
|
39
|
+
const over = entry.samples.length - types_1.MAX_BUFFERED_SAMPLES;
|
|
40
|
+
if (over > 0)
|
|
41
|
+
entry.samples.splice(0, over);
|
|
42
|
+
}
|
|
43
|
+
gaveUp(sessionId) {
|
|
44
|
+
const entry = this.entries.get(sessionId);
|
|
45
|
+
if (entry && entry.state === 'sampling')
|
|
46
|
+
entry.state = 'stopped';
|
|
47
|
+
}
|
|
48
|
+
end(sessionId) {
|
|
49
|
+
const entry = this.entries.get(sessionId);
|
|
50
|
+
if (!entry)
|
|
51
|
+
return;
|
|
52
|
+
entry.state = 'ended';
|
|
53
|
+
entry.endedAt = this.now();
|
|
54
|
+
}
|
|
55
|
+
/** The samples newer than `after` (all, without it). Those at or before it the hub has, so they go. */
|
|
56
|
+
read(sessionId, after) {
|
|
57
|
+
this.prune();
|
|
58
|
+
const entry = this.entries.get(sessionId);
|
|
59
|
+
if (!entry)
|
|
60
|
+
return { platform: '', state: 'off', samples: [] };
|
|
61
|
+
if (after !== null)
|
|
62
|
+
entry.samples = entry.samples.filter((s) => s.at > after);
|
|
63
|
+
return { platform: entry.platform, state: entry.state, samples: entry.samples.slice() };
|
|
64
|
+
}
|
|
65
|
+
prune() {
|
|
66
|
+
const cutoff = this.now() - nodeMetrics_1.KEEP_AFTER_END_MS;
|
|
67
|
+
for (const [id, entry] of this.entries) {
|
|
68
|
+
if (entry.endedAt !== null && entry.endedAt <= cutoff)
|
|
69
|
+
this.entries.delete(id);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
exports.NodeMetricsStore = NodeMetricsStore;
|
|
74
|
+
exports.NodeMetricsStore = NodeMetricsStore = __decorate([
|
|
75
|
+
(0, typedi_1.Service)()
|
|
76
|
+
], NodeMetricsStore);
|
|
@@ -34,6 +34,9 @@ const ProcessRegistry_1 = require("../ProcessRegistry");
|
|
|
34
34
|
const androidMetrics_1 = require("./androidMetrics");
|
|
35
35
|
const AndroidMetricsSampler_1 = require("./AndroidMetricsSampler");
|
|
36
36
|
const IOSMetricsSampler_1 = require("./IOSMetricsSampler");
|
|
37
|
+
const NodeMetricsStore_1 = require("./NodeMetricsStore");
|
|
38
|
+
const NodeMetricsCollector_1 = require("./NodeMetricsCollector");
|
|
39
|
+
const nodeMetrics_1 = require("./nodeMetrics");
|
|
37
40
|
const types_1 = require("./types");
|
|
38
41
|
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
39
42
|
/** One adb call of a sample; one that hangs this long is a failed sample. */
|
|
@@ -42,15 +45,18 @@ const ADB_TIMEOUT_MS = 5000;
|
|
|
42
45
|
* CPU and memory for the session page's Performance panel. A sampler per
|
|
43
46
|
* session on this server's own phones, from EventManager's start (once the
|
|
44
47
|
* session's row exists) to its stop; samples are written every
|
|
45
|
-
* FLUSH_INTERVAL_MS.
|
|
48
|
+
* FLUSH_INTERVAL_MS. On a hub, a session on a node's phone gets a
|
|
49
|
+
* NodeMetricsCollector instead, which asks the node. On a node the samples
|
|
50
|
+
* are held for the hub (NodeMetricsStore), not written. Sampling never fails
|
|
51
|
+
* a session.
|
|
46
52
|
*/
|
|
47
53
|
let SessionMetricsService = class SessionMetricsService {
|
|
48
54
|
constructor() {
|
|
49
55
|
this.log = logger_1.default.scope('SessionMetrics');
|
|
50
56
|
this.running = new Map();
|
|
51
57
|
}
|
|
52
|
-
/** Whether a session on this phone is sampled. */
|
|
53
|
-
appliesTo(device) {
|
|
58
|
+
/** Whether a session on this phone is sampled: this server's phone, or a node's through its session. */
|
|
59
|
+
appliesTo(device, source) {
|
|
54
60
|
var _a, _b;
|
|
55
61
|
if (!device)
|
|
56
62
|
return false;
|
|
@@ -62,47 +68,77 @@ let SessionMetricsService = class SessionMetricsService {
|
|
|
62
68
|
if (platform !== 'android' && !(platform === 'ios' && device.realDevice === true)) {
|
|
63
69
|
return false;
|
|
64
70
|
}
|
|
71
|
+
if (this.ownPhone(device))
|
|
72
|
+
return true;
|
|
73
|
+
// A node's phone, on a hub: collected from the node. Never a cloud provider's.
|
|
74
|
+
return !!source && !device.cloud && !this.holdsForHub();
|
|
75
|
+
}
|
|
76
|
+
ownPhone(device) {
|
|
77
|
+
const ctx = this.context();
|
|
65
78
|
return (0, localDeviceHosts_1.isOwnDevice)((0, localDeviceHosts_1.localDeviceHosts)(ctx.pluginArgs, ctx.port), ctx.nodeId, device);
|
|
66
79
|
}
|
|
67
|
-
start({ sessionId, device, capabilities }) {
|
|
68
|
-
var _a, _b;
|
|
69
|
-
if (this.running.has(sessionId) || !this.appliesTo(device))
|
|
80
|
+
start({ sessionId, device, capabilities, source, after }) {
|
|
81
|
+
var _a, _b, _c;
|
|
82
|
+
if (this.running.has(sessionId) || !this.appliesTo(device, source))
|
|
70
83
|
return;
|
|
84
|
+
// A node has no Session row for the hub's sessions, so it holds the
|
|
85
|
+
// figures for its hub to collect (GET /node/sessions/:id/metrics).
|
|
86
|
+
const held = this.holdsForHub();
|
|
87
|
+
const store = held ? this.nodeStore() : null;
|
|
88
|
+
// The entry's own buffer, not a lookup by session: `stop` removes the
|
|
89
|
+
// entry first, and the hub's collector gives its last samples while it
|
|
90
|
+
// stops (its last ask of the node).
|
|
91
|
+
const buffer = [];
|
|
71
92
|
const hooks = {
|
|
72
|
-
onSample: (s) => {
|
|
93
|
+
onSample: (s) => {
|
|
94
|
+
if (store)
|
|
95
|
+
store.add(sessionId, s);
|
|
96
|
+
else
|
|
97
|
+
buffer.push(s);
|
|
98
|
+
},
|
|
73
99
|
onGiveUp: (reason) => {
|
|
74
100
|
const entry = this.running.get(sessionId);
|
|
75
101
|
if (entry)
|
|
76
102
|
entry.gaveUp = true;
|
|
103
|
+
store === null || store === void 0 ? void 0 : store.gaveUp(sessionId);
|
|
77
104
|
this.log.warn(`[${sessionId}] Stopped sampling ${device.udid}: ${reason}`);
|
|
78
105
|
},
|
|
79
106
|
};
|
|
80
107
|
let sampler;
|
|
81
108
|
try {
|
|
82
|
-
sampler =
|
|
109
|
+
sampler =
|
|
110
|
+
source && !this.ownPhone(device)
|
|
111
|
+
? this.collectorFor(source, hooks, after !== null && after !== void 0 ? after : null)
|
|
112
|
+
: this.samplerFor(sessionId, device, capabilities, hooks);
|
|
83
113
|
}
|
|
84
114
|
catch (err) {
|
|
85
115
|
this.log.warn(`[${sessionId}] Can't sample ${device.udid}: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
86
116
|
return;
|
|
87
117
|
}
|
|
88
|
-
|
|
89
|
-
|
|
118
|
+
store === null || store === void 0 ? void 0 : store.begin(sessionId, String((_b = device.platform) !== null && _b !== void 0 ? _b : ''));
|
|
119
|
+
let flushTimer;
|
|
120
|
+
if (!held) {
|
|
121
|
+
flushTimer = setInterval(() => void this.flush(sessionId), types_1.FLUSH_INTERVAL_MS);
|
|
122
|
+
(_c = flushTimer.unref) === null || _c === void 0 ? void 0 : _c.call(flushTimer);
|
|
123
|
+
}
|
|
90
124
|
this.running.set(sessionId, {
|
|
91
125
|
sampler,
|
|
92
|
-
buffer
|
|
126
|
+
buffer,
|
|
93
127
|
flushTimer,
|
|
94
128
|
writing: Promise.resolve(),
|
|
95
129
|
gaveUp: false,
|
|
130
|
+
held,
|
|
96
131
|
});
|
|
97
132
|
sampler.start();
|
|
98
133
|
this.log.info(`[${sessionId}] Sampling CPU and memory on ${device.udid}`);
|
|
99
134
|
}
|
|
100
135
|
/** Whether this server is sampling a running session, stopped after giving up, or never did. */
|
|
101
136
|
recordingState(sessionId) {
|
|
137
|
+
var _a, _b, _c;
|
|
102
138
|
const entry = this.running.get(sessionId);
|
|
103
139
|
if (!entry)
|
|
104
140
|
return 'off';
|
|
105
|
-
return entry.gaveUp ? 'stopped' : 'sampling';
|
|
141
|
+
return (_c = (_b = (_a = entry.sampler).state) === null || _b === void 0 ? void 0 : _b.call(_a)) !== null && _c !== void 0 ? _c : (entry.gaveUp ? 'stopped' : 'sampling');
|
|
106
142
|
}
|
|
107
143
|
/** Stops the session's sampler and writes what it buffered. Idempotent. */
|
|
108
144
|
stop(sessionId) {
|
|
@@ -111,9 +147,13 @@ let SessionMetricsService = class SessionMetricsService {
|
|
|
111
147
|
if (!entry)
|
|
112
148
|
return;
|
|
113
149
|
this.running.delete(sessionId);
|
|
114
|
-
|
|
150
|
+
if (entry.flushTimer)
|
|
151
|
+
clearInterval(entry.flushTimer);
|
|
115
152
|
yield entry.sampler.stop().catch(() => undefined);
|
|
116
|
-
|
|
153
|
+
if (entry.held)
|
|
154
|
+
this.nodeStore().end(sessionId);
|
|
155
|
+
else
|
|
156
|
+
yield this.write(sessionId, entry);
|
|
117
157
|
});
|
|
118
158
|
}
|
|
119
159
|
flush(sessionId) {
|
|
@@ -149,6 +189,23 @@ let SessionMetricsService = class SessionMetricsService {
|
|
|
149
189
|
context() {
|
|
150
190
|
return typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
151
191
|
}
|
|
192
|
+
/** A node (a server with `hub`) holds the figures for its hub instead of writing them. */
|
|
193
|
+
holdsForHub() {
|
|
194
|
+
var _a;
|
|
195
|
+
return ((_a = this.context().pluginArgs) === null || _a === void 0 ? void 0 : _a.hub) !== undefined;
|
|
196
|
+
}
|
|
197
|
+
nodeStore() {
|
|
198
|
+
return typedi_1.Container.get(NodeMetricsStore_1.NodeMetricsStore);
|
|
199
|
+
}
|
|
200
|
+
collectorFor(source, hooks, after) {
|
|
201
|
+
return new NodeMetricsCollector_1.NodeMetricsCollector({
|
|
202
|
+
source,
|
|
203
|
+
hooks,
|
|
204
|
+
after,
|
|
205
|
+
support: typedi_1.Container.get(nodeMetrics_1.NodeMetricsSupport),
|
|
206
|
+
logger: this.log,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
152
209
|
writeSamples(sessionId, samples) {
|
|
153
210
|
return __awaiter(this, void 0, void 0, function* () {
|
|
154
211
|
yield prisma_1.prisma.sessionMetric.createMany({
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.NodeMetricsSupport = exports.NODE_METRICS_RECHECK_MS = exports.KEEP_AFTER_END_MS = exports.NODE_METRICS_HEADER = exports.NODE_METRICS_ROUTE = void 0;
|
|
13
|
+
exports.readNodeMetricsReply = readNodeMetricsReply;
|
|
14
|
+
exports.nodeMetricsSourceOf = nodeMetricsSourceOf;
|
|
15
|
+
const typedi_1 = require("typedi");
|
|
16
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
17
|
+
/** Where a node serves a session's figures, under `/xenon/api`. */
|
|
18
|
+
exports.NODE_METRICS_ROUTE = '/node/sessions/:sessionId/metrics';
|
|
19
|
+
/** On every answer of that route, so a hub can tell a node that has it. */
|
|
20
|
+
exports.NODE_METRICS_HEADER = 'x-xenon-node-metrics';
|
|
21
|
+
/** How long a node keeps an ended session's figures for its hub's last collection. */
|
|
22
|
+
exports.KEEP_AFTER_END_MS = 10 * 60000;
|
|
23
|
+
const STATES = ['sampling', 'stopped', 'ended', 'off'];
|
|
24
|
+
function readNodeMetricsReply(status, headers, data) {
|
|
25
|
+
var _a;
|
|
26
|
+
if (!(headers === null || headers === void 0 ? void 0 : headers[exports.NODE_METRICS_HEADER])) {
|
|
27
|
+
// An older node answers its login's 401, an unknown route's 404, or a
|
|
28
|
+
// catch-all's 2xx. Anything else without the header (a proxy's 502 in
|
|
29
|
+
// front of a node) is an outage: ask again, never give the node up.
|
|
30
|
+
if (status === 401 || status === 404 || (status >= 200 && status < 300)) {
|
|
31
|
+
return { kind: 'unsupported', status };
|
|
32
|
+
}
|
|
33
|
+
return { kind: 'unavailable', reason: `answered ${status}` };
|
|
34
|
+
}
|
|
35
|
+
if (status === 404)
|
|
36
|
+
return { kind: 'refused' };
|
|
37
|
+
const value = data === null || data === void 0 ? void 0 : data.value;
|
|
38
|
+
if (status === 200 && value && STATES.includes(value.state) && Array.isArray(value.samples)) {
|
|
39
|
+
return {
|
|
40
|
+
kind: 'answer',
|
|
41
|
+
answer: {
|
|
42
|
+
platform: String((_a = value.platform) !== null && _a !== void 0 ? _a : ''),
|
|
43
|
+
state: value.state,
|
|
44
|
+
samples: value.samples,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return { kind: 'unavailable', reason: `answered ${status}` };
|
|
49
|
+
}
|
|
50
|
+
/** The session as a NodeMetricsSource, when it is one. */
|
|
51
|
+
function nodeMetricsSourceOf(session) {
|
|
52
|
+
const s = session;
|
|
53
|
+
return s && typeof s.nodeMetrics === 'function' && typeof s.nodeOrigin === 'function'
|
|
54
|
+
? s
|
|
55
|
+
: undefined;
|
|
56
|
+
}
|
|
57
|
+
/** How long the hub leaves a node without the route alone before asking it again. */
|
|
58
|
+
exports.NODE_METRICS_RECHECK_MS = 10 * 60000;
|
|
59
|
+
/**
|
|
60
|
+
* Hub side: which nodes lack the route (an older Xenon). Their sessions show
|
|
61
|
+
* no figures; the hub says so once per node, and asks again after
|
|
62
|
+
* NODE_METRICS_RECHECK_MS, so a node upgraded in place is picked up.
|
|
63
|
+
*/
|
|
64
|
+
let NodeMetricsSupport = class NodeMetricsSupport {
|
|
65
|
+
constructor() {
|
|
66
|
+
this.logger = logger_1.default.scope('NodeMetrics');
|
|
67
|
+
this.now = () => Date.now();
|
|
68
|
+
this.unsupportedUntil = new Map();
|
|
69
|
+
this.warned = new Set();
|
|
70
|
+
}
|
|
71
|
+
shouldAsk(origin) {
|
|
72
|
+
const until = this.unsupportedUntil.get(origin);
|
|
73
|
+
if (until === undefined)
|
|
74
|
+
return true;
|
|
75
|
+
if (until > this.now())
|
|
76
|
+
return false;
|
|
77
|
+
this.unsupportedUntil.delete(origin);
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
unsupported(origin, status) {
|
|
81
|
+
this.unsupportedUntil.set(origin, this.now() + exports.NODE_METRICS_RECHECK_MS);
|
|
82
|
+
if (this.warned.has(origin))
|
|
83
|
+
return;
|
|
84
|
+
this.warned.add(origin);
|
|
85
|
+
this.logger.warn(`Node ${origin} has no session metrics route (answered ${status}): an older Xenon. ` +
|
|
86
|
+
'Its sessions show no CPU or memory. Upgrade the node.');
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
exports.NodeMetricsSupport = NodeMetricsSupport;
|
|
90
|
+
exports.NodeMetricsSupport = NodeMetricsSupport = __decorate([
|
|
91
|
+
(0, typedi_1.Service)()
|
|
92
|
+
], NodeMetricsSupport);
|
|
@@ -18,6 +18,7 @@ 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 nodeMetrics_1 = require("../services/metrics/nodeMetrics");
|
|
21
22
|
const nodeSessionStatus_1 = require("../gateway/nodeSessionStatus");
|
|
22
23
|
const XenonSession_1 = require("./XenonSession");
|
|
23
24
|
class RemoteSession extends XenonSession_1.XenonSession {
|
|
@@ -221,6 +222,37 @@ class RemoteSession extends XenonSession_1.XenonSession {
|
|
|
221
222
|
return null;
|
|
222
223
|
}
|
|
223
224
|
}
|
|
225
|
+
/** The node's origin, where its own routes are; null for a base URL that isn't one. */
|
|
226
|
+
nodeOrigin() {
|
|
227
|
+
try {
|
|
228
|
+
return new URL(this.baseUrl).origin;
|
|
229
|
+
}
|
|
230
|
+
catch (_a) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/** The node's CPU and memory for this session newer than `after` (NodeMetricsCollector). */
|
|
235
|
+
nodeMetrics(after) {
|
|
236
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
237
|
+
var _a, _b, _c;
|
|
238
|
+
const origin = this.nodeOrigin();
|
|
239
|
+
if (!origin)
|
|
240
|
+
return { kind: 'unavailable', reason: `no node origin in ${this.baseUrl}` };
|
|
241
|
+
try {
|
|
242
|
+
const response = yield this.call({
|
|
243
|
+
method: 'get',
|
|
244
|
+
url: `${origin}/xenon/api/node/sessions/${encodeURIComponent(this.sessionId)}/metrics`,
|
|
245
|
+
params: after === null ? undefined : { after },
|
|
246
|
+
timeout: 5000,
|
|
247
|
+
validateStatus: () => true,
|
|
248
|
+
});
|
|
249
|
+
return (0, nodeMetrics_1.readNodeMetricsReply)(response.status, (_a = response.headers) !== null && _a !== void 0 ? _a : {}, response.data);
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
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) };
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
224
256
|
/**
|
|
225
257
|
* Whether the node still has this session. The node is asked with its
|
|
226
258
|
* session-status route (gateway/nodeSessionStatus.ts), which reads Appium's
|