@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,343 @@
|
|
|
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 NodeDeviceLogsCollector_1 = require("../../src/services/logcat/NodeDeviceLogsCollector");
|
|
19
|
+
const nodeDeviceLogs_1 = require("../../src/services/logcat/nodeDeviceLogs");
|
|
20
|
+
const nodeAsk_1 = require("../../src/gateway/nodeAsk");
|
|
21
|
+
const RemoteSession_1 = require("../../src/sessions/RemoteSession");
|
|
22
|
+
const CloudSession_1 = require("../../src/sessions/CloudSession");
|
|
23
|
+
const line = (seq) => ({ seq, message: `line ${seq}`, timestamp: 1000 + seq });
|
|
24
|
+
const answer = (state, seqs, more = false) => ({
|
|
25
|
+
kind: 'answer',
|
|
26
|
+
answer: { state, lines: seqs.map(line), more },
|
|
27
|
+
});
|
|
28
|
+
describe('NodeDeviceLogsCollector: the hub collects a node session’s device log', () => {
|
|
29
|
+
let clock;
|
|
30
|
+
let replies;
|
|
31
|
+
let asked;
|
|
32
|
+
let got;
|
|
33
|
+
let times;
|
|
34
|
+
let warned;
|
|
35
|
+
let support;
|
|
36
|
+
/** While set, an ask waits on it: a node slow to answer. */
|
|
37
|
+
let gate;
|
|
38
|
+
const source = {
|
|
39
|
+
nodeOrigin: () => 'http://node:4723',
|
|
40
|
+
nodeDeviceLogs: (after) => __awaiter(void 0, void 0, void 0, function* () {
|
|
41
|
+
var _a;
|
|
42
|
+
asked.push(after);
|
|
43
|
+
if (gate)
|
|
44
|
+
yield gate;
|
|
45
|
+
return (_a = replies.shift()) !== null && _a !== void 0 ? _a : answer('recording', []);
|
|
46
|
+
}),
|
|
47
|
+
};
|
|
48
|
+
/** While set, writing an answer's lines waits on it: a slow database. */
|
|
49
|
+
let writing;
|
|
50
|
+
const make = (storedTail = []) => new NodeDeviceLogsCollector_1.NodeDeviceLogsCollector({
|
|
51
|
+
source,
|
|
52
|
+
onLines: (lines) => __awaiter(void 0, void 0, void 0, function* () {
|
|
53
|
+
if (writing)
|
|
54
|
+
yield writing;
|
|
55
|
+
got.push(...lines.map((l) => l.message));
|
|
56
|
+
times.push(...lines.map((l) => l.timestamp.getTime()));
|
|
57
|
+
}),
|
|
58
|
+
support,
|
|
59
|
+
logger: { info: () => undefined, warn: (m) => warned.push(m) },
|
|
60
|
+
storedTail,
|
|
61
|
+
});
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
clock = sinon_1.default.useFakeTimers({ now: 0 });
|
|
64
|
+
replies = [];
|
|
65
|
+
asked = [];
|
|
66
|
+
got = [];
|
|
67
|
+
times = [];
|
|
68
|
+
warned = [];
|
|
69
|
+
gate = null;
|
|
70
|
+
writing = null;
|
|
71
|
+
support = new nodeDeviceLogs_1.NodeDeviceLogsSupport();
|
|
72
|
+
support.now = () => clock.now;
|
|
73
|
+
support.logger = { warn: (m) => warned.push(m) };
|
|
74
|
+
});
|
|
75
|
+
afterEach(() => clock.restore());
|
|
76
|
+
it('asks at once, then every 10 s after the newest line it has, and passes the lines on', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
77
|
+
replies = [answer('recording', [1, 2]), answer('recording', [3])];
|
|
78
|
+
const c = make();
|
|
79
|
+
c.start();
|
|
80
|
+
yield clock.tickAsync(0);
|
|
81
|
+
(0, chai_1.expect)(asked).to.deep.equal([null]);
|
|
82
|
+
yield clock.tickAsync(9999);
|
|
83
|
+
(0, chai_1.expect)(asked).to.deep.equal([null]);
|
|
84
|
+
yield clock.tickAsync(1);
|
|
85
|
+
(0, chai_1.expect)(asked).to.deep.equal([null, 2]);
|
|
86
|
+
(0, chai_1.expect)(got).to.deep.equal(['line 1', 'line 2', 'line 3']);
|
|
87
|
+
(0, chai_1.expect)(times).to.deep.equal([1001, 1002, 1003]);
|
|
88
|
+
}));
|
|
89
|
+
it('asks again at once while the node holds more, a few pages a round', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
90
|
+
replies = [answer('recording', [1, 2], true), answer('recording', [3, 4], true)];
|
|
91
|
+
for (let i = 0; i < NodeDeviceLogsCollector_1.MAX_PAGES_PER_ROUND; i++) {
|
|
92
|
+
replies.push(answer('recording', [5 + i], true));
|
|
93
|
+
}
|
|
94
|
+
const c = make();
|
|
95
|
+
c.start();
|
|
96
|
+
yield clock.tickAsync(0);
|
|
97
|
+
(0, chai_1.expect)(asked).to.have.length(NodeDeviceLogsCollector_1.MAX_PAGES_PER_ROUND);
|
|
98
|
+
(0, chai_1.expect)(asked.slice(0, 3)).to.deep.equal([null, 2, 4]);
|
|
99
|
+
// The rest wait for the next round, which asks until the node has no more.
|
|
100
|
+
yield clock.tickAsync(10000);
|
|
101
|
+
(0, chai_1.expect)(asked).to.have.length(NodeDeviceLogsCollector_1.MAX_PAGES_PER_ROUND + 3);
|
|
102
|
+
yield c.stop();
|
|
103
|
+
}));
|
|
104
|
+
it('asks once more when the session ends, and takes the last lines', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
105
|
+
replies = [answer('recording', [1]), answer('ended', [2, 3])];
|
|
106
|
+
const c = make();
|
|
107
|
+
c.start();
|
|
108
|
+
yield clock.tickAsync(0);
|
|
109
|
+
yield c.stop();
|
|
110
|
+
(0, chai_1.expect)(asked).to.deep.equal([null, 1]);
|
|
111
|
+
(0, chai_1.expect)(got).to.deep.equal(['line 1', 'line 2', 'line 3']);
|
|
112
|
+
yield clock.tickAsync(60000);
|
|
113
|
+
(0, chai_1.expect)(asked).to.have.length(2);
|
|
114
|
+
}));
|
|
115
|
+
it('stops asking once the node’s session ended and it has nothing more', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
116
|
+
replies = [answer('ended', [1, 2], true), answer('ended', [3])];
|
|
117
|
+
const c = make();
|
|
118
|
+
c.start();
|
|
119
|
+
yield clock.tickAsync(30000);
|
|
120
|
+
yield c.stop();
|
|
121
|
+
(0, chai_1.expect)(asked).to.deep.equal([null, 2]);
|
|
122
|
+
(0, chai_1.expect)(got).to.deep.equal(['line 1', 'line 2', 'line 3']);
|
|
123
|
+
}));
|
|
124
|
+
for (const [what, reply] of [
|
|
125
|
+
['the node doesn’t record the session', answer('off', [])],
|
|
126
|
+
['the node refuses the hub’s token', { kind: 'refused' }],
|
|
127
|
+
]) {
|
|
128
|
+
it(`stops asking when ${what}`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
129
|
+
replies = [reply];
|
|
130
|
+
const c = make();
|
|
131
|
+
c.start();
|
|
132
|
+
yield clock.tickAsync(30000);
|
|
133
|
+
yield c.stop();
|
|
134
|
+
(0, chai_1.expect)(asked).to.deep.equal([null]);
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
it('says once that a node is older, and leaves it alone for 10 minutes', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
138
|
+
replies = [{ kind: 'unsupported', status: 404 }];
|
|
139
|
+
const first = make();
|
|
140
|
+
first.start();
|
|
141
|
+
yield clock.tickAsync(30000);
|
|
142
|
+
yield first.stop();
|
|
143
|
+
(0, chai_1.expect)(asked).to.deep.equal([null]);
|
|
144
|
+
(0, chai_1.expect)(warned).to.have.length(1);
|
|
145
|
+
(0, chai_1.expect)(warned[0]).to.contain('http://node:4723');
|
|
146
|
+
// Another session on that node: not asked at all.
|
|
147
|
+
const second = make();
|
|
148
|
+
second.start();
|
|
149
|
+
yield clock.tickAsync(30000);
|
|
150
|
+
yield second.stop();
|
|
151
|
+
(0, chai_1.expect)(asked).to.deep.equal([null]);
|
|
152
|
+
// After 10 minutes it is asked again, and an older node is said only once.
|
|
153
|
+
yield clock.tickAsync(nodeAsk_1.OLDER_NODE_RECHECK_MS);
|
|
154
|
+
replies = [{ kind: 'unsupported', status: 404 }];
|
|
155
|
+
const third = make();
|
|
156
|
+
third.start();
|
|
157
|
+
yield clock.tickAsync(0);
|
|
158
|
+
yield third.stop();
|
|
159
|
+
(0, chai_1.expect)(asked).to.deep.equal([null, null]);
|
|
160
|
+
(0, chai_1.expect)(warned).to.have.length(1);
|
|
161
|
+
}));
|
|
162
|
+
it('asks an unreachable node again, says so once, and doesn’t wait on it at the end', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
163
|
+
replies = [
|
|
164
|
+
answer('recording', [1]),
|
|
165
|
+
{ kind: 'unavailable', reason: 'ECONNREFUSED' },
|
|
166
|
+
{ kind: 'unavailable', reason: 'ECONNREFUSED' },
|
|
167
|
+
];
|
|
168
|
+
const c = make();
|
|
169
|
+
c.start();
|
|
170
|
+
yield clock.tickAsync(20000);
|
|
171
|
+
(0, chai_1.expect)(asked).to.deep.equal([null, 1, 1]);
|
|
172
|
+
(0, chai_1.expect)(warned).to.have.length(1);
|
|
173
|
+
(0, chai_1.expect)(warned[0]).to.contain('ECONNREFUSED');
|
|
174
|
+
yield c.stop();
|
|
175
|
+
(0, chai_1.expect)(asked).to.have.length(3);
|
|
176
|
+
}));
|
|
177
|
+
it('still asks at the end after one failed ask: the node may have been busy', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
178
|
+
replies = [
|
|
179
|
+
answer('recording', [1]),
|
|
180
|
+
{ kind: 'unavailable', reason: 'timeout' },
|
|
181
|
+
answer('ended', [2, 3]),
|
|
182
|
+
];
|
|
183
|
+
const c = make();
|
|
184
|
+
c.start();
|
|
185
|
+
yield clock.tickAsync(10000);
|
|
186
|
+
yield c.stop();
|
|
187
|
+
(0, chai_1.expect)(asked).to.deep.equal([null, 1, 1]);
|
|
188
|
+
(0, chai_1.expect)(got).to.deep.equal(['line 1', 'line 2', 'line 3']);
|
|
189
|
+
}));
|
|
190
|
+
it('writes each answer before asking for the next, which drops it on the node', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
191
|
+
let release;
|
|
192
|
+
writing = new Promise((r) => (release = r));
|
|
193
|
+
replies = [answer('recording', [1, 2], true), answer('recording', [3])];
|
|
194
|
+
const c = make();
|
|
195
|
+
c.start();
|
|
196
|
+
yield clock.tickAsync(0);
|
|
197
|
+
(0, chai_1.expect)(asked).to.deep.equal([null]);
|
|
198
|
+
writing = null;
|
|
199
|
+
release();
|
|
200
|
+
yield clock.tickAsync(0);
|
|
201
|
+
(0, chai_1.expect)(asked).to.deep.equal([null, 2]);
|
|
202
|
+
yield c.stop();
|
|
203
|
+
}));
|
|
204
|
+
it('takes up again where it was when an unreachable node answers', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
205
|
+
replies = [
|
|
206
|
+
answer('recording', [1]),
|
|
207
|
+
{ kind: 'unavailable', reason: 'timeout' },
|
|
208
|
+
answer('recording', [2, 3]),
|
|
209
|
+
];
|
|
210
|
+
const c = make();
|
|
211
|
+
c.start();
|
|
212
|
+
yield clock.tickAsync(20000);
|
|
213
|
+
yield c.stop();
|
|
214
|
+
(0, chai_1.expect)(asked).to.deep.equal([null, 1, 1, 3]);
|
|
215
|
+
(0, chai_1.expect)(got).to.deep.equal(['line 1', 'line 2', 'line 3']);
|
|
216
|
+
}));
|
|
217
|
+
it('never asks while an ask is still waiting for its answer', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
218
|
+
let release;
|
|
219
|
+
gate = new Promise((r) => (release = r));
|
|
220
|
+
const c = make();
|
|
221
|
+
c.start();
|
|
222
|
+
yield clock.tickAsync(35000);
|
|
223
|
+
(0, chai_1.expect)(asked).to.have.length(1);
|
|
224
|
+
gate = null;
|
|
225
|
+
release();
|
|
226
|
+
yield clock.tickAsync(0);
|
|
227
|
+
yield c.stop();
|
|
228
|
+
}));
|
|
229
|
+
it('ignores a line it already has', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
230
|
+
replies = [answer('recording', [1, 2]), answer('recording', [2, 3])];
|
|
231
|
+
const c = make();
|
|
232
|
+
c.start();
|
|
233
|
+
yield clock.tickAsync(10000);
|
|
234
|
+
yield c.stop();
|
|
235
|
+
(0, chai_1.expect)(got).to.deep.equal(['line 1', 'line 2', 'line 3']);
|
|
236
|
+
}));
|
|
237
|
+
describe('after a hub restart', () => {
|
|
238
|
+
it('leaves out the lines the hub had stored, which the node sends again', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
239
|
+
// The node still holds the last page the hub received before it
|
|
240
|
+
// restarted: it drops lines only when the next ask names them.
|
|
241
|
+
replies = [answer('recording', [7, 8, 9, 10])];
|
|
242
|
+
const c = make([line(6), line(7), line(8)]);
|
|
243
|
+
c.start();
|
|
244
|
+
yield clock.tickAsync(0);
|
|
245
|
+
yield c.stop();
|
|
246
|
+
(0, chai_1.expect)(got).to.deep.equal(['line 9', 'line 10']);
|
|
247
|
+
}));
|
|
248
|
+
it('keeps them all when the node doesn’t hold the newest stored line', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
249
|
+
replies = [answer('recording', [7, 8]), answer('recording', [9])];
|
|
250
|
+
const c = make([line(5), line(6)]);
|
|
251
|
+
c.start();
|
|
252
|
+
yield clock.tickAsync(0);
|
|
253
|
+
(0, chai_1.expect)(got).to.deep.equal(['line 7', 'line 8']);
|
|
254
|
+
yield c.stop();
|
|
255
|
+
(0, chai_1.expect)(got).to.deep.equal(['line 7', 'line 8', 'line 9']);
|
|
256
|
+
}));
|
|
257
|
+
it('compares only the first answer', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
258
|
+
replies = [answer('recording', []), answer('recording', [8, 9])];
|
|
259
|
+
const c = make([line(8)]);
|
|
260
|
+
c.start();
|
|
261
|
+
yield clock.tickAsync(0);
|
|
262
|
+
yield c.stop();
|
|
263
|
+
(0, chai_1.expect)(got).to.deep.equal(['line 8', 'line 9']);
|
|
264
|
+
}));
|
|
265
|
+
it('tells apart a line logcat printed twice in one millisecond', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
266
|
+
// B and its twin are the same line at the same time. The hub stored
|
|
267
|
+
// the first only: the twin and C are new.
|
|
268
|
+
const a = { seq: 1, message: 'A', timestamp: 1 };
|
|
269
|
+
const b = { seq: 2, message: 'B', timestamp: 2 };
|
|
270
|
+
const twin = { seq: 3, message: 'B', timestamp: 2 };
|
|
271
|
+
const c3 = { seq: 4, message: 'C', timestamp: 3 };
|
|
272
|
+
const page = {
|
|
273
|
+
kind: 'answer',
|
|
274
|
+
answer: { state: 'recording', lines: [a, b, twin, c3], more: false },
|
|
275
|
+
};
|
|
276
|
+
replies = [page];
|
|
277
|
+
const first = make([a, b]);
|
|
278
|
+
first.start();
|
|
279
|
+
yield clock.tickAsync(0);
|
|
280
|
+
yield first.stop();
|
|
281
|
+
(0, chai_1.expect)(got).to.deep.equal(['B', 'C']);
|
|
282
|
+
// Both stored: only C is new.
|
|
283
|
+
got = [];
|
|
284
|
+
replies = [Object.assign({}, page)];
|
|
285
|
+
const second = make([a, b, twin]);
|
|
286
|
+
second.start();
|
|
287
|
+
yield clock.tickAsync(0);
|
|
288
|
+
yield second.stop();
|
|
289
|
+
(0, chai_1.expect)(got).to.deep.equal(['C']);
|
|
290
|
+
}));
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
describe('reading a node’s device log answer', () => {
|
|
294
|
+
const has = { [nodeDeviceLogs_1.NODE_DEVICE_LOGS_HEADER]: '1' };
|
|
295
|
+
it('reads the lines, and leaves out any that isn’t one', () => {
|
|
296
|
+
const reply = (0, nodeDeviceLogs_1.readNodeDeviceLogsReply)(200, has, {
|
|
297
|
+
value: {
|
|
298
|
+
state: 'recording',
|
|
299
|
+
more: true,
|
|
300
|
+
lines: [
|
|
301
|
+
line(1),
|
|
302
|
+
{ seq: 0, message: 'x', timestamp: 1 },
|
|
303
|
+
{ seq: 2.5, message: 'x', timestamp: 1 },
|
|
304
|
+
{ seq: 3, message: 42, timestamp: 1 },
|
|
305
|
+
{ seq: 4, message: 'x', timestamp: 'now' },
|
|
306
|
+
null,
|
|
307
|
+
line(5),
|
|
308
|
+
],
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
(0, chai_1.expect)(reply).to.deep.equal({
|
|
312
|
+
kind: 'answer',
|
|
313
|
+
answer: { state: 'recording', more: true, lines: [line(1), line(5)] },
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
it('takes a node without the header for an older one only when it answers 401, 404 or 2xx', () => {
|
|
317
|
+
for (const status of [200, 401, 404]) {
|
|
318
|
+
(0, chai_1.expect)((0, nodeDeviceLogs_1.readNodeDeviceLogsReply)(status, {}, {})).to.deep.equal({
|
|
319
|
+
kind: 'unsupported',
|
|
320
|
+
status,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
// A proxy's 502 in front of a node is an outage.
|
|
324
|
+
(0, chai_1.expect)((0, nodeDeviceLogs_1.readNodeDeviceLogsReply)(502, {}, {}).kind).to.equal('unavailable');
|
|
325
|
+
});
|
|
326
|
+
it('takes a 404 with the header for a refusal, and anything else it can’t read for an outage', () => {
|
|
327
|
+
(0, chai_1.expect)((0, nodeDeviceLogs_1.readNodeDeviceLogsReply)(404, has, {}).kind).to.equal('refused');
|
|
328
|
+
(0, chai_1.expect)((0, nodeDeviceLogs_1.readNodeDeviceLogsReply)(503, has, {}).kind).to.equal('unavailable');
|
|
329
|
+
(0, chai_1.expect)((0, nodeDeviceLogs_1.readNodeDeviceLogsReply)(200, has, { value: { state: 'x', lines: [] } }).kind).to.equal('unavailable');
|
|
330
|
+
});
|
|
331
|
+
it('asks only a session a node runs, never a cloud provider’s', () => {
|
|
332
|
+
const opts = {
|
|
333
|
+
sessionId: 's1',
|
|
334
|
+
device: { udid: 'p', host: 'http://node:4723' },
|
|
335
|
+
sessionResponse: {},
|
|
336
|
+
xenonOption: {},
|
|
337
|
+
baseUrl: 'http://node:4723/wd/hub',
|
|
338
|
+
};
|
|
339
|
+
(0, chai_1.expect)((0, nodeDeviceLogs_1.nodeDeviceLogsSourceOf)(new RemoteSession_1.RemoteSession(opts))).to.not.equal(undefined);
|
|
340
|
+
(0, chai_1.expect)((0, nodeDeviceLogs_1.nodeDeviceLogsSourceOf)(new CloudSession_1.CloudSession(opts))).to.equal(undefined);
|
|
341
|
+
(0, chai_1.expect)((0, nodeDeviceLogs_1.nodeDeviceLogsSourceOf)(undefined)).to.equal(undefined);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
@@ -34,6 +34,7 @@ const prisma_store_1 = require("../../src/data-service/prisma-store");
|
|
|
34
34
|
const SessionManager_1 = require("../../src/sessions/SessionManager");
|
|
35
35
|
const SessionLifecycleService_1 = require("../../src/services/SessionLifecycleService");
|
|
36
36
|
const config_1 = require("../../src/config");
|
|
37
|
+
const SessionDeviceLogs_1 = require("../../src/services/logcat/SessionDeviceLogs");
|
|
37
38
|
const container_registration_1 = require("../helpers/container-registration");
|
|
38
39
|
const scratch_database_1 = require("../helpers/scratch-database");
|
|
39
40
|
const appium_umbrella_1 = require("../helpers/appium-umbrella");
|
|
@@ -90,10 +91,16 @@ describe('a create the hub forwards, on the node, in Appium 3’s own server()',
|
|
|
90
91
|
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
91
92
|
});
|
|
92
93
|
beforeEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
93
|
-
restore = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, hubSessionToken_1.HubSessionTokenIssuer, device_managers_1.XenonManager);
|
|
94
|
+
restore = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, hubSessionToken_1.HubSessionTokenIssuer, device_managers_1.XenonManager, SessionDeviceLogs_1.SessionDeviceLogs);
|
|
94
95
|
// The hub's keys sign here only because the test mints what the hub would send.
|
|
95
96
|
typedi_1.Container.set(JwtKeyService_1.JwtKeyService, hubKeys);
|
|
96
97
|
typedi_1.Container.set(hubSessionToken_1.HubSessionTokenIssuer, new hubSessionToken_1.HubSessionTokenIssuer());
|
|
98
|
+
// A node records the device log of each session its hub creates; its
|
|
99
|
+
// phone here is a row, so nothing may reach this machine's adb.
|
|
100
|
+
typedi_1.Container.set(SessionDeviceLogs_1.SessionDeviceLogs, {
|
|
101
|
+
start: () => __awaiter(this, void 0, void 0, function* () { return undefined; }),
|
|
102
|
+
stop: () => __awaiter(this, void 0, void 0, function* () { return undefined; }),
|
|
103
|
+
});
|
|
97
104
|
typedi_1.Container.set(device_managers_1.XenonManager, {
|
|
98
105
|
getMaxSessionCount: () => undefined,
|
|
99
106
|
deviceInstances: () => __awaiter(this, void 0, void 0, function* () { return []; }),
|
|
@@ -37,6 +37,7 @@ const SessionOwnerResolver_1 = require("../../src/services/device-access/Session
|
|
|
37
37
|
const LiveSessionOwners_1 = require("../../src/services/device-access/LiveSessionOwners");
|
|
38
38
|
const deviceAccessGuard_1 = require("../../src/middleware/deviceAccessGuard");
|
|
39
39
|
const config_1 = require("../../src/config");
|
|
40
|
+
const SessionDeviceLogs_1 = require("../../src/services/logcat/SessionDeviceLogs");
|
|
40
41
|
const container_registration_1 = require("../helpers/container-registration");
|
|
41
42
|
const scratch_database_1 = require("../helpers/scratch-database");
|
|
42
43
|
const appium_umbrella_1 = require("../helpers/appium-umbrella");
|
|
@@ -76,11 +77,17 @@ describe('the owner of a hub’s session, on the node', function () {
|
|
|
76
77
|
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
77
78
|
});
|
|
78
79
|
beforeEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
79
|
-
restore = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, hubSessionToken_1.HubSessionTokenIssuer, device_managers_1.XenonManager, SessionOwnerResolver_1.SessionOwnerResolver, LiveSessionOwners_1.LiveSessionOwners);
|
|
80
|
+
restore = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, hubSessionToken_1.HubSessionTokenIssuer, device_managers_1.XenonManager, SessionOwnerResolver_1.SessionOwnerResolver, LiveSessionOwners_1.LiveSessionOwners, SessionDeviceLogs_1.SessionDeviceLogs);
|
|
80
81
|
typedi_1.Container.set(JwtKeyService_1.JwtKeyService, hubKeys);
|
|
81
82
|
typedi_1.Container.set(hubSessionToken_1.HubSessionTokenIssuer, new hubSessionToken_1.HubSessionTokenIssuer());
|
|
82
83
|
typedi_1.Container.set(SessionOwnerResolver_1.SessionOwnerResolver, new SessionOwnerResolver_1.SessionOwnerResolver());
|
|
83
84
|
typedi_1.Container.set(LiveSessionOwners_1.LiveSessionOwners, new LiveSessionOwners_1.LiveSessionOwners());
|
|
85
|
+
// A node records the device log of each session its hub creates; its
|
|
86
|
+
// phone here is a row, so nothing may reach this machine's adb.
|
|
87
|
+
typedi_1.Container.set(SessionDeviceLogs_1.SessionDeviceLogs, {
|
|
88
|
+
start: () => __awaiter(this, void 0, void 0, function* () { return undefined; }),
|
|
89
|
+
stop: () => __awaiter(this, void 0, void 0, function* () { return undefined; }),
|
|
90
|
+
});
|
|
84
91
|
typedi_1.Container.set(device_managers_1.XenonManager, {
|
|
85
92
|
getMaxSessionCount: () => undefined,
|
|
86
93
|
deviceInstances: () => __awaiter(this, void 0, void 0, function* () { return []; }),
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const express_1 = __importDefault(require("express"));
|
|
18
|
+
const supertest_1 = __importDefault(require("supertest"));
|
|
19
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
20
|
+
const nodeSessionDeviceLogs_1 = require("../../src/gateway/nodeSessionDeviceLogs");
|
|
21
|
+
const nodeDeviceLogs_1 = require("../../src/services/logcat/nodeDeviceLogs");
|
|
22
|
+
const hubSessionToken_1 = require("../../src/gateway/hubSessionToken");
|
|
23
|
+
const commandAuth_1 = require("../../src/middleware/commandAuth");
|
|
24
|
+
const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
|
|
25
|
+
const quiet = { info: () => undefined, warn: () => undefined, error: () => undefined };
|
|
26
|
+
const ANSWER = {
|
|
27
|
+
state: 'recording',
|
|
28
|
+
lines: [{ seq: 1, message: 'a line', timestamp: 1000 }],
|
|
29
|
+
more: false,
|
|
30
|
+
};
|
|
31
|
+
describe("a node's session device log route", () => {
|
|
32
|
+
let verify;
|
|
33
|
+
let read;
|
|
34
|
+
let enforced;
|
|
35
|
+
const app = () => {
|
|
36
|
+
const a = (0, express_1.default)();
|
|
37
|
+
a.get('/xenon/api/node/sessions/:sessionId/device-logs', (0, nodeSessionDeviceLogs_1.nodeSessionDeviceLogsHandler)({
|
|
38
|
+
hubTokens: { verify },
|
|
39
|
+
enforced: () => enforced,
|
|
40
|
+
read,
|
|
41
|
+
logger: quiet,
|
|
42
|
+
}));
|
|
43
|
+
return a;
|
|
44
|
+
};
|
|
45
|
+
const get = (q = '') => (0, supertest_1.default)(app()).get(`/xenon/api/node/sessions/s1/device-logs${q}`);
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
verify = sinon_1.default.stub();
|
|
48
|
+
read = sinon_1.default.stub().returns(ANSWER);
|
|
49
|
+
enforced = false;
|
|
50
|
+
});
|
|
51
|
+
afterEach(() => sinon_1.default.restore());
|
|
52
|
+
it('answers the lines after `after`, with no credential when per-command auth is off', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
53
|
+
const res = yield get('?after=12');
|
|
54
|
+
(0, chai_1.expect)(res.status).to.equal(200);
|
|
55
|
+
(0, chai_1.expect)(res.body).to.deep.equal({ value: ANSWER });
|
|
56
|
+
(0, chai_1.expect)(res.headers[nodeDeviceLogs_1.NODE_DEVICE_LOGS_HEADER]).to.equal('1');
|
|
57
|
+
(0, chai_1.expect)(read.calledOnceWithExactly('s1', 12)).to.equal(true);
|
|
58
|
+
}));
|
|
59
|
+
it('reads anything but a number as no `after`', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
60
|
+
yield get('?after=-3');
|
|
61
|
+
yield get();
|
|
62
|
+
(0, chai_1.expect)(read.getCalls().map((c) => c.args[1])).to.deep.equal([null, null]);
|
|
63
|
+
}));
|
|
64
|
+
it("with per-command auth on, answers only the hub's token for that session", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
65
|
+
enforced = true;
|
|
66
|
+
verify.withArgs('good', 's1').resolves(true);
|
|
67
|
+
verify.withArgs('other', 's1').resolves(false);
|
|
68
|
+
const missing = yield get();
|
|
69
|
+
const wrong = yield get().set(hubSessionToken_1.HUB_TOKEN_HEADER, 'other');
|
|
70
|
+
const right = yield get().set(hubSessionToken_1.HUB_TOKEN_HEADER, 'good');
|
|
71
|
+
(0, chai_1.expect)([missing.status, missing.body]).to.deep.equal([404, commandAuth_1.UNKNOWN_SESSION_BODY]);
|
|
72
|
+
(0, chai_1.expect)([wrong.status, wrong.body]).to.deep.equal([404, commandAuth_1.UNKNOWN_SESSION_BODY]);
|
|
73
|
+
(0, chai_1.expect)([right.status, right.body]).to.deep.equal([200, { value: ANSWER }]);
|
|
74
|
+
// A refused ask neither reads nor drops the lines the hub hasn't collected.
|
|
75
|
+
(0, chai_1.expect)(read.calledOnce).to.equal(true);
|
|
76
|
+
for (const r of [missing, wrong, right]) {
|
|
77
|
+
(0, chai_1.expect)(r.headers[nodeDeviceLogs_1.NODE_DEVICE_LOGS_HEADER]).to.equal('1');
|
|
78
|
+
}
|
|
79
|
+
}));
|
|
80
|
+
it("answers 503 when it can't check the hub's token", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
81
|
+
enforced = true;
|
|
82
|
+
verify.rejects(new Error('JWKS unreachable'));
|
|
83
|
+
const res = yield get().set(hubSessionToken_1.HUB_TOKEN_HEADER, 'good');
|
|
84
|
+
(0, chai_1.expect)([res.status, res.body]).to.deep.equal([503, commandAuth_1.COMMAND_AUTH_UNAVAILABLE_BODY]);
|
|
85
|
+
(0, chai_1.expect)(read.called).to.equal(false);
|
|
86
|
+
}));
|
|
87
|
+
it('exists only on a node', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
88
|
+
const mount = (hub) => {
|
|
89
|
+
const a = (0, express_1.default)();
|
|
90
|
+
const r = express_1.default.Router();
|
|
91
|
+
(0, nodeSessionDeviceLogs_1.registerNodeSessionDeviceLogs)(r, Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { hub }));
|
|
92
|
+
a.use('/xenon/api', r);
|
|
93
|
+
return a;
|
|
94
|
+
};
|
|
95
|
+
const onHub = yield (0, supertest_1.default)(mount()).get('/xenon/api/node/sessions/s1/device-logs');
|
|
96
|
+
const onNode = yield (0, supertest_1.default)(mount('http://127.0.0.1:1')).get('/xenon/api/node/sessions/s1/device-logs');
|
|
97
|
+
(0, chai_1.expect)(onHub.status).to.equal(404);
|
|
98
|
+
(0, chai_1.expect)(onHub.headers[nodeDeviceLogs_1.NODE_DEVICE_LOGS_HEADER]).to.equal(undefined);
|
|
99
|
+
(0, chai_1.expect)(onNode.status).to.equal(200);
|
|
100
|
+
(0, chai_1.expect)(onNode.headers[nodeDeviceLogs_1.NODE_DEVICE_LOGS_HEADER]).to.equal('1');
|
|
101
|
+
(0, chai_1.expect)(onNode.body).to.deep.equal({ value: { state: 'off', lines: [], more: false } });
|
|
102
|
+
}));
|
|
103
|
+
});
|