@xenon-device-management/xenon 2.16.0 → 2.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/lib/package.json +1 -1
- package/lib/public/assets/{AnnotationOverlay-2W4JvEyZ.js → AnnotationOverlay-CjmMCS0S.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-GmckX_7r.js → ApiKeyGate-B4Ob0v3C.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-DqODzkyo.js → DeviceMosaicView-DTVOB_NO.js} +1 -1
- package/lib/public/assets/{EmptyState-7hETKEWb.js → EmptyState-DcEG-oAi.js} +1 -1
- package/lib/public/assets/{FieldGroup-GOgssFEP.js → FieldGroup-CtnCDshE.js} +1 -1
- package/lib/public/assets/{FilterMenu-Yvf6RGMd.js → FilterMenu-BK0dE9cc.js} +1 -1
- package/lib/public/assets/{Menu-KrPnfdsf.js → Menu-eKsmLpKS.js} +1 -1
- package/lib/public/assets/{Modal-C6WjZFeI.js → Modal-DPwop3Lz.js} +1 -1
- package/lib/public/assets/{RecordingPage-CSRIhZEh.js → RecordingPage-CfgSkZkB.js} +1 -1
- package/lib/public/assets/{RecordingsPage-CB8ST3nE.js → RecordingsPage-Bzl9pTUY.js} +1 -1
- package/lib/public/assets/{SegmentedControl-C9o90KId.js → SegmentedControl-B_TZTrPm.js} +1 -1
- package/lib/public/assets/{SettingCard-CPm3w9l2.js → SettingCard-DlmvZJPM.js} +1 -1
- package/lib/public/assets/{Table-DwIgERRt.js → Table-BigPWKXi.js} +1 -1
- package/lib/public/assets/{activity-DrLbHifn.js → activity-CvuZdQYu.js} +1 -1
- package/lib/public/assets/{ai-settings-DU_Ls06P.js → ai-settings-lI2UJ2n7.js} +1 -1
- package/lib/public/assets/{api-keys-CWYdEzJ_.js → api-keys-CPgf9XJM.js} +1 -1
- package/lib/public/assets/{apps-BqNVdln7.js → apps-DIzOvtUp.js} +1 -1
- package/lib/public/assets/{arrow-left-BtB0_Jg_.js → arrow-left-DiP5ywBT.js} +1 -1
- package/lib/public/assets/{arrow-right-C8NhdtyN.js → arrow-right-DtW_8hm5.js} +1 -1
- package/lib/public/assets/{arrow-up-right-gMg0ffvv.js → arrow-up-right-C89FbNU0.js} +1 -1
- package/lib/public/assets/{auth-shell-CiYZ3N4M.js → auth-shell-B7sUDb--.js} +2 -2
- package/lib/public/assets/{builds-page-EaaqDmLN.js → builds-page-BBhvrMfA.js} +1 -1
- package/lib/public/assets/{button-DDau4Axv.js → button-DugsdUw1.js} +1 -1
- package/lib/public/assets/{calendar-DQ83B1ed.js → calendar-grKd08ZH.js} +1 -1
- package/lib/public/assets/{check-K1ovTQam.js → check-ORjyNfnX.js} +1 -1
- package/lib/public/assets/{chevron-right-Biq00pMt.js → chevron-right-Ei2FxUXq.js} +1 -1
- package/lib/public/assets/{circle-check-CBv88cH7.js → circle-check-Day0HjW4.js} +1 -1
- package/lib/public/assets/{circle-x-Bmltailn.js → circle-x-CHxSRLHp.js} +1 -1
- package/lib/public/assets/{clock-BPrld-3h.js → clock-BDHewWTx.js} +1 -1
- package/lib/public/assets/{copy-t0QB14JI.js → copy-CTwT4I3D.js} +1 -1
- package/lib/public/assets/{device-explorer-BgGZ8AvV.js → device-explorer-DlhrKJ-g.js} +1 -1
- package/lib/public/assets/{download-BroHxNOh.js → download-D3bjz_vr.js} +1 -1
- package/lib/public/assets/{forgot-password-Bjmf49i6.js → forgot-password-BNBLi023.js} +1 -1
- package/lib/public/assets/{h264Stream-DeD__6wa.js → h264Stream-C51Au2i_.js} +1 -1
- package/lib/public/assets/{index-B0gQkEg0.js → index-Dk5J6HB1.js} +2 -2
- package/lib/public/assets/{index-B0JeuZbg.js → index-DncSz5LG.js} +1 -1
- package/lib/public/assets/{index-72oF89LJ.js → index-Z7_BY3pJ.js} +1 -1
- package/lib/public/assets/{input-DIc4MSC7.js → input-bTXM4l09.js} +1 -1
- package/lib/public/assets/{line-chart-BcWm2vW8.js → line-chart-B3YmVYyr.js} +1 -1
- package/lib/public/assets/{list-checks-B06IJJad.js → list-checks-BClKEj1-.js} +1 -1
- package/lib/public/assets/{lock-pVLvnN30.js → lock-CZsenX3D.js} +1 -1
- package/lib/public/assets/{login-CN77vy--.js → login-B6A6tOp9.js} +1 -1
- package/lib/public/assets/{maintenance-settings-CSdzQSkY.js → maintenance-settings-CmCJGy7O.js} +1 -1
- package/lib/public/assets/{monitor-BtrN5IkI.js → monitor-BdTNEglU.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CNsQfo6w.js → mouse-pointer-2-BM8CaLtt.js} +1 -1
- package/lib/public/assets/{network-BSfA8ZxG.js → network-BS6vUc1x.js} +1 -1
- package/lib/public/assets/{overview-BfTebCK5.js → overview-Yfk_Y7g3.js} +1 -1
- package/lib/public/assets/{page-header-D8KUX6Qg.js → page-header-DhXCwpml.js} +1 -1
- package/lib/public/assets/{play-Cvc8xzic.js → play-D08gP1td.js} +1 -1
- package/lib/public/assets/{plus-C9Zj84UF.js → plus-BKboorfa.js} +1 -1
- package/lib/public/assets/{profile-page-Bi27STgs.js → profile-page-Cd9NodrA.js} +1 -1
- package/lib/public/assets/{reset-password-CPsOuwSu.js → reset-password-acLDyyDW.js} +1 -1
- package/lib/public/assets/{runbook-page-DEQ-DBOX.js → runbook-page-CWLkC1Fd.js} +1 -1
- package/lib/public/assets/{select-BWpnhWXj.js → select-DZoRKNti.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BefSCSpe.js → selector-detail-redirect-DS4qrt84.js} +1 -1
- package/lib/public/assets/{selector-health-page-MSk96Zz7.js → selector-health-page-TpmN61G3.js} +1 -1
- package/lib/public/assets/{session-detail-page-DPyBNef_.js → session-detail-page-C0wYaXqy.js} +1 -1
- package/lib/public/assets/{settings-V6x-cXwy.js → settings-CxPGEY2j.js} +1 -1
- package/lib/public/assets/{stat-tile-Duex-GZD.js → stat-tile-DUpyYgl-.js} +1 -1
- package/lib/public/assets/{tablet-BBcoqBmL.js → tablet-SA02BJNQ.js} +1 -1
- package/lib/public/assets/{teams-ts8p5rEh.js → teams-jamD0Kxr.js} +1 -1
- package/lib/public/assets/{trash-2-CZ2ew6AZ.js → trash-2-DO8fv6w4.js} +1 -1
- package/lib/public/assets/{upload-Dv99y_le.js → upload-CaiGCStb.js} +1 -1
- package/lib/public/assets/{use-builds-data-DrYC8JCF.js → use-builds-data-jS-fcvVf.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-C7QkBNkP.js → use-password-reset-mode-BzoenegM.js} +1 -1
- package/lib/public/assets/{users-gJ5Y01yo.js → users-Bnw7Vs_B.js} +1 -1
- package/lib/public/assets/{users-D7tKMESH.js → users-DHvDrriG.js} +1 -1
- package/lib/public/assets/{video-off-CqZVuZGs.js → video-off-FEvy0w5U.js} +1 -1
- package/lib/public/assets/{webhook-settings-DI5DSW99.js → webhook-settings-CBVB5lxi.js} +1 -1
- package/lib/public/assets/{zap-ClcHFxfo.js → zap-BMrTJO6M.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +1 -1
- package/lib/src/app/index.js +2 -0
- package/lib/src/app/openapi/grid.yaml +102 -0
- package/lib/src/dashboard/event-manager.js +5 -8
- package/lib/src/device-utils.js +11 -2
- package/lib/src/gateway/nodeAsk.js +57 -0
- package/lib/src/gateway/nodeSessionDeviceLogs.js +35 -0
- package/lib/src/plugin.js +4 -1
- package/lib/src/scripts/initialize-database.js +0 -6
- package/lib/src/scripts/run-migrations.js +123 -880
- package/lib/src/services/SessionLifecycleService.js +20 -5
- package/lib/src/services/interceptor/CertManager.js +50 -16
- package/lib/src/services/logcat/NodeDeviceLogStore.js +99 -0
- package/lib/src/services/logcat/NodeDeviceLogsCollector.js +164 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +199 -44
- package/lib/src/services/logcat/nodeDeviceLogs.js +91 -0
- package/lib/src/services/metrics/nodeMetrics.js +20 -49
- package/lib/src/sessions/RemoteSession.js +11 -2
- package/lib/src/sessions/SessionManager.js +28 -1
- package/lib/test/helpers/fake-logcat.js +80 -0
- package/lib/test/helpers/hub-node-pair.js +227 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +15 -0
- package/lib/test/unit/hub-node-iphone-readiness.spec.js +134 -0
- package/lib/test/unit/hub-node-session-device-logs.spec.js +194 -0
- package/lib/test/unit/hub-node-session-metrics.spec.js +24 -192
- package/lib/test/unit/hub-remote-session-heartbeat.spec.js +8 -1
- package/lib/test/unit/interceptor/CertManager.spec.js +89 -0
- package/lib/test/unit/node-device-log-store.spec.js +102 -0
- package/lib/test/unit/node-device-logs-collector.spec.js +343 -0
- package/lib/test/unit/node-gateway-create-appium-server.spec.js +8 -1
- package/lib/test/unit/node-hub-session-owner.spec.js +8 -1
- package/lib/test/unit/node-session-device-logs-route.spec.js +103 -0
- package/lib/test/unit/run-migrations-database.spec.js +57 -424
- package/lib/test/unit/run-migrations.test.js +74 -674
- package/lib/test/unit/session-device-logs-hub-node.spec.js +340 -0
- package/lib/test/unit/session-device-logs-lifecycle.spec.js +137 -0
- package/lib/test/unit/session-device-logs.spec.js +32 -94
- package/lib/test/unit/session-log-indexes.spec.js +15 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/schema.json +1 -1
|
@@ -0,0 +1,340 @@
|
|
|
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
|
+
require("reflect-metadata");
|
|
13
|
+
const chai_1 = require("chai");
|
|
14
|
+
const SessionDeviceLogs_1 = require("../../src/services/logcat/SessionDeviceLogs");
|
|
15
|
+
const NodeDeviceLogStore_1 = require("../../src/services/logcat/NodeDeviceLogStore");
|
|
16
|
+
const NodeDeviceLogsCollector_1 = require("../../src/services/logcat/NodeDeviceLogsCollector");
|
|
17
|
+
const nodeDeviceLogs_1 = require("../../src/services/logcat/nodeDeviceLogs");
|
|
18
|
+
const deviceLogBook_1 = require("../../src/services/logcat/deviceLogBook");
|
|
19
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
20
|
+
const fake_logcat_1 = require("../helpers/fake-logcat");
|
|
21
|
+
const driver_log_1 = require("../helpers/driver-log");
|
|
22
|
+
const quiet = { info: () => undefined, warn: () => undefined, debug: () => undefined };
|
|
23
|
+
/** A node's service: its own phone's log stream fake, its rows held in `store`. */
|
|
24
|
+
class NodeSideDeviceLogs extends SessionDeviceLogs_1.SessionDeviceLogs {
|
|
25
|
+
constructor(logcat, store, unclaimed = 60000) {
|
|
26
|
+
super();
|
|
27
|
+
this.logcat = logcat;
|
|
28
|
+
this.store = store;
|
|
29
|
+
this.unclaimed = unclaimed;
|
|
30
|
+
this.writes = 0;
|
|
31
|
+
}
|
|
32
|
+
context() {
|
|
33
|
+
return {
|
|
34
|
+
pluginArgs: { bindHostOrIp: '127.0.0.1', hub: 'http://127.0.0.1:4724' },
|
|
35
|
+
port: 4725,
|
|
36
|
+
nodeId: 'node-1',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
streams() {
|
|
40
|
+
return this.logcat;
|
|
41
|
+
}
|
|
42
|
+
readClock() {
|
|
43
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
44
|
+
return deviceLogBook_1.UNKNOWN_CLOCK;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
nodeStore() {
|
|
48
|
+
return this.store;
|
|
49
|
+
}
|
|
50
|
+
unclaimedMs() {
|
|
51
|
+
return this.unclaimed;
|
|
52
|
+
}
|
|
53
|
+
writeLines() {
|
|
54
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
55
|
+
this.writes += 1;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** A hub's service: no phone of its own here; it asks a node every 20 ms. */
|
|
60
|
+
class HubSideDeviceLogs extends SessionDeviceLogs_1.SessionDeviceLogs {
|
|
61
|
+
context() {
|
|
62
|
+
return { pluginArgs: { bindHostOrIp: '127.0.0.1' }, port: 4724, nodeId: 'hub-1' };
|
|
63
|
+
}
|
|
64
|
+
collectorFor(source, onLines, storedTail) {
|
|
65
|
+
return new NodeDeviceLogsCollector_1.NodeDeviceLogsCollector({
|
|
66
|
+
source,
|
|
67
|
+
onLines,
|
|
68
|
+
storedTail,
|
|
69
|
+
support: new nodeDeviceLogs_1.NodeDeviceLogsSupport(),
|
|
70
|
+
logger: quiet,
|
|
71
|
+
intervalMs: 20,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** The phone as the node files it, and as the hub has it from the node's report. */
|
|
76
|
+
const NODE_PHONE = {
|
|
77
|
+
udid: 'phone-1',
|
|
78
|
+
platform: 'android',
|
|
79
|
+
host: 'http://127.0.0.1:4725',
|
|
80
|
+
nodeId: 'node-1',
|
|
81
|
+
};
|
|
82
|
+
/** The node as the hub's collector sees it: the node's own store, behind its route. */
|
|
83
|
+
const nodeAt = (store, sessionId) => ({
|
|
84
|
+
nodeOrigin: () => 'http://127.0.0.1:4725',
|
|
85
|
+
nodeDeviceLogs: (after) => __awaiter(void 0, void 0, void 0, function* () { return ({ kind: 'answer', answer: store.read(sessionId, after) }); }),
|
|
86
|
+
});
|
|
87
|
+
const row = (n) => ({
|
|
88
|
+
message: `line ${n}`,
|
|
89
|
+
timestamp: new Date(1700000000000 + n),
|
|
90
|
+
});
|
|
91
|
+
describe('A node records its phone’s device log for its hub', function () {
|
|
92
|
+
this.timeout(60000);
|
|
93
|
+
let logcat;
|
|
94
|
+
let store;
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
logcat = new fake_logcat_1.FakeLogcatStreams();
|
|
97
|
+
store = new NodeDeviceLogStore_1.NodeDeviceLogStore();
|
|
98
|
+
});
|
|
99
|
+
afterEach(() => logcat.cleanup());
|
|
100
|
+
it('holds the lines for the hub, whatever its dashboard setting, and writes none', () => __awaiter(this, void 0, void 0, function* () {
|
|
101
|
+
const logs = new NodeSideDeviceLogs(logcat, store);
|
|
102
|
+
const since = Date.now() - 60000;
|
|
103
|
+
yield logs.start({ sessionId: 'n1', device: NODE_PHONE, since });
|
|
104
|
+
logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(since - 600000, 'before the session'));
|
|
105
|
+
logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(since + 1000, 'first'));
|
|
106
|
+
logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(since + 2000, 'FATAL EXCEPTION: main', 'E', 'AndroidRuntime'));
|
|
107
|
+
yield (0, fake_logcat_1.settle)();
|
|
108
|
+
const held = store.read('n1', null);
|
|
109
|
+
(0, chai_1.expect)(held.state).to.equal('recording');
|
|
110
|
+
(0, chai_1.expect)(held.lines.map((l) => l.seq)).to.deep.equal([1, 2]);
|
|
111
|
+
(0, chai_1.expect)(held.lines[1].message).to.match(/ E AndroidRuntime: FATAL EXCEPTION: main$/);
|
|
112
|
+
(0, chai_1.expect)(held.lines[1].timestamp).to.equal(since + 2000);
|
|
113
|
+
yield logs.stop('n1');
|
|
114
|
+
(0, chai_1.expect)(store.read('n1', 2).state).to.equal('ended');
|
|
115
|
+
(0, chai_1.expect)(logs.writes).to.equal(0);
|
|
116
|
+
}));
|
|
117
|
+
it('holds an iPhone’s lines from its driver’s log, for the hub', () => __awaiter(this, void 0, void 0, function* () {
|
|
118
|
+
const logs = new NodeSideDeviceLogs(logcat, store);
|
|
119
|
+
const driverLog = new driver_log_1.TestDriverLog();
|
|
120
|
+
const iphone = Object.assign(Object.assign({}, NODE_PHONE), { udid: '00008110-000A', platform: 'ios', realDevice: true });
|
|
121
|
+
yield logs.start({
|
|
122
|
+
sessionId: 'n-ios',
|
|
123
|
+
device: iphone,
|
|
124
|
+
since: Date.now() - 1000,
|
|
125
|
+
driverLog,
|
|
126
|
+
});
|
|
127
|
+
driverLog.line('Oct 5 20:31:07 iPhone Shop[4127] <Error>: payment failed');
|
|
128
|
+
const held = store.read('n-ios', null);
|
|
129
|
+
(0, chai_1.expect)(held.state).to.equal('recording');
|
|
130
|
+
(0, chai_1.expect)(held.lines.map((l) => l.message)).to.deep.equal([
|
|
131
|
+
'Oct 5 20:31:07 iPhone Shop[4127] <Error>: payment failed',
|
|
132
|
+
]);
|
|
133
|
+
(0, chai_1.expect)(logs.writes).to.equal(0);
|
|
134
|
+
yield logs.stop('n-ios');
|
|
135
|
+
(0, chai_1.expect)(store.read('n-ios', 1).state).to.equal('ended');
|
|
136
|
+
}));
|
|
137
|
+
it('keeps the session’s line limit, so a hub is sent no more than its own phone’s session keeps', () => __awaiter(this, void 0, void 0, function* () {
|
|
138
|
+
const logs = new NodeSideDeviceLogs(logcat, store);
|
|
139
|
+
const since = Date.now() - 60000;
|
|
140
|
+
yield logs.start({ sessionId: 'n2', device: NODE_PHONE, since });
|
|
141
|
+
for (let i = 0; i < deviceLogBook_1.DEVICE_LOG_LINE_LIMIT + 5; i++) {
|
|
142
|
+
logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(since + 1000 + i, `line ${i}`));
|
|
143
|
+
}
|
|
144
|
+
logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(since + 30000, 'the end', 'E', 'AndroidRuntime'));
|
|
145
|
+
const held = () => store.entries.get('n2').lines.length;
|
|
146
|
+
yield (0, fake_logcat_1.until)(() => held() === deviceLogBook_1.DEVICE_LOG_LINE_LIMIT + 2, 20000);
|
|
147
|
+
yield logs.stop('n2');
|
|
148
|
+
let after = null;
|
|
149
|
+
const all = [];
|
|
150
|
+
for (;;) {
|
|
151
|
+
const page = store.read('n2', after);
|
|
152
|
+
all.push(...page.lines.map((l) => l.message));
|
|
153
|
+
if (!page.more)
|
|
154
|
+
break;
|
|
155
|
+
after = page.lines[page.lines.length - 1].seq;
|
|
156
|
+
}
|
|
157
|
+
// The limit, a note that only errors are kept from there, the error, and
|
|
158
|
+
// a note that the 5 other lines were left out.
|
|
159
|
+
(0, chai_1.expect)(all).to.have.length(deviceLogBook_1.DEVICE_LOG_LINE_LIMIT + 3);
|
|
160
|
+
(0, chai_1.expect)(all[deviceLogBook_1.DEVICE_LOG_LINE_LIMIT]).to.contain('only errors are kept');
|
|
161
|
+
(0, chai_1.expect)(all[deviceLogBook_1.DEVICE_LOG_LINE_LIMIT + 1]).to.match(/ E AndroidRuntime: the end$/);
|
|
162
|
+
(0, chai_1.expect)(all[deviceLogBook_1.DEVICE_LOG_LINE_LIMIT + 2]).to.contain('5 device log lines were left out');
|
|
163
|
+
}));
|
|
164
|
+
it('stops recording a session no hub has asked about', () => __awaiter(this, void 0, void 0, function* () {
|
|
165
|
+
var _a, _b, _c;
|
|
166
|
+
const logs = new NodeSideDeviceLogs(logcat, store, 30);
|
|
167
|
+
yield logs.start({ sessionId: 'n3', device: NODE_PHONE, since: Date.now() });
|
|
168
|
+
(0, chai_1.expect)((_a = logcat.getMultiplexer(NODE_PHONE.udid)) === null || _a === void 0 ? void 0 : _a.clientCount).to.equal(1);
|
|
169
|
+
yield (0, fake_logcat_1.until)(() => !logs.isRecording('n3'));
|
|
170
|
+
(0, chai_1.expect)((_c = (_b = logcat.getMultiplexer(NODE_PHONE.udid)) === null || _b === void 0 ? void 0 : _b.clientCount) !== null && _c !== void 0 ? _c : 0).to.equal(0);
|
|
171
|
+
(0, chai_1.expect)(store.read('n3', null)).to.deep.equal({ state: 'off', lines: [], more: false });
|
|
172
|
+
// Ending it later does nothing more.
|
|
173
|
+
yield logs.stop('n3');
|
|
174
|
+
(0, chai_1.expect)(store.read('n3', null).state).to.equal('off');
|
|
175
|
+
}));
|
|
176
|
+
it('records to the end once a hub has asked', () => __awaiter(this, void 0, void 0, function* () {
|
|
177
|
+
const logs = new NodeSideDeviceLogs(logcat, store, 30);
|
|
178
|
+
const since = Date.now() - 1000;
|
|
179
|
+
yield logs.start({ sessionId: 'n4', device: NODE_PHONE, since });
|
|
180
|
+
store.read('n4', null);
|
|
181
|
+
yield new Promise((r) => setTimeout(r, 80));
|
|
182
|
+
(0, chai_1.expect)(logs.isRecording('n4')).to.equal(true);
|
|
183
|
+
logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(Date.now(), 'still here'));
|
|
184
|
+
yield (0, fake_logcat_1.settle)();
|
|
185
|
+
(0, chai_1.expect)(store.read('n4', null).lines.map((l) => l.message)).to.have.length(1);
|
|
186
|
+
yield logs.stop('n4');
|
|
187
|
+
}));
|
|
188
|
+
it('keeps a session that ended before any hub asked, for the hub’s last ask', () => __awaiter(this, void 0, void 0, function* () {
|
|
189
|
+
const logs = new NodeSideDeviceLogs(logcat, store, 30);
|
|
190
|
+
const since = Date.now() - 1000;
|
|
191
|
+
yield logs.start({ sessionId: 'n5', device: NODE_PHONE, since });
|
|
192
|
+
logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(Date.now(), 'a short session'));
|
|
193
|
+
yield (0, fake_logcat_1.settle)();
|
|
194
|
+
yield logs.stop('n5');
|
|
195
|
+
yield new Promise((r) => setTimeout(r, 80));
|
|
196
|
+
const held = store.read('n5', null);
|
|
197
|
+
(0, chai_1.expect)(held.state).to.equal('ended');
|
|
198
|
+
(0, chai_1.expect)(held.lines).to.have.length(1);
|
|
199
|
+
}));
|
|
200
|
+
});
|
|
201
|
+
describe('A hub writes a node session’s device log', function () {
|
|
202
|
+
this.timeout(60000);
|
|
203
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
204
|
+
let node;
|
|
205
|
+
let count = 0;
|
|
206
|
+
let sessionId;
|
|
207
|
+
beforeEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
208
|
+
count += 1;
|
|
209
|
+
sessionId = `hub-dl-${count}`;
|
|
210
|
+
node = new NodeDeviceLogStore_1.NodeDeviceLogStore();
|
|
211
|
+
node.begin(sessionId);
|
|
212
|
+
yield scratch.db.session.create({
|
|
213
|
+
data: {
|
|
214
|
+
id: sessionId,
|
|
215
|
+
device_udid: NODE_PHONE.udid,
|
|
216
|
+
device_platform: 'android',
|
|
217
|
+
device_version: '10',
|
|
218
|
+
desired_capabilities: '{}',
|
|
219
|
+
session_capabilities: '{}',
|
|
220
|
+
node_id: 'node-1',
|
|
221
|
+
has_live_video: false,
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
}));
|
|
225
|
+
const stored = () => scratch.db.log.findMany({
|
|
226
|
+
where: { session_id: sessionId, log_type: 'DEVICE' },
|
|
227
|
+
orderBy: { createdAt: 'asc' },
|
|
228
|
+
});
|
|
229
|
+
it('collects only a node’s phone, through its session, and never a cloud provider’s', () => {
|
|
230
|
+
const hub = new HubSideDeviceLogs();
|
|
231
|
+
const source = nodeAt(node, sessionId);
|
|
232
|
+
(0, chai_1.expect)(hub.appliesTo(NODE_PHONE, source)).to.equal(true);
|
|
233
|
+
(0, chai_1.expect)(hub.appliesTo(NODE_PHONE)).to.equal(false);
|
|
234
|
+
(0, chai_1.expect)(hub.appliesTo(Object.assign(Object.assign({}, NODE_PHONE), { cloud: 'browserstack' }), source)).to.equal(false);
|
|
235
|
+
(0, chai_1.expect)(hub.appliesTo(Object.assign(Object.assign({}, NODE_PHONE), { platform: 'ios' }), source)).to.equal(true);
|
|
236
|
+
(0, chai_1.expect)(hub.appliesTo(Object.assign(Object.assign({}, NODE_PHONE), { platform: 'windows' }), source)).to.equal(false);
|
|
237
|
+
// A node never collects from another server.
|
|
238
|
+
(0, chai_1.expect)(new NodeSideDeviceLogs(new fake_logcat_1.FakeLogcatStreams(), node).appliesTo(Object.assign(Object.assign({}, NODE_PHONE), { nodeId: 'node-2', host: 'http://10.0.0.9:4725' }), source)).to.equal(false);
|
|
239
|
+
});
|
|
240
|
+
it('writes the lines as the node gives them, in its order, and the last ones at the end', () => __awaiter(this, void 0, void 0, function* () {
|
|
241
|
+
const hub = new HubSideDeviceLogs();
|
|
242
|
+
node.add(sessionId, [row(1), row(2)]);
|
|
243
|
+
yield hub.start({ sessionId, device: NODE_PHONE, source: nodeAt(node, sessionId) });
|
|
244
|
+
yield (0, fake_logcat_1.until)(() => node.claimed(sessionId));
|
|
245
|
+
yield new Promise((r) => setTimeout(r, 50));
|
|
246
|
+
(0, chai_1.expect)((yield stored()).map((r) => r.message)).to.deep.equal(['line 1', 'line 2']);
|
|
247
|
+
// The node's last lines, then its session ends before the hub's.
|
|
248
|
+
node.add(sessionId, [row(3), row(4)]);
|
|
249
|
+
node.end(sessionId);
|
|
250
|
+
yield hub.stop(sessionId);
|
|
251
|
+
const rows = yield stored();
|
|
252
|
+
(0, chai_1.expect)(rows.map((r) => r.message)).to.deep.equal(['line 1', 'line 2', 'line 3', 'line 4']);
|
|
253
|
+
// Each line keeps the node's time; each row its own `createdAt`, in order.
|
|
254
|
+
(0, chai_1.expect)(rows.map((r) => r.timestamp.getTime())).to.deep.equal([1, 2, 3, 4].map((n) => row(n).timestamp.getTime()));
|
|
255
|
+
const created = rows.map((r) => r.createdAt.getTime());
|
|
256
|
+
(0, chai_1.expect)(created.every((t, i) => i === 0 || t > created[i - 1]), String(created)).to.equal(true);
|
|
257
|
+
}));
|
|
258
|
+
it('goes on after a restart after the newest line stored, without writing it twice', () => __awaiter(this, void 0, void 0, function* () {
|
|
259
|
+
// Before the restart the hub wrote lines 1 to 3, the last two from an
|
|
260
|
+
// answer whose lines the node still holds: it drops them only when the
|
|
261
|
+
// next ask names them.
|
|
262
|
+
const later = Date.now() + 60000;
|
|
263
|
+
yield scratch.db.log.createMany({
|
|
264
|
+
data: [1, 2, 3].map((n) => ({
|
|
265
|
+
session_id: sessionId,
|
|
266
|
+
log_type: 'DEVICE',
|
|
267
|
+
message: row(n).message,
|
|
268
|
+
timestamp: row(n).timestamp,
|
|
269
|
+
createdAt: new Date(later + n),
|
|
270
|
+
})),
|
|
271
|
+
});
|
|
272
|
+
node.add(sessionId, [1, 2, 3, 4, 5].map(row));
|
|
273
|
+
node.read(sessionId, 1);
|
|
274
|
+
const hub = new HubSideDeviceLogs();
|
|
275
|
+
yield hub.start({
|
|
276
|
+
sessionId,
|
|
277
|
+
device: NODE_PHONE,
|
|
278
|
+
source: nodeAt(node, sessionId),
|
|
279
|
+
resume: true,
|
|
280
|
+
});
|
|
281
|
+
node.end(sessionId);
|
|
282
|
+
yield hub.stop(sessionId);
|
|
283
|
+
const rows = yield stored();
|
|
284
|
+
(0, chai_1.expect)(rows.map((r) => r.message)).to.deep.equal([1, 2, 3, 4, 5].map((n) => `line ${n}`));
|
|
285
|
+
(0, chai_1.expect)(rows[3].createdAt.getTime()).to.be.greaterThan(later + 3);
|
|
286
|
+
}));
|
|
287
|
+
it('a second stop waits for the first one’s last lines', () => __awaiter(this, void 0, void 0, function* () {
|
|
288
|
+
// A node slow to answer: the first stop is still in its last ask.
|
|
289
|
+
const slow = nodeAt(node, sessionId);
|
|
290
|
+
const source = {
|
|
291
|
+
nodeOrigin: slow.nodeOrigin,
|
|
292
|
+
nodeDeviceLogs: (after) => __awaiter(this, void 0, void 0, function* () {
|
|
293
|
+
yield new Promise((r) => setTimeout(r, 50));
|
|
294
|
+
return slow.nodeDeviceLogs(after);
|
|
295
|
+
}),
|
|
296
|
+
};
|
|
297
|
+
const hub = new HubSideDeviceLogs();
|
|
298
|
+
node.add(sessionId, [row(1)]);
|
|
299
|
+
yield hub.start({ sessionId, device: NODE_PHONE, source });
|
|
300
|
+
yield (0, fake_logcat_1.until)(() => node.claimed(sessionId));
|
|
301
|
+
node.add(sessionId, [row(2)]);
|
|
302
|
+
node.end(sessionId);
|
|
303
|
+
const first = hub.stop(sessionId);
|
|
304
|
+
// The failure analysis, after a second ending's stop, reads them all.
|
|
305
|
+
yield hub.stop(sessionId);
|
|
306
|
+
(0, chai_1.expect)((yield stored()).map((r) => r.message)).to.deep.equal(['line 1', 'line 2']);
|
|
307
|
+
yield first;
|
|
308
|
+
}));
|
|
309
|
+
it('takes the node’s last lines though the session ends while collection resumes', () => __awaiter(this, void 0, void 0, function* () {
|
|
310
|
+
class SlowToResume extends HubSideDeviceLogs {
|
|
311
|
+
storedTail(id) {
|
|
312
|
+
const _super = Object.create(null, {
|
|
313
|
+
storedTail: { get: () => super.storedTail }
|
|
314
|
+
});
|
|
315
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
316
|
+
yield new Promise((r) => setTimeout(r, 50));
|
|
317
|
+
return _super.storedTail.call(this, id);
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const hub = new SlowToResume();
|
|
322
|
+
node.add(sessionId, [row(1), row(2)]);
|
|
323
|
+
node.end(sessionId);
|
|
324
|
+
void hub.start({
|
|
325
|
+
sessionId,
|
|
326
|
+
device: NODE_PHONE,
|
|
327
|
+
source: nodeAt(node, sessionId),
|
|
328
|
+
resume: true,
|
|
329
|
+
});
|
|
330
|
+
yield hub.stop(sessionId);
|
|
331
|
+
(0, chai_1.expect)((yield stored()).map((r) => r.message)).to.deep.equal(['line 1', 'line 2']);
|
|
332
|
+
}));
|
|
333
|
+
it('collects nothing from a node that doesn’t record the session', () => __awaiter(this, void 0, void 0, function* () {
|
|
334
|
+
const hub = new HubSideDeviceLogs();
|
|
335
|
+
node.forget(sessionId);
|
|
336
|
+
yield hub.start({ sessionId, device: NODE_PHONE, source: nodeAt(node, sessionId) });
|
|
337
|
+
yield hub.stop(sessionId);
|
|
338
|
+
(0, chai_1.expect)(yield stored()).to.deep.equal([]);
|
|
339
|
+
}));
|
|
340
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
18
|
+
const typedi_1 = require("typedi");
|
|
19
|
+
const plugin_1 = require("../../src/plugin");
|
|
20
|
+
const NodeDevices_1 = __importDefault(require("../../src/device-managers/NodeDevices"));
|
|
21
|
+
const SessionLifecycleService_1 = require("../../src/services/SessionLifecycleService");
|
|
22
|
+
const SessionMetricsService_1 = require("../../src/services/metrics/SessionMetricsService");
|
|
23
|
+
const SessionDeviceLogs_1 = require("../../src/services/logcat/SessionDeviceLogs");
|
|
24
|
+
const event_manager_1 = require("../../src/dashboard/event-manager");
|
|
25
|
+
const SessionManager_1 = require("../../src/sessions/SessionManager");
|
|
26
|
+
const device_store_1 = require("../../src/data-service/device-store");
|
|
27
|
+
const prisma_store_1 = require("../../src/data-service/prisma-store");
|
|
28
|
+
const PluginContext_1 = require("../../src/PluginContext");
|
|
29
|
+
const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
|
|
30
|
+
const container_registration_1 = require("../helpers/container-registration");
|
|
31
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
32
|
+
/**
|
|
33
|
+
* A session's device log stops however the session ends on this server, its
|
|
34
|
+
* dashboard on or off: on a node that ends the lines it holds for the hub.
|
|
35
|
+
*/
|
|
36
|
+
describe('a session’s device log stops however the session ends', function () {
|
|
37
|
+
this.timeout(30000);
|
|
38
|
+
(0, scratch_database_1.useScratchDatabase)();
|
|
39
|
+
let restore;
|
|
40
|
+
let stop;
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
restore = (0, container_registration_1.saveRegistrations)(SessionMetricsService_1.SessionMetricsService, SessionDeviceLogs_1.SessionDeviceLogs);
|
|
43
|
+
stop = sinon_1.default.stub().resolves();
|
|
44
|
+
typedi_1.Container.set(SessionMetricsService_1.SessionMetricsService, { stop: sinon_1.default.stub().resolves() });
|
|
45
|
+
typedi_1.Container.set(SessionDeviceLogs_1.SessionDeviceLogs, { stop });
|
|
46
|
+
});
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
sinon_1.default.restore();
|
|
49
|
+
restore();
|
|
50
|
+
});
|
|
51
|
+
it('at shutdown', () => __awaiter(this, void 0, void 0, function* () {
|
|
52
|
+
sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'onSessionStopped').resolves();
|
|
53
|
+
yield typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService).stopSessionForShutdown('s-down', 'shutdown');
|
|
54
|
+
(0, chai_1.expect)(stop.calledWith('s-down')).to.equal(true);
|
|
55
|
+
}));
|
|
56
|
+
it('when the driver shuts down unexpectedly on a node', () => __awaiter(this, void 0, void 0, function* () {
|
|
57
|
+
sinon_1.default.stub(NodeDevices_1.default.prototype, 'unblockDevice').resolves();
|
|
58
|
+
const plugin = { pluginArgs: { hub: 'http://hub:4723' } };
|
|
59
|
+
yield plugin_1.XenonPlugin.prototype.onUnexpectedShutdown.call(plugin, { sessionId: 's-crash', caps: {} }, null);
|
|
60
|
+
(0, chai_1.expect)(stop.calledWith('s-crash')).to.equal(true);
|
|
61
|
+
}));
|
|
62
|
+
});
|
|
63
|
+
/**
|
|
64
|
+
* After a hub restart, a node session's device log goes on being collected
|
|
65
|
+
* where the hub records sessions: after the newest line it stored.
|
|
66
|
+
*/
|
|
67
|
+
describe('a hub restart resumes collecting a node session’s device log', function () {
|
|
68
|
+
this.timeout(30000);
|
|
69
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
70
|
+
let restore;
|
|
71
|
+
let start;
|
|
72
|
+
let savedStore;
|
|
73
|
+
let savedArgs;
|
|
74
|
+
beforeEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
75
|
+
restore = (0, container_registration_1.saveRegistrations)(SessionMetricsService_1.SessionMetricsService, SessionDeviceLogs_1.SessionDeviceLogs);
|
|
76
|
+
start = sinon_1.default.stub().resolves();
|
|
77
|
+
typedi_1.Container.set(SessionMetricsService_1.SessionMetricsService, { start: sinon_1.default.stub() });
|
|
78
|
+
typedi_1.Container.set(SessionDeviceLogs_1.SessionDeviceLogs, { start });
|
|
79
|
+
savedArgs = typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs;
|
|
80
|
+
savedStore = device_store_1.DeviceStoreFactory._deviceStore;
|
|
81
|
+
device_store_1.DeviceStoreFactory._deviceStore = new prisma_store_1.PrismaDeviceStore();
|
|
82
|
+
yield scratch.db.session.deleteMany({});
|
|
83
|
+
yield scratch.db.device.deleteMany({});
|
|
84
|
+
const phone = (udid, extra) => scratch.db.device.create({
|
|
85
|
+
data: Object.assign({ udid, host: 'http://127.0.0.1:1', nodeId: 'node-2', platform: 'android', name: udid, sdk: '10', busy: true, userBlocked: false, offline: false }, extra),
|
|
86
|
+
});
|
|
87
|
+
yield phone('node-phone', {});
|
|
88
|
+
yield phone('cloud-phone', {
|
|
89
|
+
cloud: JSON.stringify('browserstack'),
|
|
90
|
+
host: 'https://hub.example.com',
|
|
91
|
+
});
|
|
92
|
+
for (const [id, udid, desired] of [
|
|
93
|
+
['on-node', 'node-phone', '{}'],
|
|
94
|
+
['logs-off', 'node-phone', JSON.stringify({ 'xe:save_device_logs': false })],
|
|
95
|
+
['in-cloud', 'cloud-phone', '{}'],
|
|
96
|
+
]) {
|
|
97
|
+
yield scratch.db.session.create({
|
|
98
|
+
data: {
|
|
99
|
+
id,
|
|
100
|
+
device_udid: udid,
|
|
101
|
+
device_platform: 'android',
|
|
102
|
+
device_version: '10',
|
|
103
|
+
desired_capabilities: desired,
|
|
104
|
+
session_capabilities: '{}',
|
|
105
|
+
node_id: 'node-2',
|
|
106
|
+
has_live_video: false,
|
|
107
|
+
status: 'running',
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}));
|
|
112
|
+
afterEach(() => {
|
|
113
|
+
for (const s of SessionManager_1.SESSION_MANAGER.getAllSessions())
|
|
114
|
+
SessionManager_1.SESSION_MANAGER.removeSession(s.getId());
|
|
115
|
+
device_store_1.DeviceStoreFactory._deviceStore = savedStore;
|
|
116
|
+
typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs = savedArgs;
|
|
117
|
+
sinon_1.default.restore();
|
|
118
|
+
restore();
|
|
119
|
+
});
|
|
120
|
+
const recover = (enableDashboard) => __awaiter(this, void 0, void 0, function* () {
|
|
121
|
+
typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs = Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { enableDashboard });
|
|
122
|
+
yield new SessionManager_1.SessionManager().recoverActiveSessions('hub-1', '/wd/hub');
|
|
123
|
+
// The device log is resumed without waiting on it.
|
|
124
|
+
yield new Promise((r) => setImmediate(r));
|
|
125
|
+
});
|
|
126
|
+
it('resumes a node session’s, through the node, never a cloud provider’s nor one turned off', () => __awaiter(this, void 0, void 0, function* () {
|
|
127
|
+
yield recover(true);
|
|
128
|
+
(0, chai_1.expect)(start.getCalls().map((c) => c.args[0].sessionId)).to.deep.equal(['on-node']);
|
|
129
|
+
const args = start.firstCall.args[0];
|
|
130
|
+
(0, chai_1.expect)(args.resume).to.equal(true);
|
|
131
|
+
(0, chai_1.expect)(typeof args.source.nodeDeviceLogs).to.equal('function');
|
|
132
|
+
}));
|
|
133
|
+
it('resumes none on a hub with its dashboard off, which records no device logs', () => __awaiter(this, void 0, void 0, function* () {
|
|
134
|
+
yield recover(false);
|
|
135
|
+
(0, chai_1.expect)(start.called).to.equal(false);
|
|
136
|
+
}));
|
|
137
|
+
});
|