@xenon-device-management/xenon 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/package.json +3 -3
- package/lib/public/assets/{AnnotationOverlay-C-Pw_hvw.js → AnnotationOverlay-XpewX5nn.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-DmYQ0oSk.js → ApiKeyGate-BQDv3ZCC.js} +1 -1
- package/lib/public/assets/{BugReportButton-n8T1dFUp.js → BugReportButton-C7OmS61B.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-D-rGOBbJ.js → DeviceMosaicView-wl3l2Zlt.js} +1 -1
- package/lib/public/assets/{EmptyState-DExETevp.js → EmptyState-fPxujqqE.js} +1 -1
- package/lib/public/assets/{FieldGroup-CCdFlZm6.js → FieldGroup-5bCutWrs.js} +1 -1
- package/lib/public/assets/{FilterMenu-DiUSOSL-.js → FilterMenu-owsBiDT1.js} +1 -1
- package/lib/public/assets/{Menu-ZOfpH6gh.js → Menu-DFhqzPSt.js} +1 -1
- package/lib/public/assets/{Modal-CZNtoB-1.js → Modal-Bw-a-E-e.js} +1 -1
- package/lib/public/assets/{RecordingPage-BKp6h8Jl.js → RecordingPage-WtByf5bC.js} +1 -1
- package/lib/public/assets/{RecordingsPage-Bf881FX4.js → RecordingsPage-FpWWFNgT.js} +1 -1
- package/lib/public/assets/{SegmentedControl-EUoyizvW.js → SegmentedControl-Jzn9c7o8.js} +1 -1
- package/lib/public/assets/{SettingCard-ClT4hf4_.js → SettingCard-BR7nOhpT.js} +1 -1
- package/lib/public/assets/{Table-ucR4bnmc.js → Table-B_Va3V05.js} +1 -1
- package/lib/public/assets/{activity-K-hdylwq.js → activity-DxRFWvkN.js} +1 -1
- package/lib/public/assets/{ai-settings-BLhdwI3c.js → ai-settings-CQf-9_V6.js} +1 -1
- package/lib/public/assets/{api-keys-BbiN7rBI.js → api-keys-SoE1Kd7N.js} +1 -1
- package/lib/public/assets/{apps-Dez1petz.js → apps-CgGXTlgn.js} +1 -1
- package/lib/public/assets/{arrow-left-UjmFHLAJ.js → arrow-left-uQ6WIwiC.js} +1 -1
- package/lib/public/assets/{arrow-right-BVyEigbu.js → arrow-right-C9RyfzXS.js} +1 -1
- package/lib/public/assets/{arrow-up-right-B-ZlBof3.js → arrow-up-right-DFHi_8yG.js} +1 -1
- package/lib/public/assets/{auth-shell-DAQjAgUv.js → auth-shell-Bii1t4qc.js} +2 -2
- package/lib/public/assets/{builds-page-CmIPa5JK.js → builds-page-Bnv8Z9hi.js} +1 -1
- package/lib/public/assets/{button-Dd-IhMC1.js → button-_Y78wXiP.js} +1 -1
- package/lib/public/assets/{calendar-CAIeJwa5.js → calendar-D4MqSOc2.js} +1 -1
- package/lib/public/assets/{check-C4Jrjg8f.js → check-uGHCXe-L.js} +1 -1
- package/lib/public/assets/{chevron-left-BGzlf-4i.js → chevron-left-Dy1OqpzB.js} +1 -1
- package/lib/public/assets/{chevron-right-_wYqDjDC.js → chevron-right-BUsldEli.js} +1 -1
- package/lib/public/assets/{circle-check-Ckn00yMB.js → circle-check-D3Qq0tKp.js} +1 -1
- package/lib/public/assets/{circle-x-pdVkomLa.js → circle-x-B4IOD1DE.js} +1 -1
- package/lib/public/assets/{clock-B8vmwkHj.js → clock-BfYfzvSg.js} +1 -1
- package/lib/public/assets/{copy-BAqnFI-F.js → copy-DZwYE5Va.js} +1 -1
- package/lib/public/assets/{device-explorer-CYq_4F_N.js → device-explorer-CRG4Utt5.js} +1 -1
- package/lib/public/assets/{download-B6UEAp_1.js → download-CCDYo_TN.js} +1 -1
- package/lib/public/assets/{forgot-password-CLqNvmrN.js → forgot-password-DuLCmVFb.js} +1 -1
- package/lib/public/assets/{funnel-Da90BBbT.js → funnel-CtKNPqzb.js} +1 -1
- package/lib/public/assets/{index-BuIIcmGp.js → index-3uxl3o7K.js} +43 -43
- package/lib/public/assets/{index-MX7IzqyW.js → index-Zy1Op5C1.js} +1 -1
- package/lib/public/assets/{input-Bq9aY-We.js → input-CzTWvUXZ.js} +1 -1
- package/lib/public/assets/{keycodes-CIhXIJyP.js → keycodes-CI-nYDFM.js} +1 -1
- package/lib/public/assets/{layers-bXjpswQI.js → layers-YZZGFyTX.js} +1 -1
- package/lib/public/assets/{lock-pJ72y5Fy.js → lock-L8tzWgDo.js} +1 -1
- package/lib/public/assets/{login-UOcHALV2.js → login-DBbX-fu8.js} +1 -1
- package/lib/public/assets/{maintenance-settings-XnJgp5yb.js → maintenance-settings-AJwkOIsE.js} +1 -1
- package/lib/public/assets/{monitor-QkAhI1Jx.js → monitor-Di_66F2m.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-B42Da0m4.js → mouse-pointer-2-Cnab2zdg.js} +1 -1
- package/lib/public/assets/{network-CKVqcyp6.js → network-DiSumY-1.js} +1 -1
- package/lib/public/assets/{overview-BORGkN3N.js → overview-CwE4gXl5.js} +1 -1
- package/lib/public/assets/{page-header-CKWFOjOI.js → page-header-CfR4Sm9V.js} +1 -1
- package/lib/public/assets/{plus-BpFKVdC8.js → plus-DOS8P9CQ.js} +1 -1
- package/lib/public/assets/{profile-page-CcbKBicY.js → profile-page-BYpHwQIH.js} +1 -1
- package/lib/public/assets/{reset-password-BA0W9bxl.js → reset-password-BRVJU_qV.js} +1 -1
- package/lib/public/assets/{runbook-page-Cp7ZBq3s.js → runbook-page-Bi3JvAFZ.js} +1 -1
- package/lib/public/assets/{select-DIZWIoVR.js → select-Cqx7pEou.js} +1 -1
- package/lib/public/assets/{selector-detail-page-C_4YuvAp.js → selector-detail-page-C0DsncKj.js} +1 -1
- package/lib/public/assets/{selector-health-CXjD_Xth.js → selector-health-CAhpdilF.js} +1 -1
- package/lib/public/assets/{selector-health-page-CTqIHeoG.js → selector-health-page-CrCVeW2m.js} +1 -1
- package/lib/public/assets/{session-detail-page-B0KYkk8z.js → session-detail-page-F_hwYqEt.js} +1 -1
- package/lib/public/assets/{settings-BcRBKUXd.js → settings-C-s5pypL.js} +1 -1
- package/lib/public/assets/{status-pill-outline-Dh2PXSf_.js → status-pill-outline-DY5uzv6A.js} +1 -1
- package/lib/public/assets/{tablet-_sXqZztt.js → tablet-Dli7gwyg.js} +1 -1
- package/lib/public/assets/{teams-DGHT8MKm.js → teams-ui4IhHNC.js} +1 -1
- package/lib/public/assets/{trash-2-BV88cV3Z.js → trash-2-BV4hSmXJ.js} +1 -1
- package/lib/public/assets/{upload-CIDH9wkj.js → upload-DkGy_J-L.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DYGuSNwl.js → use-password-reset-mode-BFtMqQOh.js} +1 -1
- package/lib/public/assets/{users-BUE7MoTq.js → users-BU0nXIVe.js} +1 -1
- package/lib/public/assets/{users-7yviqflc.js → users-Nb_rzRmF.js} +1 -1
- package/lib/public/assets/{video-off-BTRxtlGo.js → video-off-CUNzD2kH.js} +1 -1
- package/lib/public/assets/{webhook-settings-DMKD9Pgm.js → webhook-settings-Ca3UFXZe.js} +1 -1
- package/lib/public/assets/{zap-CWE94qcK.js → zap-B2bH2vKX.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/src/app/routers/control.js +5 -142
- package/lib/src/app/routers/nodePhoneControl.js +301 -0
- package/lib/src/app/ws/h264StreamWs.js +30 -4
- package/lib/src/app/ws/logcatWs.js +36 -7
- package/lib/src/app/ws/nodeSocketRelay.js +167 -0
- package/lib/src/data-service/device-store.js +11 -0
- package/lib/src/data-service/deviceClaims.js +18 -0
- package/lib/src/data-service/prisma-store.js +8 -0
- package/lib/src/device-managers/AndroidDeviceManager.js +27 -27
- package/lib/src/device-managers/android/AndroidH264StreamService.js +10 -2
- package/lib/src/device-managers/android/AndroidStreamService.js +3 -1
- package/lib/src/device-managers/ios/IOSStreamService.js +4 -2
- package/lib/src/middleware/deviceAccessGuard.js +4 -1
- package/lib/src/services/ServerManager.js +13 -0
- package/lib/src/services/device-access/ticketActorAccess.js +3 -1
- package/lib/src/services/lease/LeaseOrphanSweeper.js +4 -2
- package/lib/test/helpers/own-node-id.js +29 -0
- package/lib/test/setup/scratch-default-database.js +32 -0
- package/lib/test/unit/AndroidDeviceManager.spec.js +48 -0
- package/lib/test/unit/android-h264-service.spec.js +18 -0
- package/lib/test/unit/clipboard-route.spec.js +4 -0
- package/lib/test/unit/control-forward-decision.spec.js +17 -17
- package/lib/test/unit/device-access-guard.spec.js +38 -0
- package/lib/test/unit/hub-node-control-forward.spec.js +318 -1
- package/lib/test/unit/hub-node-race.spec.js +25 -0
- package/lib/test/unit/install-repository-app-team.spec.js +4 -0
- package/lib/test/unit/lease/lease-sweeper-expiry.spec.js +15 -0
- package/lib/test/unit/logcat-ws.spec.js +8 -0
- package/lib/test/unit/mark-node-busy-loki.spec.js +53 -0
- package/lib/test/unit/node-phone-control.spec.js +131 -0
- package/lib/test/unit/node-socket-relay.spec.js +357 -0
- package/lib/test/unit/stream-leave-route.spec.js +4 -0
- package/lib/test/unit/stream-stop-ownership.spec.js +4 -0
- package/lib/test/unit/stream-stop-recording.spec.js +4 -0
- package/lib/test/unit/stream-ticket-identity.spec.js +11 -1
- package/lib/test/unit/stream-watchdog-timers.spec.js +46 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
|
@@ -18,6 +18,7 @@ exports.attachH264Ws = attachH264Ws;
|
|
|
18
18
|
const ws_1 = require("ws");
|
|
19
19
|
const logger_1 = __importDefault(require("../../logger"));
|
|
20
20
|
const upgradeRouter_1 = require("./upgradeRouter");
|
|
21
|
+
const nodeSocketRelay_1 = require("./nodeSocketRelay");
|
|
21
22
|
/**
|
|
22
23
|
* Authenticated H.264 video WebSocket.
|
|
23
24
|
*
|
|
@@ -62,6 +63,7 @@ function attachH264Ws(server, deps) {
|
|
|
62
63
|
function upgrade(req, socket, head) {
|
|
63
64
|
const parsed = parseH264WsPath(req.url);
|
|
64
65
|
wss.handleUpgrade(req, socket, head, (ws) => __awaiter(this, void 0, void 0, function* () {
|
|
66
|
+
var _a, _b, _c;
|
|
65
67
|
// Register cleanup BEFORE the awaits below. redeem + startStream can take
|
|
66
68
|
// seconds; if the client disconnects during that window we must still
|
|
67
69
|
// remove it from the multiplexer — otherwise clientCount stays inflated
|
|
@@ -74,20 +76,44 @@ function attachH264Ws(server, deps) {
|
|
|
74
76
|
};
|
|
75
77
|
ws.on('close', onClose);
|
|
76
78
|
ws.on('error', onClose);
|
|
79
|
+
let actor;
|
|
77
80
|
try {
|
|
78
|
-
yield deps.redeem(parsed.ticket, parsed.udid);
|
|
81
|
+
actor = yield deps.redeem(parsed.ticket, parsed.udid);
|
|
79
82
|
}
|
|
80
|
-
catch (
|
|
83
|
+
catch (_d) {
|
|
81
84
|
try {
|
|
82
85
|
ws.close(1008, 'unauthorized');
|
|
83
86
|
}
|
|
84
|
-
catch (
|
|
87
|
+
catch (_e) {
|
|
85
88
|
/* noop */
|
|
86
89
|
}
|
|
87
90
|
return;
|
|
88
91
|
}
|
|
89
92
|
if (closed)
|
|
90
93
|
return; // disconnected during redeem
|
|
94
|
+
// Another server's phone: its node streams it, relayed from here.
|
|
95
|
+
let upstream = null;
|
|
96
|
+
try {
|
|
97
|
+
upstream = (_b = (yield ((_a = deps.nodeSocket) === null || _a === void 0 ? void 0 : _a.call(deps, parsed.udid, actor)))) !== null && _b !== void 0 ? _b : null;
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
const refused = e instanceof nodeSocketRelay_1.NodeSocketRefused;
|
|
101
|
+
logger_1.default.warn(`[${parsed.udid}] H.264 WS from its node failed: ${(_c = e === null || e === void 0 ? void 0 : e.message) !== null && _c !== void 0 ? _c : e}`);
|
|
102
|
+
try {
|
|
103
|
+
ws.close(refused ? 1008 : 1011, refused ? 'refused by the node' : 'node unreachable');
|
|
104
|
+
}
|
|
105
|
+
catch (_f) {
|
|
106
|
+
/* noop */
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (upstream) {
|
|
111
|
+
if (closed)
|
|
112
|
+
return void upstream.terminate();
|
|
113
|
+
(0, nodeSocketRelay_1.relaySocket)(ws, upstream, maxBuffered);
|
|
114
|
+
logger_1.default.info(`[${parsed.udid}] H.264 WS client connected, relayed from its node`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
91
117
|
let mux;
|
|
92
118
|
try {
|
|
93
119
|
mux = yield deps.startStream(parsed.udid);
|
|
@@ -97,7 +123,7 @@ function attachH264Ws(server, deps) {
|
|
|
97
123
|
try {
|
|
98
124
|
ws.close(1011, 'stream failed');
|
|
99
125
|
}
|
|
100
|
-
catch (
|
|
126
|
+
catch (_g) {
|
|
101
127
|
/* noop */
|
|
102
128
|
}
|
|
103
129
|
return;
|
|
@@ -17,6 +17,7 @@ exports.attachLogcatWs = attachLogcatWs;
|
|
|
17
17
|
const ws_1 = require("ws");
|
|
18
18
|
const logger_1 = __importDefault(require("../../logger"));
|
|
19
19
|
const upgradeRouter_1 = require("./upgradeRouter");
|
|
20
|
+
const nodeSocketRelay_1 = require("./nodeSocketRelay");
|
|
20
21
|
const ostraceParse_1 = require("../../services/logcat/ostraceParse");
|
|
21
22
|
const LOGCAT_PATH_RE = /^\/xenon\/api\/control\/([^/]+)\/logcat$/;
|
|
22
23
|
/** Parse the logcat stream upgrade path; returns null for any non-matching URL. */
|
|
@@ -68,7 +69,7 @@ function attachLogcatWs(server, deps) {
|
|
|
68
69
|
function upgrade(req, socket, head) {
|
|
69
70
|
const parsed = parseLogcatWsPath(req.url);
|
|
70
71
|
wss.handleUpgrade(req, socket, head, (ws) => __awaiter(this, void 0, void 0, function* () {
|
|
71
|
-
var _a, _b;
|
|
72
|
+
var _a, _b, _c, _d, _e;
|
|
72
73
|
// Register cleanup BEFORE the awaits below. redeem + authorize +
|
|
73
74
|
// startStream can together take seconds; a client that disconnects in
|
|
74
75
|
// that window must still be removed from the multiplexer once it's
|
|
@@ -86,11 +87,11 @@ function attachLogcatWs(server, deps) {
|
|
|
86
87
|
try {
|
|
87
88
|
actor = yield deps.redeem(parsed.ticket, parsed.udid);
|
|
88
89
|
}
|
|
89
|
-
catch (
|
|
90
|
+
catch (_f) {
|
|
90
91
|
try {
|
|
91
92
|
ws.close(1008, 'unauthorized');
|
|
92
93
|
}
|
|
93
|
-
catch (
|
|
94
|
+
catch (_g) {
|
|
94
95
|
/* noop */
|
|
95
96
|
}
|
|
96
97
|
return;
|
|
@@ -110,7 +111,7 @@ function attachLogcatWs(server, deps) {
|
|
|
110
111
|
try {
|
|
111
112
|
ws.close(1011, 'ownership unavailable');
|
|
112
113
|
}
|
|
113
|
-
catch (
|
|
114
|
+
catch (_h) {
|
|
114
115
|
/* noop */
|
|
115
116
|
}
|
|
116
117
|
return;
|
|
@@ -135,13 +136,41 @@ function attachLogcatWs(server, deps) {
|
|
|
135
136
|
try {
|
|
136
137
|
ws.close(1008, unresolvable ? 'device not found' : 'device held by another user');
|
|
137
138
|
}
|
|
138
|
-
catch (
|
|
139
|
+
catch (_j) {
|
|
139
140
|
/* noop */
|
|
140
141
|
}
|
|
141
142
|
return;
|
|
142
143
|
}
|
|
143
144
|
if (closed)
|
|
144
145
|
return; // disconnected during authorize
|
|
146
|
+
// Another server's phone: its node streams the log, relayed from here.
|
|
147
|
+
// The node keeps the replay, the drop markers and the close codes.
|
|
148
|
+
let upstream = null;
|
|
149
|
+
try {
|
|
150
|
+
upstream =
|
|
151
|
+
(_c = (yield ((_b = deps.nodeSocket) === null || _b === void 0 ? void 0 : _b.call(deps, parsed.udid, actor, {
|
|
152
|
+
levels: parsed.levels,
|
|
153
|
+
process: parsed.process,
|
|
154
|
+
})))) !== null && _c !== void 0 ? _c : null;
|
|
155
|
+
}
|
|
156
|
+
catch (e) {
|
|
157
|
+
const refused = e instanceof nodeSocketRelay_1.NodeSocketRefused;
|
|
158
|
+
logger_1.default.warn(`[${parsed.udid}] logcat WS from its node failed: ${(_d = e === null || e === void 0 ? void 0 : e.message) !== null && _d !== void 0 ? _d : e}`);
|
|
159
|
+
try {
|
|
160
|
+
ws.close(refused ? 1008 : 1011, refused ? 'refused by the node' : 'node unreachable');
|
|
161
|
+
}
|
|
162
|
+
catch (_k) {
|
|
163
|
+
/* noop */
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (upstream) {
|
|
168
|
+
if (closed)
|
|
169
|
+
return void upstream.terminate();
|
|
170
|
+
(0, nodeSocketRelay_1.relaySocket)(ws, upstream, maxBuffered);
|
|
171
|
+
logger_1.default.info(`[${parsed.udid}] logcat WS client connected, relayed from its node`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
145
174
|
let mux;
|
|
146
175
|
let effectiveLevels;
|
|
147
176
|
try {
|
|
@@ -153,11 +182,11 @@ function attachLogcatWs(server, deps) {
|
|
|
153
182
|
effectiveLevels = started.levels;
|
|
154
183
|
}
|
|
155
184
|
catch (e) {
|
|
156
|
-
logger_1.default.warn(`[${parsed.udid}] logcat WS start failed: ${(
|
|
185
|
+
logger_1.default.warn(`[${parsed.udid}] logcat WS start failed: ${(_e = e === null || e === void 0 ? void 0 : e.message) !== null && _e !== void 0 ? _e : e}`);
|
|
157
186
|
try {
|
|
158
187
|
ws.close(1011, 'stream failed');
|
|
159
188
|
}
|
|
160
|
-
catch (
|
|
189
|
+
catch (_l) {
|
|
161
190
|
/* noop */
|
|
162
191
|
}
|
|
163
192
|
return;
|
|
@@ -0,0 +1,167 @@
|
|
|
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
|
+
exports.RELAY_HIGH_WATER_BYTES = exports.NODE_SOCKET_TIMEOUT_MS = exports.NodeSocketRefused = void 0;
|
|
16
|
+
exports.openNodeSocket = openNodeSocket;
|
|
17
|
+
exports.relaySocket = relaySocket;
|
|
18
|
+
const typedi_1 = require("typedi");
|
|
19
|
+
const ws_1 = require("ws");
|
|
20
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
21
|
+
const hubSessionToken_1 = require("../../gateway/hubSessionToken");
|
|
22
|
+
const forwardToNode_1 = require("../../gateway/forwardToNode");
|
|
23
|
+
const controlDevice_1 = require("../../middleware/controlDevice");
|
|
24
|
+
const nodePhoneControl_1 = require("../routers/nodePhoneControl");
|
|
25
|
+
/**
|
|
26
|
+
* The live preview's H.264 socket and the logcat socket, for another server's
|
|
27
|
+
* phone. The viewer's ticket is this server's (stream/ticket is answered
|
|
28
|
+
* here, after the team check) and is redeemed here; the socket itself is the
|
|
29
|
+
* node's, opened with a ticket the node mints for the same user, and relayed.
|
|
30
|
+
* The node keeps doing what it does for its own viewers: the multiplexer,
|
|
31
|
+
* the replay, the slow-viewer drops and the idle release.
|
|
32
|
+
*/
|
|
33
|
+
/** The node, or this server's own rule, refused: the viewer gets 1008. */
|
|
34
|
+
class NodeSocketRefused extends Error {
|
|
35
|
+
}
|
|
36
|
+
exports.NodeSocketRefused = NodeSocketRefused;
|
|
37
|
+
/** How long the node has to mint its ticket and open its socket. */
|
|
38
|
+
exports.NODE_SOCKET_TIMEOUT_MS = 15000;
|
|
39
|
+
/** Unwritten bytes to the viewer beyond which reading from the node pauses. */
|
|
40
|
+
exports.RELAY_HIGH_WATER_BYTES = 4 * 1024 * 1024;
|
|
41
|
+
/**
|
|
42
|
+
* The node's socket for another server's phone, open and paused until
|
|
43
|
+
* relaySocket reads it; null for this server's own phone, which the caller
|
|
44
|
+
* serves itself. Throws NodeSocketRefused when the phone is a cloud
|
|
45
|
+
* provider's or the node refuses the user, and any other error when the node
|
|
46
|
+
* can't be reached.
|
|
47
|
+
*/
|
|
48
|
+
function openNodeSocket(request_1) {
|
|
49
|
+
return __awaiter(this, arguments, void 0, function* (request, deps = {}) {
|
|
50
|
+
var _a, _b, _c, _d;
|
|
51
|
+
const findDevice = (_a = deps.findDevice) !== null && _a !== void 0 ? _a : controlDevice_1.findControlDeviceInStore;
|
|
52
|
+
const controlToken = (_b = deps.controlToken) !== null && _b !== void 0 ? _b : ((grant) => typedi_1.Container.get(hubSessionToken_1.HubSessionTokenIssuer).controlTokenFor(grant));
|
|
53
|
+
const timeoutMs = (_c = deps.timeoutMs) !== null && _c !== void 0 ? _c : exports.NODE_SOCKET_TIMEOUT_MS;
|
|
54
|
+
const { udid, actor, path } = request;
|
|
55
|
+
const device = yield findDevice(udid);
|
|
56
|
+
if (!device || !(0, nodePhoneControl_1.isOtherServersPhone)(device))
|
|
57
|
+
return null;
|
|
58
|
+
if (device.cloud)
|
|
59
|
+
throw new NodeSocketRefused("a cloud provider's phone");
|
|
60
|
+
const origin = (0, nodePhoneControl_1.safeNodeOrigin)(device.host);
|
|
61
|
+
if (!origin)
|
|
62
|
+
throw new NodeSocketRefused('unsafe device host');
|
|
63
|
+
const token = yield controlToken({
|
|
64
|
+
userId: actor.actorId,
|
|
65
|
+
isAdmin: !!actor.isAdmin,
|
|
66
|
+
udid,
|
|
67
|
+
host: device.host,
|
|
68
|
+
});
|
|
69
|
+
const base = `/xenon/api/control/${encodeURIComponent(udid)}`;
|
|
70
|
+
const answer = yield (0, forwardToNode_1.readAnswer)(yield (0, forwardToNode_1.sendToNode)({
|
|
71
|
+
url: `${origin}${base}/stream/ticket`,
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: Object.assign({ accept: 'application/json', 'content-type': 'application/json' }, (token ? { [hubSessionToken_1.HUB_TOKEN_HEADER]: token } : {})),
|
|
74
|
+
body: Buffer.from('{}'),
|
|
75
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
76
|
+
}));
|
|
77
|
+
if (answer.status >= 400 && answer.status < 500) {
|
|
78
|
+
throw new NodeSocketRefused(`the node answered ${answer.status} for a ticket`);
|
|
79
|
+
}
|
|
80
|
+
if (answer.status >= 300)
|
|
81
|
+
throw new Error(`the node answered ${answer.status} for a ticket`);
|
|
82
|
+
const ticket = (_d = JSON.parse(answer.body.toString('utf8'))) === null || _d === void 0 ? void 0 : _d.ticket;
|
|
83
|
+
if (typeof ticket !== 'string')
|
|
84
|
+
throw new Error('the node sent no ticket');
|
|
85
|
+
const query = new URLSearchParams(request.query);
|
|
86
|
+
query.set('ticket', ticket);
|
|
87
|
+
const url = `${origin.replace(/^http/, 'ws')}${base}/${path}?${query}`;
|
|
88
|
+
const socket = new ws_1.WebSocket(url, { handshakeTimeout: timeoutMs, perMessageDeflate: false });
|
|
89
|
+
yield new Promise((resolve, reject) => {
|
|
90
|
+
socket.once('open', () => {
|
|
91
|
+
// Paused until something reads it: the node sends H.264's config
|
|
92
|
+
// packet and logcat's replay as soon as it opens, and a message with
|
|
93
|
+
// no listener yet is lost. relaySocket resumes it.
|
|
94
|
+
socket.pause();
|
|
95
|
+
resolve();
|
|
96
|
+
});
|
|
97
|
+
socket.once('error', reject);
|
|
98
|
+
});
|
|
99
|
+
logger_1.default.info(`[${udid}] ${path} socket opened on node ${origin}`);
|
|
100
|
+
return socket;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/** A close code a server may send. 1005, 1006 and 1015 only describe a close. */
|
|
104
|
+
function isSendable(code) {
|
|
105
|
+
return ((code >= 1000 && code <= 1014 && ![1004, 1005, 1006].includes(code)) ||
|
|
106
|
+
(code >= 3000 && code <= 4999));
|
|
107
|
+
}
|
|
108
|
+
function closeWith(ws, code, reason) {
|
|
109
|
+
if (ws.readyState === ws_1.WebSocket.CLOSING || ws.readyState === ws_1.WebSocket.CLOSED)
|
|
110
|
+
return;
|
|
111
|
+
try {
|
|
112
|
+
if (code === 1005)
|
|
113
|
+
ws.close();
|
|
114
|
+
// The node's connection dropped with no close: the viewer retries.
|
|
115
|
+
else if (!isSendable(code))
|
|
116
|
+
ws.close(1011, 'node connection lost');
|
|
117
|
+
else
|
|
118
|
+
ws.close(code, reason.toString());
|
|
119
|
+
}
|
|
120
|
+
catch (_a) {
|
|
121
|
+
ws.terminate();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Relay `viewer` to and from `node` until either closes. Messages pass
|
|
126
|
+
* unchanged, binary or text as they came, and the node's close code and
|
|
127
|
+
* reason reach the viewer. While more than `highWaterBytes` sent to the
|
|
128
|
+
* viewer are still unwritten, reading from the node pauses: the node then
|
|
129
|
+
* sees a slow viewer and applies its own rule, rather than this server
|
|
130
|
+
* buffering without bound.
|
|
131
|
+
*/
|
|
132
|
+
function relaySocket(viewer, node, highWaterBytes = exports.RELAY_HIGH_WATER_BYTES) {
|
|
133
|
+
let unwritten = 0;
|
|
134
|
+
let paused = false;
|
|
135
|
+
node.on('message', (data, isBinary) => {
|
|
136
|
+
if (viewer.readyState !== ws_1.WebSocket.OPEN)
|
|
137
|
+
return;
|
|
138
|
+
const size = data.length;
|
|
139
|
+
unwritten += size;
|
|
140
|
+
viewer.send(data, { binary: isBinary }, () => {
|
|
141
|
+
unwritten -= size;
|
|
142
|
+
if (paused && unwritten <= highWaterBytes / 2) {
|
|
143
|
+
paused = false;
|
|
144
|
+
node.resume();
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
if (!paused && unwritten > highWaterBytes) {
|
|
148
|
+
paused = true;
|
|
149
|
+
node.pause();
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
viewer.on('message', (data, isBinary) => {
|
|
153
|
+
if (node.readyState === ws_1.WebSocket.OPEN)
|
|
154
|
+
node.send(data, { binary: isBinary });
|
|
155
|
+
});
|
|
156
|
+
node.on('close', (code, reason) => closeWith(viewer, code, reason));
|
|
157
|
+
viewer.on('close', () => {
|
|
158
|
+
if (node.readyState === ws_1.WebSocket.OPEN || node.readyState === ws_1.WebSocket.CONNECTING) {
|
|
159
|
+
node.close(1000);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
// A 'close' follows each; nothing more to do here.
|
|
163
|
+
node.on('error', () => undefined);
|
|
164
|
+
viewer.on('error', () => undefined);
|
|
165
|
+
// openNodeSocket hands the node's socket over paused; read it now.
|
|
166
|
+
node.resume();
|
|
167
|
+
}
|
|
@@ -293,6 +293,17 @@ class LokiDeviceStore {
|
|
|
293
293
|
return true;
|
|
294
294
|
});
|
|
295
295
|
}
|
|
296
|
+
markNodeBusy(udid, host) {
|
|
297
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
298
|
+
const model = yield db_1.XenonDatabase.DeviceModel;
|
|
299
|
+
const device = model.findOne({ udid, host });
|
|
300
|
+
if (!device)
|
|
301
|
+
return;
|
|
302
|
+
device.nodeBusy = true;
|
|
303
|
+
device.busy = true;
|
|
304
|
+
model.update(device);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
296
307
|
resetMetrics() {
|
|
297
308
|
return __awaiter(this, void 0, void 0, function* () {
|
|
298
309
|
(yield db_1.XenonDatabase.DeviceModel)
|
|
@@ -7,6 +7,7 @@ exports.claimWhere = claimWhere;
|
|
|
7
7
|
exports.holdsClaim = holdsClaim;
|
|
8
8
|
exports.hasNoClaim = hasNoClaim;
|
|
9
9
|
exports.isUnheld = isUnheld;
|
|
10
|
+
exports.heldHere = heldHere;
|
|
10
11
|
/**
|
|
11
12
|
* Why a phone is busy, on the server that allocates it.
|
|
12
13
|
*
|
|
@@ -94,5 +95,22 @@ exports.UNHELD = Object.assign(Object.assign({}, exports.NO_CLAIM), { session_id
|
|
|
94
95
|
function isUnheld(device) {
|
|
95
96
|
return hasNoClaim(device) && device.session_id == null && device.nodeBusy !== true;
|
|
96
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Busy for a reason this server has on record: its claim, a session or hold
|
|
100
|
+
* in `session_id`, or plain `busy` on a phone of its own. False for a node's
|
|
101
|
+
* phone busy only by the node's report (`nodeBusy`), a preview the node
|
|
102
|
+
* holds say: this server knows no holder for it, and the node, which does,
|
|
103
|
+
* judges every call forwarded there with its own guard. An ownership check
|
|
104
|
+
* reads this, not `busy`, or it fails closed on every such phone, refusing
|
|
105
|
+
* the preview's own holder. A server with no nodes never has nodeBusy, so
|
|
106
|
+
* for it this is `busy`.
|
|
107
|
+
*/
|
|
108
|
+
function heldHere(device) {
|
|
109
|
+
if (!device.busy)
|
|
110
|
+
return false;
|
|
111
|
+
if (device.nodeBusy !== true)
|
|
112
|
+
return true;
|
|
113
|
+
return device.session_id != null || device.claimedAt != null || device.claimSessionId != null;
|
|
114
|
+
}
|
|
97
115
|
/** What ending a claim resets: the claim, and the session's bookkeeping. */
|
|
98
116
|
exports.CLAIM_RESET = Object.assign(Object.assign({}, exports.NO_CLAIM), { session_id: null, lastCmdExecutedAt: null, sessionStartTime: 0, newCommandTimeout: null, sessionProgress: '', owningSessionId: null, lockedAt: null });
|
|
@@ -386,6 +386,14 @@ class PrismaDeviceStore {
|
|
|
386
386
|
return cleared.count > 0;
|
|
387
387
|
});
|
|
388
388
|
}
|
|
389
|
+
markNodeBusy(udid, host) {
|
|
390
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
391
|
+
yield this.prisma.device.updateMany({
|
|
392
|
+
where: { udid, host },
|
|
393
|
+
data: { nodeBusy: true, busy: true },
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
}
|
|
389
397
|
resetMetrics() {
|
|
390
398
|
return __awaiter(this, void 0, void 0, function* () {
|
|
391
399
|
yield this.prisma.device.updateMany({
|
|
@@ -54,6 +54,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
54
54
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
55
55
|
};
|
|
56
56
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
57
|
+
exports.includesAndroidDevice = includesAndroidDevice;
|
|
57
58
|
const displayState_1 = require("./android/displayState");
|
|
58
59
|
const networkAddresses_1 = require("../helpers/networkAddresses");
|
|
59
60
|
const appium_adb_1 = require("appium-adb");
|
|
@@ -80,6 +81,21 @@ const deviceIdentity_1 = require("./deviceIdentity");
|
|
|
80
81
|
const localDeviceHosts_1 = require("./localDeviceHosts");
|
|
81
82
|
const PluginContext_1 = require("../PluginContext");
|
|
82
83
|
const typedi_2 = require("typedi");
|
|
84
|
+
/**
|
|
85
|
+
* Whether a server set to `androidDeviceType` serves this phone: real phones,
|
|
86
|
+
* emulators ('simulated'), or both, and with `bootedEmulators` only the
|
|
87
|
+
* emulators that are up. Discovery and a plugged phone (onDeviceAdded) both
|
|
88
|
+
* ask this, so a phone the server excludes is never added by either.
|
|
89
|
+
*/
|
|
90
|
+
function includesAndroidDevice(device, androidDeviceType, bootedEmulators) {
|
|
91
|
+
if (androidDeviceType === 'real')
|
|
92
|
+
return device.deviceType === 'real';
|
|
93
|
+
if (androidDeviceType === 'simulated' && device.deviceType !== 'emulator')
|
|
94
|
+
return false;
|
|
95
|
+
if (bootedEmulators && device.deviceType === 'emulator')
|
|
96
|
+
return device.state === 'device';
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
83
99
|
let AndroidDeviceManager = class AndroidDeviceManager {
|
|
84
100
|
constructor(context) {
|
|
85
101
|
this.context = context;
|
|
@@ -149,33 +165,7 @@ let AndroidDeviceManager = class AndroidDeviceManager {
|
|
|
149
165
|
devices = yield this.fetchAndroidDevices(existingDeviceDetails, this.pluginArgs);
|
|
150
166
|
logger_1.default.info(`Found ${devices.length} android devices`);
|
|
151
167
|
}
|
|
152
|
-
|
|
153
|
-
return devices.filter((device) => {
|
|
154
|
-
console.log(`Filtering device ${device.udid}, type: ${device.deviceType}, expected real`);
|
|
155
|
-
return device.deviceType === 'real';
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
else if (deviceTypes.androidDeviceType === 'simulated') {
|
|
159
|
-
const simulated = devices.filter((device) => {
|
|
160
|
-
return device.deviceType === 'emulator';
|
|
161
|
-
});
|
|
162
|
-
if (this.pluginArgs.bootedEmulators) {
|
|
163
|
-
return simulated.filter((device) => device.state === 'device');
|
|
164
|
-
}
|
|
165
|
-
return simulated;
|
|
166
|
-
// return both real and simulated (emulated) devices
|
|
167
|
-
}
|
|
168
|
-
else {
|
|
169
|
-
if (this.pluginArgs.bootedEmulators) {
|
|
170
|
-
return devices.filter((device) => {
|
|
171
|
-
if (device.deviceType === 'emulator') {
|
|
172
|
-
return device.state === 'device';
|
|
173
|
-
}
|
|
174
|
-
return true;
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
return devices;
|
|
178
|
-
}
|
|
168
|
+
return devices.filter((device) => includesAndroidDevice(device, deviceTypes.androidDeviceType, this.pluginArgs.bootedEmulators));
|
|
179
169
|
}
|
|
180
170
|
catch (e) {
|
|
181
171
|
logger_1.default.error(`Error while getting android devices. Error: ${e instanceof Error ? e.message : e}`);
|
|
@@ -514,6 +504,16 @@ let AndroidDeviceManager = class AndroidDeviceManager {
|
|
|
514
504
|
return;
|
|
515
505
|
}
|
|
516
506
|
this.cancelAbort(newDevice.udid);
|
|
507
|
+
// Discovery's rule, before anything is leased or written: a phone of a
|
|
508
|
+
// kind this server doesn't serve was listed (and could be allocated)
|
|
509
|
+
// until the next sync pruned it. A phone that can't say goes on to
|
|
510
|
+
// deviceInfo, which needs the same property and skips it.
|
|
511
|
+
const realDevice = yield this.isRealDevice(originalADB, newDevice.udid).catch(() => undefined);
|
|
512
|
+
if (realDevice !== undefined &&
|
|
513
|
+
!includesAndroidDevice({ deviceType: realDevice ? 'real' : 'emulator', state: newDevice.state }, this.pluginArgs.androidDeviceType, this.pluginArgs.bootedEmulators)) {
|
|
514
|
+
logger_1.default.info(`Device ${newDevice.udid} is ${realDevice ? 'a real phone' : 'an emulator'}, and androidDeviceType is ${this.pluginArgs.androidDeviceType}. Ignoring`);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
517
|
const trackedDevice = yield this.deviceInfo(newDevice, originalADB, this.pluginArgs, this.hostPort);
|
|
518
518
|
if (!trackedDevice) {
|
|
519
519
|
logger_1.default.info(`Cannot get device info for ${newDevice.udid}. Skipping`);
|
|
@@ -91,6 +91,8 @@ let AndroidH264StreamService = class AndroidH264StreamService {
|
|
|
91
91
|
this.startWatchdog();
|
|
92
92
|
}
|
|
93
93
|
startWatchdog() {
|
|
94
|
+
// unref()ed below: the server's listener keeps the process alive; this only
|
|
95
|
+
// needs to fire while it runs.
|
|
94
96
|
setInterval(() => {
|
|
95
97
|
const now = Date.now();
|
|
96
98
|
for (const [udid, s] of this.sessions.entries()) {
|
|
@@ -111,7 +113,7 @@ let AndroidH264StreamService = class AndroidH264StreamService {
|
|
|
111
113
|
void this.stop(udid).then(() => (0, previewHold_1.releaseIdlePreviewHold)(udid));
|
|
112
114
|
}
|
|
113
115
|
}
|
|
114
|
-
}, 60000);
|
|
116
|
+
}, 60000).unref();
|
|
115
117
|
}
|
|
116
118
|
getMultiplexer(udid) {
|
|
117
119
|
var _a;
|
|
@@ -150,7 +152,13 @@ let AndroidH264StreamService = class AndroidH264StreamService {
|
|
|
150
152
|
session.capture = yield this.openCapture(udid, onPacket, source);
|
|
151
153
|
// Give the first keyframe/config a moment so callers get a ready stream,
|
|
152
154
|
// but never block start-up indefinitely.
|
|
153
|
-
|
|
155
|
+
let cap;
|
|
156
|
+
try {
|
|
157
|
+
yield Promise.race([firstConfig, new Promise((r) => (cap = setTimeout(r, 3000)))]);
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
clearTimeout(cap);
|
|
161
|
+
}
|
|
154
162
|
return mux;
|
|
155
163
|
}
|
|
156
164
|
catch (e) {
|
|
@@ -81,6 +81,8 @@ let AndroidStreamService = class AndroidStreamService {
|
|
|
81
81
|
this.startWatchdog();
|
|
82
82
|
}
|
|
83
83
|
startWatchdog() {
|
|
84
|
+
// unref()ed below: the server's listener keeps the process alive; this only
|
|
85
|
+
// needs to fire while it runs.
|
|
84
86
|
setInterval(() => __awaiter(this, void 0, void 0, function* () {
|
|
85
87
|
const now = Date.now();
|
|
86
88
|
for (const [udid, session] of this.sessions.entries()) {
|
|
@@ -93,7 +95,7 @@ let AndroidStreamService = class AndroidStreamService {
|
|
|
93
95
|
}
|
|
94
96
|
// Every minute, not every hour: an hourly check left an abandoned
|
|
95
97
|
// preview hold in place for up to 70 minutes. The loop is in memory.
|
|
96
|
-
}), 60000);
|
|
98
|
+
}), 60000).unref();
|
|
97
99
|
}
|
|
98
100
|
/**
|
|
99
101
|
* Resolve the actual adb binary path (and remote-host args) via the same
|
|
@@ -138,7 +138,9 @@ let IOSStreamService = class IOSStreamService {
|
|
|
138
138
|
startWatchdog() {
|
|
139
139
|
// Every minute, and cheap: an unwatched preview's hold was held for good,
|
|
140
140
|
// because the only idle check ran hourly and kept every busy device.
|
|
141
|
-
|
|
141
|
+
// unref()ed: the server's listener keeps the process alive; these only
|
|
142
|
+
// need to fire while it runs.
|
|
143
|
+
setInterval(() => void this.sweepIdle(), 60000).unref();
|
|
142
144
|
// Hourly: port leases and the stream health check, which probes WDA.
|
|
143
145
|
setInterval(() => __awaiter(this, void 0, void 0, function* () {
|
|
144
146
|
for (const [udid, session] of this.sessions.entries()) {
|
|
@@ -187,7 +189,7 @@ let IOSStreamService = class IOSStreamService {
|
|
|
187
189
|
}
|
|
188
190
|
}
|
|
189
191
|
}
|
|
190
|
-
}), 3600000); // 1hr interval for background stability
|
|
192
|
+
}), 3600000).unref(); // 1hr interval for background stability
|
|
191
193
|
}
|
|
192
194
|
/**
|
|
193
195
|
* Stop streams nobody has watched for 10 minutes, which releases an
|
|
@@ -21,6 +21,7 @@ const controlDevice_1 = require("./controlDevice");
|
|
|
21
21
|
const SessionOwnerResolver_1 = require("../services/device-access/SessionOwnerResolver");
|
|
22
22
|
const deviceAccessPolicy_1 = require("../services/device-access/deviceAccessPolicy");
|
|
23
23
|
const actor_1 = require("../services/device-access/actor");
|
|
24
|
+
const deviceClaims_1 = require("../data-service/deviceClaims");
|
|
24
25
|
const STATE_CHANGING = new Set(['POST', 'PUT', 'DELETE', 'PATCH']);
|
|
25
26
|
/**
|
|
26
27
|
* Mutations under /control that this guard must NOT handle.
|
|
@@ -129,7 +130,9 @@ function deviceAccessGuard(deps = {}) {
|
|
|
129
130
|
}
|
|
130
131
|
const decision = (0, deviceAccessPolicy_1.evaluateDeviceAccess)({
|
|
131
132
|
udid,
|
|
132
|
-
|
|
133
|
+
// Not device.busy: a node's phone busy only by its node's report is
|
|
134
|
+
// the node's to judge (heldHere).
|
|
135
|
+
busy: (0, deviceClaims_1.heldHere)(device),
|
|
133
136
|
sessionId: device.session_id,
|
|
134
137
|
sessionOwnerUserId,
|
|
135
138
|
actorUserId: actor.userId,
|
|
@@ -63,6 +63,7 @@ const networkAddresses_1 = require("../helpers/networkAddresses");
|
|
|
63
63
|
const logger_1 = __importDefault(require("../logger"));
|
|
64
64
|
const h264StreamWs_1 = require("../app/ws/h264StreamWs");
|
|
65
65
|
const logcatWs_1 = require("../app/ws/logcatWs");
|
|
66
|
+
const nodeSocketRelay_1 = require("../app/ws/nodeSocketRelay");
|
|
66
67
|
const upgradeRouter_1 = require("../app/ws/upgradeRouter");
|
|
67
68
|
const StreamTicketService_1 = require("./token/StreamTicketService");
|
|
68
69
|
const AndroidH264StreamService_1 = __importDefault(require("../device-managers/android/AndroidH264StreamService"));
|
|
@@ -147,6 +148,8 @@ let ServerManager = ServerManager_1 = class ServerManager {
|
|
|
147
148
|
// streaming.androidH264 config itself, so this WS auto-start honours a
|
|
148
149
|
// { source: 'screenrecord' } rollback just like the REST stream/start.
|
|
149
150
|
startStream: (udid) => typedi_1.Container.get(AndroidH264StreamService_1.default).start(udid),
|
|
151
|
+
// A node's phone is streamed by its node and relayed from here.
|
|
152
|
+
nodeSocket: (udid, actor) => (0, nodeSocketRelay_1.openNodeSocket)({ udid, actor, path: 'stream/h264' }),
|
|
150
153
|
});
|
|
151
154
|
// Continuous logcat WebSocket (Android; replaces the 3s dump-poll).
|
|
152
155
|
// A route of the upgrade router: only /logcat upgrades reach it.
|
|
@@ -191,6 +194,16 @@ let ServerManager = ServerManager_1 = class ServerManager {
|
|
|
191
194
|
// server-side level filter applies.
|
|
192
195
|
return { mux: yield typedi_1.Container.get(LogcatStreamService_1.LogcatStreamService).start(udid) };
|
|
193
196
|
}),
|
|
197
|
+
// A node's phone: its node streams the log, with the same filter.
|
|
198
|
+
nodeSocket: (udid, actor, filter) => {
|
|
199
|
+
var _a;
|
|
200
|
+
return (0, nodeSocketRelay_1.openNodeSocket)({
|
|
201
|
+
udid,
|
|
202
|
+
actor,
|
|
203
|
+
path: 'logcat',
|
|
204
|
+
query: new URLSearchParams(Object.assign(Object.assign({}, (((_a = filter.levels) === null || _a === void 0 ? void 0 : _a.length) ? { levels: filter.levels.join(',') } : {})), (filter.process ? { process: filter.process } : {}))),
|
|
205
|
+
});
|
|
206
|
+
},
|
|
194
207
|
});
|
|
195
208
|
}
|
|
196
209
|
// Principal Cleaning: Attempt to recover remote sessions before marking as failed
|
|
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
12
12
|
exports.makeTicketActorAuthorizer = makeTicketActorAuthorizer;
|
|
13
13
|
const manualLock_1 = require("../recording/manualLock");
|
|
14
14
|
const deviceAccessPolicy_1 = require("./deviceAccessPolicy");
|
|
15
|
+
const deviceClaims_1 = require("../../data-service/deviceClaims");
|
|
15
16
|
/**
|
|
16
17
|
* Build the ownership check for a caller authenticated by a stream ticket.
|
|
17
18
|
*
|
|
@@ -54,7 +55,8 @@ function makeTicketActorAuthorizer(deps) {
|
|
|
54
55
|
: null;
|
|
55
56
|
return (0, deviceAccessPolicy_1.evaluateDeviceAccess)({
|
|
56
57
|
udid,
|
|
57
|
-
|
|
58
|
+
// heldHere, as deviceAccessGuard reads it.
|
|
59
|
+
busy: (0, deviceClaims_1.heldHere)(device),
|
|
58
60
|
sessionId: device.session_id,
|
|
59
61
|
sessionOwnerUserId,
|
|
60
62
|
// The ticket's actorId is a User id — see StreamTicketService.
|
|
@@ -36,11 +36,13 @@ const logger_1 = __importDefault(require("../../logger"));
|
|
|
36
36
|
*/
|
|
37
37
|
function leaseReapReason(lease, now) {
|
|
38
38
|
var _a;
|
|
39
|
+
// The end time first: past it, heartbeats are refused, so a lease that
|
|
40
|
+
// expired has missed its heartbeats too by the next sweep.
|
|
41
|
+
if (lease.expiresAt < now)
|
|
42
|
+
return `expired at ${new Date(lease.expiresAt).toISOString()}`;
|
|
39
43
|
const thresholdMs = ((_a = lease.heartbeatSeconds) !== null && _a !== void 0 ? _a : 30) * 1000 * 3;
|
|
40
44
|
if (lease.lastHeartbeatAt + thresholdMs < now)
|
|
41
45
|
return 'missed heartbeats';
|
|
42
|
-
if (lease.expiresAt < now)
|
|
43
|
-
return `expired at ${new Date(lease.expiresAt).toISOString()}`;
|
|
44
46
|
return null;
|
|
45
47
|
}
|
|
46
48
|
let LeaseOrphanSweeper = class LeaseOrphanSweeper {
|