@xenon-device-management/xenon 2.1.0 → 2.3.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-AxcBYoKp.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-DmYQ0oSk.js → ApiKeyGate-CIU02EIx.js} +1 -1
- package/lib/public/assets/{BugReportButton-n8T1dFUp.js → BugReportButton-_akEEjXm.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-Bhk1udmn.js +2 -0
- package/lib/public/assets/{EmptyState-DExETevp.js → EmptyState-DRKRm19_.js} +1 -1
- package/lib/public/assets/{FieldGroup-CCdFlZm6.js → FieldGroup-zQQHPh7x.js} +1 -1
- package/lib/public/assets/{FilterMenu-DiUSOSL-.js → FilterMenu-C9_StnDi.js} +1 -1
- package/lib/public/assets/{Menu-ZOfpH6gh.js → Menu-0J7io6pF.js} +1 -1
- package/lib/public/assets/{Modal-CZNtoB-1.js → Modal-C677ljNX.js} +1 -1
- package/lib/public/assets/{RecordingPage-BKp6h8Jl.js → RecordingPage-BDJFck-L.js} +1 -1
- package/lib/public/assets/{RecordingsPage-Bf881FX4.js → RecordingsPage-DQkimbKK.js} +1 -1
- package/lib/public/assets/{SegmentedControl-EUoyizvW.js → SegmentedControl-DvGMZBIE.js} +1 -1
- package/lib/public/assets/{SettingCard-ClT4hf4_.js → SettingCard-ogyouYBq.js} +1 -1
- package/lib/public/assets/{Table-ucR4bnmc.js → Table-BLFCOLlO.js} +1 -1
- package/lib/public/assets/{activity-K-hdylwq.js → activity-BU55TGRG.js} +1 -1
- package/lib/public/assets/{ai-settings-BLhdwI3c.js → ai-settings-GpDAiHav.js} +1 -1
- package/lib/public/assets/{api-keys-BbiN7rBI.js → api-keys-dOzP0k8_.js} +1 -1
- package/lib/public/assets/{apps-Dez1petz.js → apps-mAwAQmkn.js} +1 -1
- package/lib/public/assets/{arrow-left-UjmFHLAJ.js → arrow-left-Dg9mxe9T.js} +1 -1
- package/lib/public/assets/{arrow-right-BVyEigbu.js → arrow-right-BVcmUyq3.js} +1 -1
- package/lib/public/assets/{arrow-up-right-B-ZlBof3.js → arrow-up-right-ByR5pZrH.js} +1 -1
- package/lib/public/assets/{auth-shell-DAQjAgUv.js → auth-shell-Jpgk8CgY.js} +2 -2
- package/lib/public/assets/{builds-page-CmIPa5JK.js → builds-page-VnRTrcdl.js} +1 -1
- package/lib/public/assets/{button-Dd-IhMC1.js → button-D-X4wjO8.js} +1 -1
- package/lib/public/assets/{calendar-CAIeJwa5.js → calendar-ur_p5ETA.js} +1 -1
- package/lib/public/assets/{check-C4Jrjg8f.js → check-K33iMeyq.js} +1 -1
- package/lib/public/assets/{chevron-left-BGzlf-4i.js → chevron-left-DMVWShd-.js} +1 -1
- package/lib/public/assets/{chevron-right-_wYqDjDC.js → chevron-right-DphLJ2_X.js} +1 -1
- package/lib/public/assets/{circle-check-Ckn00yMB.js → circle-check-CNFhu8oE.js} +1 -1
- package/lib/public/assets/{circle-x-pdVkomLa.js → circle-x-BH_cUbqZ.js} +1 -1
- package/lib/public/assets/{clock-B8vmwkHj.js → clock-CVMVdVvp.js} +1 -1
- package/lib/public/assets/{copy-BAqnFI-F.js → copy-CZ2XHafL.js} +1 -1
- package/lib/public/assets/{device-explorer-CYq_4F_N.js → device-explorer-B94BhaqF.js} +52 -52
- package/lib/public/assets/{download-B6UEAp_1.js → download-UUiyw7kO.js} +1 -1
- package/lib/public/assets/{forgot-password-CLqNvmrN.js → forgot-password-CrS14F79.js} +1 -1
- package/lib/public/assets/{funnel-Da90BBbT.js → funnel-BMOE5qJY.js} +1 -1
- package/lib/public/assets/{index-BuIIcmGp.js → index-2XPPbPwy.js} +43 -43
- package/lib/public/assets/{index-MX7IzqyW.js → index-D0R9Yic3.js} +1 -1
- package/lib/public/assets/{input-Bq9aY-We.js → input-C-1b8J7i.js} +1 -1
- package/lib/public/assets/{keycodes-CIhXIJyP.js → keycodes-Cly7dQc9.js} +4 -4
- package/lib/public/assets/{layers-bXjpswQI.js → layers-DFZfqGdX.js} +1 -1
- package/lib/public/assets/{lock-pJ72y5Fy.js → lock-ylHMJpfP.js} +1 -1
- package/lib/public/assets/{login-UOcHALV2.js → login-DWsWKqDx.js} +1 -1
- package/lib/public/assets/{maintenance-settings-XnJgp5yb.js → maintenance-settings-9B6_MTqW.js} +1 -1
- package/lib/public/assets/{monitor-QkAhI1Jx.js → monitor-CzX0hBH1.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-B42Da0m4.js → mouse-pointer-2-CZHD7N3z.js} +1 -1
- package/lib/public/assets/{network-CKVqcyp6.js → network-Bikc4MyT.js} +1 -1
- package/lib/public/assets/{overview-BORGkN3N.js → overview-DzaA0aMe.js} +1 -1
- package/lib/public/assets/{page-header-CKWFOjOI.js → page-header-DqGlDufy.js} +1 -1
- package/lib/public/assets/{plus-BpFKVdC8.js → plus-Ct6aQTGM.js} +1 -1
- package/lib/public/assets/{profile-page-CcbKBicY.js → profile-page-CCnUQ1Qk.js} +1 -1
- package/lib/public/assets/{reset-password-BA0W9bxl.js → reset-password-AmkGGXo5.js} +1 -1
- package/lib/public/assets/{runbook-page-Cp7ZBq3s.js → runbook-page-DQQn9VyE.js} +1 -1
- package/lib/public/assets/{select-DIZWIoVR.js → select-k5ogtEWW.js} +1 -1
- package/lib/public/assets/{selector-detail-page-C_4YuvAp.js → selector-detail-page-BCcwC5ku.js} +1 -1
- package/lib/public/assets/{selector-health-CXjD_Xth.js → selector-health-BVyX6kIh.js} +1 -1
- package/lib/public/assets/{selector-health-page-CTqIHeoG.js → selector-health-page-CElBB6XT.js} +1 -1
- package/lib/public/assets/{session-detail-page-B0KYkk8z.js → session-detail-page-6bqo_-ND.js} +1 -1
- package/lib/public/assets/{settings-BcRBKUXd.js → settings-DKBj9KyN.js} +1 -1
- package/lib/public/assets/{status-pill-outline-Dh2PXSf_.js → status-pill-outline-CZNpvqej.js} +1 -1
- package/lib/public/assets/{tablet-_sXqZztt.js → tablet-B6HI5OxV.js} +1 -1
- package/lib/public/assets/{teams-DGHT8MKm.js → teams-owoDHbOU.js} +1 -1
- package/lib/public/assets/{trash-2-BV88cV3Z.js → trash-2-D61PoUrZ.js} +1 -1
- package/lib/public/assets/{upload-CIDH9wkj.js → upload-1ZYEVvgD.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DYGuSNwl.js → use-password-reset-mode-Dx0ghiiQ.js} +1 -1
- package/lib/public/assets/{users-7yviqflc.js → users-C5CS5Qez.js} +1 -1
- package/lib/public/assets/{users-BUE7MoTq.js → users-PbGMd5sD.js} +1 -1
- package/lib/public/assets/{video-off-BTRxtlGo.js → video-off-_XusI0HK.js} +1 -1
- package/lib/public/assets/{webhook-settings-DMKD9Pgm.js → webhook-settings-Bi_HHQlQ.js} +1 -1
- package/lib/public/assets/{zap-CWE94qcK.js → zap-uzHWFAKI.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/src/app/routers/control.js +119 -203
- package/lib/src/app/routers/nodePhoneControl.js +520 -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 +17 -2
- package/lib/src/data-service/deviceClaims.js +32 -0
- package/lib/src/data-service/deviceFieldOwners.js +3 -1
- package/lib/src/data-service/prisma-store.js +9 -1
- package/lib/src/device-managers/AndroidDeviceManager.js +38 -28
- 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/gateway/forwardToNode.js +10 -3
- package/lib/src/generated/client/edge.js +5 -4
- package/lib/src/generated/client/index-browser.js +2 -1
- package/lib/src/generated/client/index.d.ts +36 -1
- package/lib/src/generated/client/index.js +5 -4
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +4 -0
- package/lib/src/generated/client/wasm.js +2 -1
- 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 +60 -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 +374 -1
- package/lib/test/unit/hub-node-race.spec.js +51 -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 +66 -0
- package/lib/test/unit/node-phone-control.spec.js +225 -0
- package/lib/test/unit/node-socket-relay.spec.js +357 -0
- package/lib/test/unit/screenshot-route.spec.js +104 -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/test/unit/upload-install-route.spec.js +205 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/prisma/migrations/20261001120000_device_node_hold/migration.sql +2 -0
- package/prisma/schema.prisma +4 -0
- package/lib/public/assets/DeviceMosaicView-D-rGOBbJ.js +0 -2
|
@@ -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
|
+
}
|
|
@@ -173,8 +173,9 @@ class LokiDeviceStore {
|
|
|
173
173
|
const existing = deviceModel.findOne({ udid: device.udid, host: device.host });
|
|
174
174
|
// A node's report (PrismaDeviceStore.addNodeReport has the rule).
|
|
175
175
|
const nodeBusy = device.busy === true;
|
|
176
|
+
const nodeHold = (0, deviceClaims_1.nodeHoldOf)(device.session_id);
|
|
176
177
|
if (existing && options.nodeReport) {
|
|
177
|
-
Object.assign(existing, (0, deviceFieldOwners_1.pickNodeReportFields)(device), { nodeBusy });
|
|
178
|
+
Object.assign(existing, (0, deviceFieldOwners_1.pickNodeReportFields)(device), { nodeBusy, nodeHold });
|
|
178
179
|
if (nodeBusy)
|
|
179
180
|
existing.busy = true;
|
|
180
181
|
else if ((0, deviceClaims_1.isUnheld)(existing))
|
|
@@ -184,7 +185,8 @@ class LokiDeviceStore {
|
|
|
184
185
|
}
|
|
185
186
|
if (!existing) {
|
|
186
187
|
const cleanDevice = options.nodeReport
|
|
187
|
-
? Object.assign(Object.assign({}, (0, deviceFieldOwners_1.pickNodeReportFields)(device)), { udid: device.udid, host: device.host, busy: nodeBusy, nodeBusy
|
|
188
|
+
? Object.assign(Object.assign({}, (0, deviceFieldOwners_1.pickNodeReportFields)(device)), { udid: device.udid, host: device.host, busy: nodeBusy, nodeBusy,
|
|
189
|
+
nodeHold })
|
|
188
190
|
: Object.assign({}, device);
|
|
189
191
|
if (cleanDevice.host === undefined)
|
|
190
192
|
cleanDevice.host = 'Local';
|
|
@@ -293,6 +295,19 @@ class LokiDeviceStore {
|
|
|
293
295
|
return true;
|
|
294
296
|
});
|
|
295
297
|
}
|
|
298
|
+
markNodeBusy(udid, host, hold) {
|
|
299
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
300
|
+
const model = yield db_1.XenonDatabase.DeviceModel;
|
|
301
|
+
const device = model.findOne({ udid, host });
|
|
302
|
+
if (!device)
|
|
303
|
+
return;
|
|
304
|
+
device.nodeBusy = true;
|
|
305
|
+
device.busy = true;
|
|
306
|
+
if (hold)
|
|
307
|
+
device.nodeHold = hold;
|
|
308
|
+
model.update(device);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
296
311
|
resetMetrics() {
|
|
297
312
|
return __awaiter(this, void 0, void 0, function* () {
|
|
298
313
|
(yield db_1.XenonDatabase.DeviceModel)
|
|
@@ -7,6 +7,9 @@ exports.claimWhere = claimWhere;
|
|
|
7
7
|
exports.holdsClaim = holdsClaim;
|
|
8
8
|
exports.hasNoClaim = hasNoClaim;
|
|
9
9
|
exports.isUnheld = isUnheld;
|
|
10
|
+
exports.nodeHoldOf = nodeHoldOf;
|
|
11
|
+
exports.heldHere = heldHere;
|
|
12
|
+
const manualLock_1 = require("../services/recording/manualLock");
|
|
10
13
|
/**
|
|
11
14
|
* Why a phone is busy, on the server that allocates it.
|
|
12
15
|
*
|
|
@@ -94,5 +97,34 @@ exports.UNHELD = Object.assign(Object.assign({}, exports.NO_CLAIM), { session_id
|
|
|
94
97
|
function isUnheld(device) {
|
|
95
98
|
return hasNoClaim(device) && device.session_id == null && device.nodeBusy !== true;
|
|
96
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Who holds a node's phone there, from the node's report of it: its
|
|
102
|
+
* `session_id` when that is a preview hold (`manual_<user>_<udid>`), else
|
|
103
|
+
* null. A session the node runs is not a hold. The hub keeps it as
|
|
104
|
+
* `nodeHold`, apart from its own `session_id`, and shows it; the node decides
|
|
105
|
+
* access, so a stale value can change a label and nothing else.
|
|
106
|
+
*/
|
|
107
|
+
function nodeHoldOf(reportedSessionId) {
|
|
108
|
+
return typeof reportedSessionId === 'string' && (0, manualLock_1.isManualLock)(reportedSessionId)
|
|
109
|
+
? reportedSessionId
|
|
110
|
+
: null;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Busy for a reason this server has on record: its claim, a session or hold
|
|
114
|
+
* in `session_id`, or plain `busy` on a phone of its own. False for a node's
|
|
115
|
+
* phone busy only by the node's report (`nodeBusy`), a preview the node
|
|
116
|
+
* holds say: this server knows no holder for it, and the node, which does,
|
|
117
|
+
* judges every call forwarded there with its own guard. An ownership check
|
|
118
|
+
* reads this, not `busy`, or it fails closed on every such phone, refusing
|
|
119
|
+
* the preview's own holder. A server with no nodes never has nodeBusy, so
|
|
120
|
+
* for it this is `busy`.
|
|
121
|
+
*/
|
|
122
|
+
function heldHere(device) {
|
|
123
|
+
if (!device.busy)
|
|
124
|
+
return false;
|
|
125
|
+
if (device.nodeBusy !== true)
|
|
126
|
+
return true;
|
|
127
|
+
return device.session_id != null || device.claimedAt != null || device.claimSessionId != null;
|
|
128
|
+
}
|
|
97
129
|
/** What ending a claim resets: the claim, and the session's bookkeeping. */
|
|
98
130
|
exports.CLAIM_RESET = Object.assign(Object.assign({}, exports.NO_CLAIM), { session_id: null, lastCmdExecutedAt: null, sessionStartTime: 0, newCommandTimeout: null, sessionProgress: '', owningSessionId: null, lockedAt: null });
|
|
@@ -89,6 +89,7 @@ const OWNER = {
|
|
|
89
89
|
thermalStatus: 'observed',
|
|
90
90
|
storageFree: 'observed',
|
|
91
91
|
nodeBusy: 'report',
|
|
92
|
+
nodeHold: 'report',
|
|
92
93
|
};
|
|
93
94
|
/** The columns a sync may write to a phone it already knows. */
|
|
94
95
|
exports.DISCOVERY_FIELDS = new Set(Object.keys(OWNER).filter((column) => OWNER[column] === 'discovery'));
|
|
@@ -98,7 +99,8 @@ function pickDiscoveryFields(data) {
|
|
|
98
99
|
}
|
|
99
100
|
/**
|
|
100
101
|
* The columns a hub takes from a node's report of a phone: what the node
|
|
101
|
-
* observes of it. Not `nodeBusy`, which the hub sets from the
|
|
102
|
+
* observes of it. Not `nodeBusy` and `nodeHold`, which the hub sets from the
|
|
103
|
+
* report's `busy` and `session_id` (deviceClaims.ts nodeHoldOf).
|
|
102
104
|
*/
|
|
103
105
|
exports.NODE_REPORT_FIELDS = new Set(Object.keys(OWNER).filter((column) => OWNER[column] === 'discovery' || OWNER[column] === 'observed'));
|
|
104
106
|
/** Only the columns of `data` a hub takes from a node's report. */
|
|
@@ -254,7 +254,7 @@ class PrismaDeviceStore {
|
|
|
254
254
|
return __awaiter(this, void 0, void 0, function* () {
|
|
255
255
|
const { udid, host } = device;
|
|
256
256
|
const nodeBusy = device.busy === true;
|
|
257
|
-
const reported = Object.assign(Object.assign({}, (0, deviceFieldOwners_1.pickNodeReportFields)(this.fromIDevice(device))), { nodeBusy });
|
|
257
|
+
const reported = Object.assign(Object.assign({}, (0, deviceFieldOwners_1.pickNodeReportFields)(this.fromIDevice(device))), { nodeBusy, nodeHold: (0, deviceClaims_1.nodeHoldOf)(device.session_id) });
|
|
258
258
|
const row = yield this.prisma.device.upsert({
|
|
259
259
|
where: { udid_host: { udid, host } },
|
|
260
260
|
update: nodeBusy ? Object.assign(Object.assign({}, reported), { busy: true }) : reported,
|
|
@@ -386,6 +386,14 @@ class PrismaDeviceStore {
|
|
|
386
386
|
return cleared.count > 0;
|
|
387
387
|
});
|
|
388
388
|
}
|
|
389
|
+
markNodeBusy(udid, host, hold) {
|
|
390
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
391
|
+
yield this.prisma.device.updateMany({
|
|
392
|
+
where: { udid, host },
|
|
393
|
+
data: Object.assign({ nodeBusy: true, busy: true }, (hold ? { nodeHold: hold } : {})),
|
|
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,8 @@ 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.ANDROID_INSTALL_TIMEOUT_MS = void 0;
|
|
58
|
+
exports.includesAndroidDevice = includesAndroidDevice;
|
|
57
59
|
const displayState_1 = require("./android/displayState");
|
|
58
60
|
const networkAddresses_1 = require("../helpers/networkAddresses");
|
|
59
61
|
const appium_adb_1 = require("appium-adb");
|
|
@@ -80,6 +82,28 @@ const deviceIdentity_1 = require("./deviceIdentity");
|
|
|
80
82
|
const localDeviceHosts_1 = require("./localDeviceHosts");
|
|
81
83
|
const PluginContext_1 = require("../PluginContext");
|
|
82
84
|
const typedi_2 = require("typedi");
|
|
85
|
+
/**
|
|
86
|
+
* How long `adb install` may take. It answers when the phone has installed
|
|
87
|
+
* the app, which for a large app on a slow phone is minutes; appium-adb's
|
|
88
|
+
* own limit (adbExecTimeout, 20 s) reported such installs as failed though
|
|
89
|
+
* they had worked. Within the 15 minutes a hub gives a node's install.
|
|
90
|
+
*/
|
|
91
|
+
exports.ANDROID_INSTALL_TIMEOUT_MS = 10 * 60000;
|
|
92
|
+
/**
|
|
93
|
+
* Whether a server set to `androidDeviceType` serves this phone: real phones,
|
|
94
|
+
* emulators ('simulated'), or both, and with `bootedEmulators` only the
|
|
95
|
+
* emulators that are up. Discovery and a plugged phone (onDeviceAdded) both
|
|
96
|
+
* ask this, so a phone the server excludes is never added by either.
|
|
97
|
+
*/
|
|
98
|
+
function includesAndroidDevice(device, androidDeviceType, bootedEmulators) {
|
|
99
|
+
if (androidDeviceType === 'real')
|
|
100
|
+
return device.deviceType === 'real';
|
|
101
|
+
if (androidDeviceType === 'simulated' && device.deviceType !== 'emulator')
|
|
102
|
+
return false;
|
|
103
|
+
if (bootedEmulators && device.deviceType === 'emulator')
|
|
104
|
+
return device.state === 'device';
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
83
107
|
let AndroidDeviceManager = class AndroidDeviceManager {
|
|
84
108
|
constructor(context) {
|
|
85
109
|
this.context = context;
|
|
@@ -149,33 +173,7 @@ let AndroidDeviceManager = class AndroidDeviceManager {
|
|
|
149
173
|
devices = yield this.fetchAndroidDevices(existingDeviceDetails, this.pluginArgs);
|
|
150
174
|
logger_1.default.info(`Found ${devices.length} android devices`);
|
|
151
175
|
}
|
|
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
|
-
}
|
|
176
|
+
return devices.filter((device) => includesAndroidDevice(device, deviceTypes.androidDeviceType, this.pluginArgs.bootedEmulators));
|
|
179
177
|
}
|
|
180
178
|
catch (e) {
|
|
181
179
|
logger_1.default.error(`Error while getting android devices. Error: ${e instanceof Error ? e.message : e}`);
|
|
@@ -514,6 +512,16 @@ let AndroidDeviceManager = class AndroidDeviceManager {
|
|
|
514
512
|
return;
|
|
515
513
|
}
|
|
516
514
|
this.cancelAbort(newDevice.udid);
|
|
515
|
+
// Discovery's rule, before anything is leased or written: a phone of a
|
|
516
|
+
// kind this server doesn't serve was listed (and could be allocated)
|
|
517
|
+
// until the next sync pruned it. A phone that can't say goes on to
|
|
518
|
+
// deviceInfo, which needs the same property and skips it.
|
|
519
|
+
const realDevice = yield this.isRealDevice(originalADB, newDevice.udid).catch(() => undefined);
|
|
520
|
+
if (realDevice !== undefined &&
|
|
521
|
+
!includesAndroidDevice({ deviceType: realDevice ? 'real' : 'emulator', state: newDevice.state }, this.pluginArgs.androidDeviceType, this.pluginArgs.bootedEmulators)) {
|
|
522
|
+
logger_1.default.info(`Device ${newDevice.udid} is ${realDevice ? 'a real phone' : 'an emulator'}, and androidDeviceType is ${this.pluginArgs.androidDeviceType}. Ignoring`);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
517
525
|
const trackedDevice = yield this.deviceInfo(newDevice, originalADB, this.pluginArgs, this.hostPort);
|
|
518
526
|
if (!trackedDevice) {
|
|
519
527
|
logger_1.default.info(`Cannot get device info for ${newDevice.udid}. Skipping`);
|
|
@@ -784,7 +792,9 @@ let AndroidDeviceManager = class AndroidDeviceManager {
|
|
|
784
792
|
const { adbInstance } = yield this.getAdb();
|
|
785
793
|
if (!adbInstance)
|
|
786
794
|
throw new Error('ADB is not available');
|
|
787
|
-
yield adbInstance.adbExec(['-s', udid, 'install', '-r', appPath]
|
|
795
|
+
yield adbInstance.adbExec(['-s', udid, 'install', '-r', appPath], {
|
|
796
|
+
timeout: exports.ANDROID_INSTALL_TIMEOUT_MS,
|
|
797
|
+
});
|
|
788
798
|
});
|
|
789
799
|
}
|
|
790
800
|
uninstallApp(udid, bundleId) {
|
|
@@ -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
|
|
@@ -74,12 +74,19 @@ function sendToNode(request) {
|
|
|
74
74
|
const url = new URL(request.url);
|
|
75
75
|
const lib = url.protocol === 'https:' ? https_1.default : http_1.default;
|
|
76
76
|
const headers = Object.assign({}, request.headers);
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
const body = request.body;
|
|
78
|
+
if (Buffer.isBuffer(body))
|
|
79
|
+
headers['content-length'] = body.length;
|
|
79
80
|
return new Promise((resolve, reject) => {
|
|
80
81
|
const outgoing = lib.request(url, { method: request.method, headers, agent: proxyAgentFor(url), signal: request.signal }, resolve);
|
|
81
82
|
outgoing.on('error', reject);
|
|
82
|
-
|
|
83
|
+
if (body && !Buffer.isBuffer(body)) {
|
|
84
|
+
body.on('error', (err) => outgoing.destroy(err));
|
|
85
|
+
body.pipe(outgoing);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
outgoing.end(body);
|
|
89
|
+
}
|
|
83
90
|
});
|
|
84
91
|
}
|
|
85
92
|
function copyResponseHeaders(upstream, res) {
|