@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
|
@@ -79,6 +79,7 @@ const appVisibility_1 = require("./device-access/appVisibility");
|
|
|
79
79
|
const LiveSessionOwners_1 = require("./device-access/LiveSessionOwners");
|
|
80
80
|
const sessionMemory_1 = require("../sessions/sessionMemory");
|
|
81
81
|
const SessionMetricsService_1 = require("./metrics/SessionMetricsService");
|
|
82
|
+
const SessionDeviceLogs_1 = require("./logcat/SessionDeviceLogs");
|
|
82
83
|
const appCapability_1 = require("./session/appCapability");
|
|
83
84
|
const CircuitBreaker_1 = require("../data-service/CircuitBreaker");
|
|
84
85
|
const nodeWebDriverUrl_1 = require("../gateway/nodeWebDriverUrl");
|
|
@@ -904,15 +905,26 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
904
905
|
if (sessionInstance instanceof LocalSession_1.LocalSession) {
|
|
905
906
|
typedi_1.Container.get(LiveSessionOwners_1.LiveSessionOwners).record(sessionId, userId);
|
|
906
907
|
}
|
|
907
|
-
// A node samples its own phones for its hub,
|
|
908
|
-
//
|
|
909
|
-
//
|
|
908
|
+
// A node samples its own phones for its hub, and records their device
|
|
909
|
+
// log unless the session turned it off, whatever its dashboard setting
|
|
910
|
+
// (the hub collects both). A hub and a standalone server start them in
|
|
911
|
+
// EventManager.onSessionStarted, once the row exists.
|
|
910
912
|
if (!this.isHub(context.pluginArgs) && sessionInstance instanceof LocalSession_1.LocalSession) {
|
|
911
913
|
typedi_1.Container.get(SessionMetricsService_1.SessionMetricsService).start({
|
|
912
914
|
sessionId,
|
|
913
915
|
device: freshDevice,
|
|
914
916
|
capabilities: sessionResponse,
|
|
915
917
|
});
|
|
918
|
+
if (xenonCapabilities[XenonCapabilityManager_1.XENON_CAPABILITIES.SAVE_DEVICE_LOGS] !== false) {
|
|
919
|
+
void typedi_1.Container.get(SessionDeviceLogs_1.SessionDeviceLogs).start({
|
|
920
|
+
sessionId,
|
|
921
|
+
device: freshDevice,
|
|
922
|
+
since: allocatedAt,
|
|
923
|
+
// An iPhone's or simulator's: the log its driver captures.
|
|
924
|
+
driverLog: sessionInstance.deviceLog(),
|
|
925
|
+
appUnderTest: sessionInstance.appUnderTest(),
|
|
926
|
+
});
|
|
927
|
+
}
|
|
916
928
|
}
|
|
917
929
|
yield this.applyPostSessionLogic(sessionInstance, xenonCapabilities, freshDevice);
|
|
918
930
|
});
|
|
@@ -1218,9 +1230,11 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
1218
1230
|
reason,
|
|
1219
1231
|
});
|
|
1220
1232
|
// Before the lock's "still in memory?" check, which a dashboard-off
|
|
1221
|
-
// node fails. On a node this ends the figures
|
|
1222
|
-
// on a hub it collects a node session's last ones
|
|
1233
|
+
// node fails. On a node this ends the figures and device log it
|
|
1234
|
+
// holds for its hub; on a hub it collects a node session's last ones,
|
|
1235
|
+
// before onSessionStopped's failure analysis reads the log. Idempotent.
|
|
1223
1236
|
yield typedi_1.Container.get(SessionMetricsService_1.SessionMetricsService).stop(sessionId);
|
|
1237
|
+
yield typedi_1.Container.get(SessionDeviceLogs_1.SessionDeviceLogs).stop(sessionId);
|
|
1224
1238
|
yield sessionCleanupLock.acquire(sessionId, () => __awaiter(this, void 0, void 0, function* () {
|
|
1225
1239
|
const session = SessionManager_1.SESSION_MANAGER.getSession(sessionId);
|
|
1226
1240
|
if (!session) {
|
|
@@ -1338,6 +1352,7 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
1338
1352
|
(0, sessionMemory_1.forgetSessionMemory)(sessionId);
|
|
1339
1353
|
typedi_1.Container.get(TracingService_1.TracingService).endSessionSpan(sessionId, { failed: true, reason });
|
|
1340
1354
|
yield typedi_1.Container.get(SessionMetricsService_1.SessionMetricsService).stop(sessionId);
|
|
1355
|
+
yield typedi_1.Container.get(SessionDeviceLogs_1.SessionDeviceLogs).stop(sessionId);
|
|
1341
1356
|
}));
|
|
1342
1357
|
});
|
|
1343
1358
|
}
|
|
@@ -41,13 +41,19 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
41
41
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
42
|
});
|
|
43
43
|
};
|
|
44
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
45
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
|
+
};
|
|
44
47
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
48
|
exports.CertManager = void 0;
|
|
49
|
+
exports.caSerialNumber = caSerialNumber;
|
|
50
|
+
exports.subjectHashOld = subjectHashOld;
|
|
46
51
|
const fs = __importStar(require("fs"));
|
|
47
52
|
const path = __importStar(require("path"));
|
|
48
|
-
const child_process_1 = require("child_process");
|
|
49
53
|
const crypto_1 = require("crypto");
|
|
50
54
|
const node_forge_1 = require("node-forge");
|
|
55
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
56
|
+
const logger = logger_1.default.scope('CertManager');
|
|
51
57
|
class CertManager {
|
|
52
58
|
constructor(dir) {
|
|
53
59
|
this.dir = dir;
|
|
@@ -71,6 +77,12 @@ class CertManager {
|
|
|
71
77
|
if (!fs.existsSync(this.certPath) || !fs.existsSync(this.keyPath)) {
|
|
72
78
|
this.generateCa();
|
|
73
79
|
}
|
|
80
|
+
else if (!loadsInOpenSsl(this.certPath)) {
|
|
81
|
+
// Through 2.15 the serial had illegal leading zeros, so OpenSSL, Go and
|
|
82
|
+
// a phone's BoringSSL refused the CA. It never worked; make a new one.
|
|
83
|
+
logger.warn(`Replacing the interceptor CA at ${this.certPath}: it can't be loaded`);
|
|
84
|
+
this.generateCa();
|
|
85
|
+
}
|
|
74
86
|
const subjectHash = this.computeAndroidHash(this.certPath);
|
|
75
87
|
return { certPath: this.certPath, keyPath: this.keyPath, subjectHash };
|
|
76
88
|
});
|
|
@@ -83,7 +95,7 @@ class CertManager {
|
|
|
83
95
|
const keys = node_forge_1.pki.rsa.generateKeyPair(2048);
|
|
84
96
|
const cert = node_forge_1.pki.createCertificate();
|
|
85
97
|
cert.publicKey = keys.publicKey;
|
|
86
|
-
cert.serialNumber =
|
|
98
|
+
cert.serialNumber = caSerialNumber();
|
|
87
99
|
const now = new Date();
|
|
88
100
|
cert.validity.notBefore = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
89
101
|
cert.validity.notAfter = new Date(now.getTime() + 10 * 365 * 24 * 60 * 60 * 1000);
|
|
@@ -112,20 +124,42 @@ class CertManager {
|
|
|
112
124
|
fs.writeFileSync(this.publicKeyPath, node_forge_1.pki.publicKeyToPem(keys.publicKey), 'utf8');
|
|
113
125
|
}
|
|
114
126
|
computeAndroidHash(certPath) {
|
|
115
|
-
|
|
116
|
-
const out = (0, child_process_1.execSync)(`openssl x509 -inform PEM -subject_hash_old -noout -in "${certPath}"`, {
|
|
117
|
-
encoding: 'utf8',
|
|
118
|
-
timeout: 5000,
|
|
119
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
120
|
-
}).trim();
|
|
121
|
-
if (/^[0-9a-f]{8}$/.test(out))
|
|
122
|
-
return out;
|
|
123
|
-
}
|
|
124
|
-
catch (e) {
|
|
125
|
-
/* openssl unavailable — fall through */
|
|
126
|
-
}
|
|
127
|
-
const pem = fs.readFileSync(certPath, 'utf8');
|
|
128
|
-
return (0, crypto_1.createHash)('sha1').update(pem).digest('hex').slice(0, 8);
|
|
127
|
+
return subjectHashOld(fs.readFileSync(certPath, 'utf8'));
|
|
129
128
|
}
|
|
130
129
|
}
|
|
131
130
|
exports.CertManager = CertManager;
|
|
131
|
+
/**
|
|
132
|
+
* 16 random bytes, positive and minimally encoded as DER wants: the first
|
|
133
|
+
* byte is 0x01..0x7f (no leading zero, no sign bit), as node-forge writes the
|
|
134
|
+
* hex as the INTEGER's bytes unchanged.
|
|
135
|
+
*/
|
|
136
|
+
function caSerialNumber() {
|
|
137
|
+
const bytes = (0, crypto_1.randomBytes)(16);
|
|
138
|
+
bytes[0] = bytes[0] & 0x7f || 0x01;
|
|
139
|
+
return bytes.toString('hex');
|
|
140
|
+
}
|
|
141
|
+
function loadsInOpenSsl(certPath) {
|
|
142
|
+
try {
|
|
143
|
+
new crypto_1.X509Certificate(fs.readFileSync(certPath));
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
catch (_a) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* OpenSSL's `x509 -subject_hash_old`, the name Android files a system CA
|
|
152
|
+
* under: MD5 of the subject's DER as the certificate holds it, the first four
|
|
153
|
+
* bytes read little-endian.
|
|
154
|
+
*/
|
|
155
|
+
function subjectHashOld(pem) {
|
|
156
|
+
const der = node_forge_1.pki.pemToDer(pem).getBytes();
|
|
157
|
+
const tbs = node_forge_1.asn1.fromDer(der).value[0];
|
|
158
|
+
const fields = tbs.value;
|
|
159
|
+
const hasVersion = fields[0].tagClass === node_forge_1.asn1.Class.CONTEXT_SPECIFIC;
|
|
160
|
+
const subject = fields[hasVersion ? 5 : 4];
|
|
161
|
+
const digest = (0, crypto_1.createHash)('md5')
|
|
162
|
+
.update(Buffer.from(node_forge_1.asn1.toDer(subject).getBytes(), 'binary'))
|
|
163
|
+
.digest();
|
|
164
|
+
return digest.readUInt32LE(0).toString(16).padStart(8, '0');
|
|
165
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
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.NodeDeviceLogStore = void 0;
|
|
10
|
+
const typedi_1 = require("typedi");
|
|
11
|
+
const nodeDeviceLogs_1 = require("./nodeDeviceLogs");
|
|
12
|
+
/**
|
|
13
|
+
* On a node: each recorded session's device log lines, held in memory until
|
|
14
|
+
* the hub collects them. A node has no Session row for a session its hub
|
|
15
|
+
* created, so it can't write them to Log. The session's DeviceLogBook bounds
|
|
16
|
+
* how many there can be; what the hub has collected is dropped; an ended
|
|
17
|
+
* session is kept NODE_DEVICE_LOGS_KEEP_AFTER_END_MS, then forgotten,
|
|
18
|
+
* whether or not a hub asked.
|
|
19
|
+
*/
|
|
20
|
+
let NodeDeviceLogStore = class NodeDeviceLogStore {
|
|
21
|
+
constructor() {
|
|
22
|
+
this.now = () => Date.now();
|
|
23
|
+
this.entries = new Map();
|
|
24
|
+
}
|
|
25
|
+
begin(sessionId) {
|
|
26
|
+
this.prune();
|
|
27
|
+
this.entries.set(sessionId, {
|
|
28
|
+
lines: [],
|
|
29
|
+
nextSeq: 1,
|
|
30
|
+
state: 'recording',
|
|
31
|
+
claimed: false,
|
|
32
|
+
endedAt: null,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
add(sessionId, rows) {
|
|
36
|
+
const entry = this.entries.get(sessionId);
|
|
37
|
+
if (!entry)
|
|
38
|
+
return;
|
|
39
|
+
for (const row of rows) {
|
|
40
|
+
entry.lines.push({
|
|
41
|
+
seq: entry.nextSeq++,
|
|
42
|
+
message: row.message,
|
|
43
|
+
timestamp: row.timestamp.getTime(),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
end(sessionId) {
|
|
48
|
+
var _a;
|
|
49
|
+
const entry = this.entries.get(sessionId);
|
|
50
|
+
if (!entry)
|
|
51
|
+
return;
|
|
52
|
+
entry.state = 'ended';
|
|
53
|
+
entry.endedAt = this.now();
|
|
54
|
+
// Forgotten on time even if nothing else happens on this node: an ended
|
|
55
|
+
// session can hold the book's whole limit.
|
|
56
|
+
const timer = setTimeout(() => this.prune(), nodeDeviceLogs_1.NODE_DEVICE_LOGS_KEEP_AFTER_END_MS);
|
|
57
|
+
(_a = timer.unref) === null || _a === void 0 ? void 0 : _a.call(timer);
|
|
58
|
+
}
|
|
59
|
+
/** Drops the session at once: it isn't recorded any more. */
|
|
60
|
+
forget(sessionId) {
|
|
61
|
+
this.entries.delete(sessionId);
|
|
62
|
+
}
|
|
63
|
+
/** Whether a hub has asked about the session. */
|
|
64
|
+
claimed(sessionId) {
|
|
65
|
+
var _a;
|
|
66
|
+
return ((_a = this.entries.get(sessionId)) === null || _a === void 0 ? void 0 : _a.claimed) === true;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Up to `limit` lines after `after` (from the first, without it). Those at
|
|
70
|
+
* or before it the hub has, so they go.
|
|
71
|
+
*/
|
|
72
|
+
read(sessionId, after, limit = nodeDeviceLogs_1.NODE_DEVICE_LOG_PAGE) {
|
|
73
|
+
this.prune();
|
|
74
|
+
const entry = this.entries.get(sessionId);
|
|
75
|
+
if (!entry)
|
|
76
|
+
return { state: 'off', lines: [], more: false };
|
|
77
|
+
entry.claimed = true;
|
|
78
|
+
if (after !== null) {
|
|
79
|
+
const keep = entry.lines.findIndex((l) => l.seq > after);
|
|
80
|
+
entry.lines.splice(0, keep === -1 ? entry.lines.length : keep);
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
state: entry.state,
|
|
84
|
+
lines: entry.lines.slice(0, limit),
|
|
85
|
+
more: entry.lines.length > limit,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
prune() {
|
|
89
|
+
const cutoff = this.now() - nodeDeviceLogs_1.NODE_DEVICE_LOGS_KEEP_AFTER_END_MS;
|
|
90
|
+
for (const [id, entry] of this.entries) {
|
|
91
|
+
if (entry.endedAt !== null && entry.endedAt <= cutoff)
|
|
92
|
+
this.entries.delete(id);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
exports.NodeDeviceLogStore = NodeDeviceLogStore;
|
|
97
|
+
exports.NodeDeviceLogStore = NodeDeviceLogStore = __decorate([
|
|
98
|
+
(0, typedi_1.Service)()
|
|
99
|
+
], NodeDeviceLogStore);
|
|
@@ -0,0 +1,164 @@
|
|
|
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.NodeDeviceLogsCollector = exports.OUTAGE_AFTER_FAILURES = exports.MAX_PAGES_PER_ROUND = exports.COLLECT_INTERVAL_MS = void 0;
|
|
13
|
+
/** How often the hub asks a node for a session's new lines. */
|
|
14
|
+
exports.COLLECT_INTERVAL_MS = 10000;
|
|
15
|
+
/** Answers asked for in one round while the node says it holds more. */
|
|
16
|
+
exports.MAX_PAGES_PER_ROUND = 10;
|
|
17
|
+
/**
|
|
18
|
+
* Failed asks in a row after which the node is taken to be gone, and not
|
|
19
|
+
* asked once more when the session ends. One failure is a busy node, often at
|
|
20
|
+
* the end of a test, and the last ask brings the test's last lines.
|
|
21
|
+
*/
|
|
22
|
+
exports.OUTAGE_AFTER_FAILURES = 2;
|
|
23
|
+
const rowOf = (l) => ({
|
|
24
|
+
message: l.message,
|
|
25
|
+
timestamp: new Date(l.timestamp),
|
|
26
|
+
});
|
|
27
|
+
/**
|
|
28
|
+
* On a hub: a session on a node's phone, its device log lines collected by
|
|
29
|
+
* asking the node (GET /node/sessions/:id/device-logs) at once, then every
|
|
30
|
+
* COLLECT_INTERVAL_MS, instead of reading the phone. Asking at once tells the
|
|
31
|
+
* node a hub wants the session's lines (it stops recording a session nobody
|
|
32
|
+
* asks about). While the node holds more than one answer, it is asked again at
|
|
33
|
+
* once. Asks never overlap. An unreachable node is asked again; whether the
|
|
34
|
+
* session is alive is the heartbeat's call. When the session ends it asks
|
|
35
|
+
* once more, for the last lines, unless the node stopped answering
|
|
36
|
+
* (OUTAGE_AFTER_FAILURES).
|
|
37
|
+
*/
|
|
38
|
+
class NodeDeviceLogsCollector {
|
|
39
|
+
constructor(o) {
|
|
40
|
+
var _a;
|
|
41
|
+
this.o = o;
|
|
42
|
+
/** The newest line's `seq` the hub has. */
|
|
43
|
+
this.after = null;
|
|
44
|
+
this.done = false;
|
|
45
|
+
this.asking = null;
|
|
46
|
+
this.outage = false;
|
|
47
|
+
/** Failed asks in a row. */
|
|
48
|
+
this.failures = 0;
|
|
49
|
+
this.storedTail = (_a = o.storedTail) !== null && _a !== void 0 ? _a : [];
|
|
50
|
+
}
|
|
51
|
+
start() {
|
|
52
|
+
var _a, _b, _c;
|
|
53
|
+
const origin = this.o.source.nodeOrigin();
|
|
54
|
+
if (!origin || !this.o.support.shouldAsk(origin)) {
|
|
55
|
+
this.finish();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
this.timer = setInterval(() => void this.tick(), (_a = this.o.intervalMs) !== null && _a !== void 0 ? _a : exports.COLLECT_INTERVAL_MS);
|
|
59
|
+
(_c = (_b = this.timer).unref) === null || _c === void 0 ? void 0 : _c.call(_b);
|
|
60
|
+
void this.tick();
|
|
61
|
+
}
|
|
62
|
+
stop() {
|
|
63
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
64
|
+
if (this.timer)
|
|
65
|
+
clearInterval(this.timer);
|
|
66
|
+
if (this.asking)
|
|
67
|
+
yield this.asking;
|
|
68
|
+
// No last ask of a node that stopped answering: ending each of a dark
|
|
69
|
+
// node's sessions would wait out another timeout for nothing.
|
|
70
|
+
if (!this.done && this.failures < exports.OUTAGE_AFTER_FAILURES)
|
|
71
|
+
yield this.round();
|
|
72
|
+
this.done = true;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
tick() {
|
|
76
|
+
if (this.done || this.asking)
|
|
77
|
+
return undefined;
|
|
78
|
+
this.asking = this.round().finally(() => {
|
|
79
|
+
this.asking = null;
|
|
80
|
+
});
|
|
81
|
+
return this.asking;
|
|
82
|
+
}
|
|
83
|
+
/** Asks until the node has nothing more to give, at most MAX_PAGES_PER_ROUND times. */
|
|
84
|
+
round() {
|
|
85
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
86
|
+
for (let page = 0; page < exports.MAX_PAGES_PER_ROUND; page++) {
|
|
87
|
+
if (!(yield this.ask()))
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/** One ask; true when the node holds more, to ask for at once. */
|
|
93
|
+
ask() {
|
|
94
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
95
|
+
var _a, _b;
|
|
96
|
+
const reply = yield this.o.source.nodeDeviceLogs(this.after);
|
|
97
|
+
switch (reply.kind) {
|
|
98
|
+
case 'answer': {
|
|
99
|
+
this.failures = 0;
|
|
100
|
+
if (this.outage) {
|
|
101
|
+
this.outage = false;
|
|
102
|
+
this.o.logger.info(`${this.o.source.nodeOrigin()} answers again`);
|
|
103
|
+
}
|
|
104
|
+
const after = this.after;
|
|
105
|
+
const lines = reply.answer.lines.filter((l) => after === null || l.seq > after);
|
|
106
|
+
for (const l of lines)
|
|
107
|
+
this.after = Math.max((_a = this.after) !== null && _a !== void 0 ? _a : 0, l.seq);
|
|
108
|
+
const fresh = this.pastStored(lines);
|
|
109
|
+
if (fresh.length > 0)
|
|
110
|
+
yield this.o.onLines(fresh.map(rowOf));
|
|
111
|
+
if (reply.answer.more && lines.length > 0)
|
|
112
|
+
return true;
|
|
113
|
+
if (reply.answer.state !== 'recording')
|
|
114
|
+
this.finish();
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
case 'unsupported':
|
|
118
|
+
this.o.support.unsupported((_b = this.o.source.nodeOrigin()) !== null && _b !== void 0 ? _b : '', reply.status);
|
|
119
|
+
this.finish();
|
|
120
|
+
return false;
|
|
121
|
+
case 'refused':
|
|
122
|
+
this.finish();
|
|
123
|
+
return false;
|
|
124
|
+
case 'unavailable':
|
|
125
|
+
this.failures += 1;
|
|
126
|
+
if (!this.outage) {
|
|
127
|
+
this.outage = true;
|
|
128
|
+
this.o.logger.warn(`Can't collect device logs from ${this.o.source.nodeOrigin()}: ${reply.reason}. Asking again.`);
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* After a hub restart the node still holds the last page the hub received
|
|
136
|
+
* (it drops lines only when the next ask names them), so the first answer
|
|
137
|
+
* can repeat lines the hub stored. They end where the answer's lines match
|
|
138
|
+
* the stored tail, line for line back from there: logcat often prints the
|
|
139
|
+
* same line twice in one millisecond, so one line alone can't say which
|
|
140
|
+
* copy the hub has.
|
|
141
|
+
*/
|
|
142
|
+
pastStored(lines) {
|
|
143
|
+
const tail = this.storedTail;
|
|
144
|
+
if (tail.length === 0)
|
|
145
|
+
return lines;
|
|
146
|
+
this.storedTail = [];
|
|
147
|
+
const same = (l, t) => l.message === t.message && l.timestamp === t.timestamp;
|
|
148
|
+
for (let end = lines.length - 1; end >= 0; end -= 1) {
|
|
149
|
+
const span = Math.min(tail.length, end + 1);
|
|
150
|
+
let k = 0;
|
|
151
|
+
while (k < span && same(lines[end - k], tail[tail.length - 1 - k]))
|
|
152
|
+
k += 1;
|
|
153
|
+
if (k === span)
|
|
154
|
+
return lines.slice(end + 1);
|
|
155
|
+
}
|
|
156
|
+
return lines;
|
|
157
|
+
}
|
|
158
|
+
finish() {
|
|
159
|
+
this.done = true;
|
|
160
|
+
if (this.timer)
|
|
161
|
+
clearInterval(this.timer);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
exports.NodeDeviceLogsCollector = NodeDeviceLogsCollector;
|