@xenon-device-management/xenon 2.2.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 +1 -1
- package/lib/public/assets/{AnnotationOverlay-XpewX5nn.js → AnnotationOverlay-AxcBYoKp.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-BQDv3ZCC.js → ApiKeyGate-CIU02EIx.js} +1 -1
- package/lib/public/assets/{BugReportButton-C7OmS61B.js → BugReportButton-_akEEjXm.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-Bhk1udmn.js +2 -0
- package/lib/public/assets/{EmptyState-fPxujqqE.js → EmptyState-DRKRm19_.js} +1 -1
- package/lib/public/assets/{FieldGroup-5bCutWrs.js → FieldGroup-zQQHPh7x.js} +1 -1
- package/lib/public/assets/{FilterMenu-owsBiDT1.js → FilterMenu-C9_StnDi.js} +1 -1
- package/lib/public/assets/{Menu-DFhqzPSt.js → Menu-0J7io6pF.js} +1 -1
- package/lib/public/assets/{Modal-Bw-a-E-e.js → Modal-C677ljNX.js} +1 -1
- package/lib/public/assets/{RecordingPage-WtByf5bC.js → RecordingPage-BDJFck-L.js} +1 -1
- package/lib/public/assets/{RecordingsPage-FpWWFNgT.js → RecordingsPage-DQkimbKK.js} +1 -1
- package/lib/public/assets/{SegmentedControl-Jzn9c7o8.js → SegmentedControl-DvGMZBIE.js} +1 -1
- package/lib/public/assets/{SettingCard-BR7nOhpT.js → SettingCard-ogyouYBq.js} +1 -1
- package/lib/public/assets/{Table-B_Va3V05.js → Table-BLFCOLlO.js} +1 -1
- package/lib/public/assets/{activity-DxRFWvkN.js → activity-BU55TGRG.js} +1 -1
- package/lib/public/assets/{ai-settings-CQf-9_V6.js → ai-settings-GpDAiHav.js} +1 -1
- package/lib/public/assets/{api-keys-SoE1Kd7N.js → api-keys-dOzP0k8_.js} +1 -1
- package/lib/public/assets/{apps-CgGXTlgn.js → apps-mAwAQmkn.js} +1 -1
- package/lib/public/assets/{arrow-left-uQ6WIwiC.js → arrow-left-Dg9mxe9T.js} +1 -1
- package/lib/public/assets/{arrow-right-C9RyfzXS.js → arrow-right-BVcmUyq3.js} +1 -1
- package/lib/public/assets/{arrow-up-right-DFHi_8yG.js → arrow-up-right-ByR5pZrH.js} +1 -1
- package/lib/public/assets/{auth-shell-Bii1t4qc.js → auth-shell-Jpgk8CgY.js} +2 -2
- package/lib/public/assets/{builds-page-Bnv8Z9hi.js → builds-page-VnRTrcdl.js} +1 -1
- package/lib/public/assets/{button-_Y78wXiP.js → button-D-X4wjO8.js} +1 -1
- package/lib/public/assets/{calendar-D4MqSOc2.js → calendar-ur_p5ETA.js} +1 -1
- package/lib/public/assets/{check-uGHCXe-L.js → check-K33iMeyq.js} +1 -1
- package/lib/public/assets/{chevron-left-Dy1OqpzB.js → chevron-left-DMVWShd-.js} +1 -1
- package/lib/public/assets/{chevron-right-BUsldEli.js → chevron-right-DphLJ2_X.js} +1 -1
- package/lib/public/assets/{circle-check-D3Qq0tKp.js → circle-check-CNFhu8oE.js} +1 -1
- package/lib/public/assets/{circle-x-B4IOD1DE.js → circle-x-BH_cUbqZ.js} +1 -1
- package/lib/public/assets/{clock-BfYfzvSg.js → clock-CVMVdVvp.js} +1 -1
- package/lib/public/assets/{copy-DZwYE5Va.js → copy-CZ2XHafL.js} +1 -1
- package/lib/public/assets/{device-explorer-CRG4Utt5.js → device-explorer-B94BhaqF.js} +52 -52
- package/lib/public/assets/{download-CCDYo_TN.js → download-UUiyw7kO.js} +1 -1
- package/lib/public/assets/{forgot-password-DuLCmVFb.js → forgot-password-CrS14F79.js} +1 -1
- package/lib/public/assets/{funnel-CtKNPqzb.js → funnel-BMOE5qJY.js} +1 -1
- package/lib/public/assets/{index-3uxl3o7K.js → index-2XPPbPwy.js} +2 -2
- package/lib/public/assets/{index-Zy1Op5C1.js → index-D0R9Yic3.js} +1 -1
- package/lib/public/assets/{input-CzTWvUXZ.js → input-C-1b8J7i.js} +1 -1
- package/lib/public/assets/{keycodes-CI-nYDFM.js → keycodes-Cly7dQc9.js} +4 -4
- package/lib/public/assets/{layers-YZZGFyTX.js → layers-DFZfqGdX.js} +1 -1
- package/lib/public/assets/{lock-L8tzWgDo.js → lock-ylHMJpfP.js} +1 -1
- package/lib/public/assets/{login-DBbX-fu8.js → login-DWsWKqDx.js} +1 -1
- package/lib/public/assets/{maintenance-settings-AJwkOIsE.js → maintenance-settings-9B6_MTqW.js} +1 -1
- package/lib/public/assets/{monitor-Di_66F2m.js → monitor-CzX0hBH1.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-Cnab2zdg.js → mouse-pointer-2-CZHD7N3z.js} +1 -1
- package/lib/public/assets/{network-DiSumY-1.js → network-Bikc4MyT.js} +1 -1
- package/lib/public/assets/{overview-CwE4gXl5.js → overview-DzaA0aMe.js} +1 -1
- package/lib/public/assets/{page-header-CfR4Sm9V.js → page-header-DqGlDufy.js} +1 -1
- package/lib/public/assets/{plus-DOS8P9CQ.js → plus-Ct6aQTGM.js} +1 -1
- package/lib/public/assets/{profile-page-BYpHwQIH.js → profile-page-CCnUQ1Qk.js} +1 -1
- package/lib/public/assets/{reset-password-BRVJU_qV.js → reset-password-AmkGGXo5.js} +1 -1
- package/lib/public/assets/{runbook-page-Bi3JvAFZ.js → runbook-page-DQQn9VyE.js} +1 -1
- package/lib/public/assets/{select-Cqx7pEou.js → select-k5ogtEWW.js} +1 -1
- package/lib/public/assets/{selector-detail-page-C0DsncKj.js → selector-detail-page-BCcwC5ku.js} +1 -1
- package/lib/public/assets/{selector-health-CAhpdilF.js → selector-health-BVyX6kIh.js} +1 -1
- package/lib/public/assets/{selector-health-page-CrCVeW2m.js → selector-health-page-CElBB6XT.js} +1 -1
- package/lib/public/assets/{session-detail-page-F_hwYqEt.js → session-detail-page-6bqo_-ND.js} +1 -1
- package/lib/public/assets/{settings-C-s5pypL.js → settings-DKBj9KyN.js} +1 -1
- package/lib/public/assets/{status-pill-outline-DY5uzv6A.js → status-pill-outline-CZNpvqej.js} +1 -1
- package/lib/public/assets/{tablet-Dli7gwyg.js → tablet-B6HI5OxV.js} +1 -1
- package/lib/public/assets/{teams-ui4IhHNC.js → teams-owoDHbOU.js} +1 -1
- package/lib/public/assets/{trash-2-BV4hSmXJ.js → trash-2-D61PoUrZ.js} +1 -1
- package/lib/public/assets/{upload-DkGy_J-L.js → upload-1ZYEVvgD.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-BFtMqQOh.js → use-password-reset-mode-Dx0ghiiQ.js} +1 -1
- package/lib/public/assets/{users-Nb_rzRmF.js → users-C5CS5Qez.js} +1 -1
- package/lib/public/assets/{users-BU0nXIVe.js → users-PbGMd5sD.js} +1 -1
- package/lib/public/assets/{video-off-CUNzD2kH.js → video-off-_XusI0HK.js} +1 -1
- package/lib/public/assets/{webhook-settings-Ca3UFXZe.js → webhook-settings-Bi_HHQlQ.js} +1 -1
- package/lib/public/assets/{zap-B2bH2vKX.js → zap-uzHWFAKI.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/src/app/routers/control.js +114 -61
- package/lib/src/app/routers/nodePhoneControl.js +265 -46
- package/lib/src/data-service/device-store.js +7 -3
- package/lib/src/data-service/deviceClaims.js +14 -0
- package/lib/src/data-service/deviceFieldOwners.js +3 -1
- package/lib/src/data-service/prisma-store.js +3 -3
- package/lib/src/device-managers/AndroidDeviceManager.js +11 -1
- 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/test/unit/AndroidDeviceManager.spec.js +12 -0
- package/lib/test/unit/hub-node-control-forward.spec.js +66 -10
- package/lib/test/unit/hub-node-race.spec.js +29 -3
- package/lib/test/unit/mark-node-busy-loki.spec.js +17 -4
- package/lib/test/unit/node-phone-control.spec.js +98 -4
- package/lib/test/unit/screenshot-route.spec.js +104 -0
- package/lib/test/unit/upload-install-route.spec.js +205 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/prisma/migrations/20261001120000_device_node_hold/migration.sql +2 -0
- package/prisma/schema.prisma +4 -0
- package/lib/public/assets/DeviceMosaicView-wl3l2Zlt.js +0 -2
|
@@ -8,15 +8,45 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
|
+
var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); }
|
|
12
|
+
var __asyncValues = (this && this.__asyncValues) || function (o) {
|
|
13
|
+
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
|
|
14
|
+
var m = o[Symbol.asyncIterator], i;
|
|
15
|
+
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
|
|
16
|
+
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
|
|
17
|
+
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
|
|
18
|
+
};
|
|
19
|
+
var __asyncDelegator = (this && this.__asyncDelegator) || function (o) {
|
|
20
|
+
var i, p;
|
|
21
|
+
return i = {}, verb("next"), verb("throw", function (e) { throw e; }), verb("return"), i[Symbol.iterator] = function () { return this; }, i;
|
|
22
|
+
function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: false } : f ? f(v) : v; } : f; }
|
|
23
|
+
};
|
|
24
|
+
var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) {
|
|
25
|
+
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
|
|
26
|
+
var g = generator.apply(thisArg, _arguments || []), i, q = [];
|
|
27
|
+
return i = Object.create((typeof AsyncIterator === "function" ? AsyncIterator : Object).prototype), verb("next"), verb("throw"), verb("return", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i;
|
|
28
|
+
function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; }
|
|
29
|
+
function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } }
|
|
30
|
+
function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }
|
|
31
|
+
function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }
|
|
32
|
+
function fulfill(value) { resume("next", value); }
|
|
33
|
+
function reject(value) { resume("throw", value); }
|
|
34
|
+
function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }
|
|
35
|
+
};
|
|
11
36
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
37
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
38
|
};
|
|
14
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.NODE_CONTROL_TIMEOUT_MS = exports.ANSWERED_HERE = exports.NODE_FORWARDED_CONTROL = void 0;
|
|
40
|
+
exports.NODE_INSTALL_TIMEOUT_MS = exports.NODE_CONTROL_TIMEOUT_MS = exports.ANSWERED_HERE_FOR_NODES = exports.ANSWERED_HERE = exports.NODE_FORWARDED_CONTROL = void 0;
|
|
16
41
|
exports.safeNodeOrigin = safeNodeOrigin;
|
|
17
42
|
exports.nodeControlUrl = nodeControlUrl;
|
|
18
43
|
exports.isOtherServersPhone = isOtherServersPhone;
|
|
19
44
|
exports.nodePhoneControl = nodePhoneControl;
|
|
45
|
+
exports.installFileOnNode = installFileOnNode;
|
|
46
|
+
exports.screenshotFromNode = screenshotFromNode;
|
|
47
|
+
const crypto_1 = require("crypto");
|
|
48
|
+
const fs_1 = __importDefault(require("fs"));
|
|
49
|
+
const stream_1 = require("stream");
|
|
20
50
|
const typedi_1 = require("typedi");
|
|
21
51
|
const logger_1 = __importDefault(require("../../logger"));
|
|
22
52
|
const PluginContext_1 = require("../../PluginContext");
|
|
@@ -24,6 +54,8 @@ const localDeviceHosts_1 = require("../../device-managers/localDeviceHosts");
|
|
|
24
54
|
const actor_1 = require("../../services/device-access/actor");
|
|
25
55
|
const hubSessionToken_1 = require("../../gateway/hubSessionToken");
|
|
26
56
|
const forwardToNode_1 = require("../../gateway/forwardToNode");
|
|
57
|
+
const manualLock_1 = require("../../services/recording/manualLock");
|
|
58
|
+
const SessionOwnerResolver_1 = require("../../services/device-access/SessionOwnerResolver");
|
|
27
59
|
const device_store_1 = require("../../data-service/device-store");
|
|
28
60
|
const deviceAccessPolicy_1 = require("../../services/device-access/deviceAccessPolicy");
|
|
29
61
|
const controlDevice_1 = require("../../middleware/controlDevice");
|
|
@@ -66,6 +98,8 @@ exports.NODE_FORWARDED_CONTROL = [
|
|
|
66
98
|
'POST stream/stop',
|
|
67
99
|
// The MJPEG stream itself: relayed for as long as the viewer watches.
|
|
68
100
|
'GET stream',
|
|
101
|
+
// The upload is streamed on as it came: nothing here reads it first.
|
|
102
|
+
'POST upload-install',
|
|
69
103
|
];
|
|
70
104
|
/**
|
|
71
105
|
* Answered by this server's own handler.
|
|
@@ -77,19 +111,36 @@ exports.NODE_FORWARDED_CONTROL = [
|
|
|
77
111
|
* which get a ticket of the node's own.
|
|
78
112
|
*/
|
|
79
113
|
exports.ANSWERED_HERE = ['GET appium-session', 'POST stream/ticket'];
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
114
|
+
/**
|
|
115
|
+
* Answered by this server's own handler for a node's phone (a cloud
|
|
116
|
+
* provider's is refused), which does its part here and asks the node for
|
|
117
|
+
* the rest:
|
|
118
|
+
* - install-repository-app: the app is in this server's library, checked
|
|
119
|
+
* against the caller's teams here, and sent to the node's upload-install
|
|
120
|
+
* (installFileOnNode).
|
|
121
|
+
* - omni-scan, test-locator: Omni-Vision runs here, with this server's AI
|
|
122
|
+
* settings, on the node's screenshot (screenshotFromNode).
|
|
123
|
+
*/
|
|
124
|
+
exports.ANSWERED_HERE_FOR_NODES = [
|
|
125
|
+
'POST install-repository-app',
|
|
126
|
+
'GET omni-scan',
|
|
127
|
+
'POST test-locator',
|
|
128
|
+
];
|
|
129
|
+
// Not on any list, so refused: install takes a path, which names a file on
|
|
130
|
+
// one machine only.
|
|
87
131
|
/**
|
|
88
132
|
* A node that hasn't started answering in this long is treated as gone. Once
|
|
89
133
|
* it has, the answer may take as long as it takes: a stream lasts as long as
|
|
90
134
|
* its viewer.
|
|
91
135
|
*/
|
|
92
136
|
exports.NODE_CONTROL_TIMEOUT_MS = 60000;
|
|
137
|
+
/**
|
|
138
|
+
* An install is answered when it is done, which can take minutes (a large
|
|
139
|
+
* app, a slow phone): its node gets this long to start answering instead.
|
|
140
|
+
*/
|
|
141
|
+
exports.NODE_INSTALL_TIMEOUT_MS = 15 * 60000;
|
|
142
|
+
/** Forwarded actions that are installs, and get NODE_INSTALL_TIMEOUT_MS. */
|
|
143
|
+
const INSTALLS = ['POST upload-install'];
|
|
93
144
|
// Cloud metadata endpoints — never proxy to these regardless of caller.
|
|
94
145
|
const FORBIDDEN_PROXY_HOSTS = new Set([
|
|
95
146
|
'169.254.169.254', // AWS/Azure/GCP IMDS
|
|
@@ -166,10 +217,15 @@ function refuse(res, error, message) {
|
|
|
166
217
|
return res.status(501).json({ success: false, error, message });
|
|
167
218
|
}
|
|
168
219
|
function nodePhoneControl(deps = {}) {
|
|
169
|
-
var _a, _b, _c;
|
|
220
|
+
var _a, _b, _c, _d, _e, _f;
|
|
170
221
|
const findDevice = (_a = deps.findDevice) !== null && _a !== void 0 ? _a : controlDevice_1.findControlDeviceInStore;
|
|
171
|
-
const
|
|
172
|
-
|
|
222
|
+
const forwardDeps = {
|
|
223
|
+
timeoutMs: (_b = deps.timeoutMs) !== null && _b !== void 0 ? _b : exports.NODE_CONTROL_TIMEOUT_MS,
|
|
224
|
+
installTimeoutMs: (_c = deps.installTimeoutMs) !== null && _c !== void 0 ? _c : exports.NODE_INSTALL_TIMEOUT_MS,
|
|
225
|
+
markNodeBusy: (_d = deps.markNodeBusy) !== null && _d !== void 0 ? _d : ((udid, host, hold) => device_store_1.DeviceStoreFactory.getStore().markNodeBusy(udid, host, hold)),
|
|
226
|
+
describeHolder: (_e = deps.describeHolder) !== null && _e !== void 0 ? _e : ((id) => typedi_1.Container.get(SessionOwnerResolver_1.SessionOwnerResolver).displayName(id)),
|
|
227
|
+
controlToken: (_f = deps.controlToken) !== null && _f !== void 0 ? _f : defaultControlToken,
|
|
228
|
+
};
|
|
173
229
|
return function (req, res, next) {
|
|
174
230
|
return __awaiter(this, void 0, void 0, function* () {
|
|
175
231
|
var _a;
|
|
@@ -201,30 +257,32 @@ function nodePhoneControl(deps = {}) {
|
|
|
201
257
|
if (device.cloud) {
|
|
202
258
|
return refuse(res, 'not_available_for_cloud_phone', "This phone is a cloud provider's. Device control isn't available for it here.");
|
|
203
259
|
}
|
|
260
|
+
if (exports.ANSWERED_HERE_FOR_NODES.includes(key))
|
|
261
|
+
return next();
|
|
204
262
|
const node = originOf(device.host);
|
|
205
263
|
if (!exports.NODE_FORWARDED_CONTROL.includes(key)) {
|
|
206
264
|
return refuse(res, 'not_available_through_hub', `This phone is on node ${node}, and the hub doesn't pass ${action} on to nodes yet.`);
|
|
207
265
|
}
|
|
208
|
-
return forward(req, res, { udid, device, node
|
|
266
|
+
return forward(req, res, { udid, device, node }, forwardDeps);
|
|
209
267
|
});
|
|
210
268
|
};
|
|
211
269
|
}
|
|
212
|
-
|
|
270
|
+
const defaultControlToken = (grant) => typedi_1.Container.get(hubSessionToken_1.HubSessionTokenIssuer).controlTokenFor(grant);
|
|
271
|
+
/**
|
|
272
|
+
* The headers of a call to the phone's node, signed for the caller. Null
|
|
273
|
+
* token when this server can't sign (it warns once): the call goes without,
|
|
274
|
+
* which a node with auth disabled accepts and one with auth enabled refuses.
|
|
275
|
+
*/
|
|
276
|
+
function signedHeaders(req, udid, device, controlToken) {
|
|
213
277
|
return __awaiter(this, void 0, void 0, function* () {
|
|
214
|
-
var _a
|
|
215
|
-
const { udid, device, node, timeoutMs } = target;
|
|
216
|
-
const url = nodeControlUrl(device.host, req);
|
|
217
|
-
if (!url)
|
|
218
|
-
return res.status(400).json({ error: 'Unsafe device host' });
|
|
278
|
+
var _a;
|
|
219
279
|
const actor = (0, actor_1.resolveActor)(req);
|
|
220
280
|
const headers = {
|
|
221
281
|
accept: (_a = req.headers.accept) !== null && _a !== void 0 ? _a : 'application/json',
|
|
222
282
|
'accept-encoding': 'identity',
|
|
223
283
|
};
|
|
224
|
-
// Null when this server can't sign (it warns once): the call goes without,
|
|
225
|
-
// which a node with auth disabled accepts and one with auth enabled refuses.
|
|
226
284
|
const token = actor.userId
|
|
227
|
-
? yield
|
|
285
|
+
? yield controlToken({
|
|
228
286
|
userId: actor.userId,
|
|
229
287
|
isAdmin: actor.isAdmin,
|
|
230
288
|
udid,
|
|
@@ -233,44 +291,42 @@ function forward(req, res, target) {
|
|
|
233
291
|
: null;
|
|
234
292
|
if (token)
|
|
235
293
|
headers[hubSessionToken_1.HUB_TOKEN_HEADER] = token;
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
294
|
+
return headers;
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Send one call to the node and relay its answer to `res`. A node that can't
|
|
299
|
+
* be reached is 502, and one that hasn't started answering in `timeoutMs` is
|
|
300
|
+
* 504; once it has, the answer takes as long as it takes. `onAnswer` sees the
|
|
301
|
+
* node's answer first and may reply itself (returning true). Sent once: a
|
|
302
|
+
* retry after a slow node's timeout or 5xx could land the same tap twice.
|
|
303
|
+
*/
|
|
304
|
+
function relayNodeCall(res, call, onAnswer) {
|
|
305
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
306
|
+
var _a, _b, _c;
|
|
241
307
|
const controller = new AbortController();
|
|
242
308
|
let timedOut = false;
|
|
243
309
|
const timer = setTimeout(() => {
|
|
244
310
|
timedOut = true;
|
|
245
311
|
controller.abort();
|
|
246
|
-
}, timeoutMs);
|
|
312
|
+
}, call.timeoutMs);
|
|
247
313
|
// The caller left: stop waiting for the node on their behalf.
|
|
248
314
|
const onClose = () => {
|
|
249
315
|
if (!res.writableFinished)
|
|
250
316
|
controller.abort();
|
|
251
317
|
};
|
|
252
318
|
res.on('close', onClose);
|
|
253
|
-
logger_1.default.debug(`Forwarding ${req.method} ${(0, controlDevice_1.controlAction)(req)} for ${udid} to node ${node}`);
|
|
254
319
|
try {
|
|
255
320
|
const upstream = yield (0, forwardToNode_1.sendToNode)({
|
|
256
|
-
url,
|
|
257
|
-
method:
|
|
258
|
-
headers,
|
|
259
|
-
body,
|
|
321
|
+
url: call.url,
|
|
322
|
+
method: call.method,
|
|
323
|
+
headers: call.headers,
|
|
324
|
+
body: call.body,
|
|
260
325
|
signal: controller.signal,
|
|
261
326
|
});
|
|
262
|
-
// The node has answered: from here the answer takes as long as it takes.
|
|
263
327
|
clearTimeout(timer);
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
// The node holds the phone for the preview now, and says so in its
|
|
267
|
-
// next report, up to an interval away. Until then this server could
|
|
268
|
-
// hand the phone to a session the node would refuse.
|
|
269
|
-
yield target.markNodeBusy(udid, device.host).catch((e) => {
|
|
270
|
-
var _a;
|
|
271
|
-
logger_1.default.warn(`nodePhoneControl: could not mark ${udid} busy for its node: ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}`);
|
|
272
|
-
});
|
|
273
|
-
}
|
|
328
|
+
if (onAnswer && (yield onAnswer(upstream)))
|
|
329
|
+
return;
|
|
274
330
|
yield (0, forwardToNode_1.relayAnswer)(upstream, res, false);
|
|
275
331
|
}
|
|
276
332
|
catch (e) {
|
|
@@ -281,15 +337,15 @@ function forward(req, res, target) {
|
|
|
281
337
|
res.status(504).json({
|
|
282
338
|
success: false,
|
|
283
339
|
error: 'node_timeout',
|
|
284
|
-
message: `Node ${node} didn't answer within ${Math.round(timeoutMs / 1000)} s.`,
|
|
340
|
+
message: `Node ${call.node} didn't answer within ${Math.round(call.timeoutMs / 1000)} s.`,
|
|
285
341
|
});
|
|
286
342
|
}
|
|
287
343
|
else {
|
|
288
|
-
logger_1.default.warn(`nodePhoneControl: node ${node} unreachable for ${udid}: ${(
|
|
344
|
+
logger_1.default.warn(`nodePhoneControl: node ${call.node} unreachable for ${call.udid}: ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}`);
|
|
289
345
|
res.status(502).json({
|
|
290
346
|
success: false,
|
|
291
347
|
error: 'node_unreachable',
|
|
292
|
-
message: `Xenon could not reach node ${node} for this phone: ${(
|
|
348
|
+
message: `Xenon could not reach node ${call.node} for this phone: ${(_c = (_b = e === null || e === void 0 ? void 0 : e.code) !== null && _b !== void 0 ? _b : e === null || e === void 0 ? void 0 : e.message) !== null && _c !== void 0 ? _c : e}`,
|
|
293
349
|
});
|
|
294
350
|
}
|
|
295
351
|
}
|
|
@@ -299,3 +355,166 @@ function forward(req, res, target) {
|
|
|
299
355
|
}
|
|
300
356
|
});
|
|
301
357
|
}
|
|
358
|
+
function forward(req, res, target, deps) {
|
|
359
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
360
|
+
var _a;
|
|
361
|
+
const { udid, device, node } = target;
|
|
362
|
+
const url = nodeControlUrl(device.host, req);
|
|
363
|
+
if (!url)
|
|
364
|
+
return res.status(400).json({ error: 'Unsafe device host' });
|
|
365
|
+
const key = `${req.method} ${(0, controlDevice_1.controlAction)(req)}`;
|
|
366
|
+
const actor = (0, actor_1.resolveActor)(req);
|
|
367
|
+
const headers = yield signedHeaders(req, udid, device, deps.controlToken);
|
|
368
|
+
let body;
|
|
369
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
370
|
+
const type = req.headers['content-type'];
|
|
371
|
+
if (typeof type === 'string' && type.toLowerCase().startsWith('multipart/')) {
|
|
372
|
+
// An upload, streamed on as it came: the route's own parser runs after
|
|
373
|
+
// this, so nothing here has read it.
|
|
374
|
+
body = req;
|
|
375
|
+
headers['content-type'] = type;
|
|
376
|
+
if (req.headers['content-length'])
|
|
377
|
+
headers['content-length'] = req.headers['content-length'];
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
body = Buffer.from(JSON.stringify((_a = req.body) !== null && _a !== void 0 ? _a : {}));
|
|
381
|
+
headers['content-type'] = 'application/json';
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
logger_1.default.debug(`Forwarding ${key} for ${udid} to node ${node}`);
|
|
385
|
+
const timeoutMs = INSTALLS.includes(key) ? deps.installTimeoutMs : deps.timeoutMs;
|
|
386
|
+
yield relayNodeCall(res, { url, method: req.method, headers, body, timeoutMs, node, udid }, (upstream) => __awaiter(this, void 0, void 0, function* () {
|
|
387
|
+
var _a;
|
|
388
|
+
const status = (_a = upstream.statusCode) !== null && _a !== void 0 ? _a : 502;
|
|
389
|
+
if (key === 'POST stream/start' && status < 300) {
|
|
390
|
+
// The node holds the phone for the preview now, and says so in its
|
|
391
|
+
// next report, up to an interval away. Until then this server could
|
|
392
|
+
// hand the phone to a session the node would refuse, and would show
|
|
393
|
+
// no holder. The node writes the same hold for the same user.
|
|
394
|
+
const hold = actor.userId ? (0, manualLock_1.formatManualLock)(actor.userId, udid) : undefined;
|
|
395
|
+
yield deps.markNodeBusy(udid, device.host, hold).catch((e) => {
|
|
396
|
+
var _a;
|
|
397
|
+
logger_1.default.warn(`nodePhoneControl: could not mark ${udid} busy for its node: ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}`);
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
if (status === 409) {
|
|
401
|
+
yield relayRefusal(yield (0, forwardToNode_1.readAnswer)(upstream), res, deps.describeHolder);
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
return false;
|
|
405
|
+
}));
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* A file as a multipart upload in one field, streamed from disk, with its
|
|
410
|
+
* length known so the node's parser can apply its limit up front.
|
|
411
|
+
*/
|
|
412
|
+
function multipartFile(field, file) {
|
|
413
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
414
|
+
const boundary = `----xenon-${(0, crypto_1.randomBytes)(12).toString('hex')}`;
|
|
415
|
+
const name = file.name.replace(/["\\\r\n]/g, '_');
|
|
416
|
+
const head = Buffer.from(`--${boundary}\r\n` +
|
|
417
|
+
`Content-Disposition: form-data; name="${field}"; filename="${name}"\r\n` +
|
|
418
|
+
'Content-Type: application/octet-stream\r\n\r\n');
|
|
419
|
+
const tail = Buffer.from(`\r\n--${boundary}--\r\n`);
|
|
420
|
+
const { size } = yield fs_1.default.promises.stat(file.path);
|
|
421
|
+
const stream = stream_1.Readable.from((function () {
|
|
422
|
+
return __asyncGenerator(this, arguments, function* () {
|
|
423
|
+
yield yield __await(head);
|
|
424
|
+
yield __await(yield* __asyncDelegator(__asyncValues(fs_1.default.createReadStream(file.path))));
|
|
425
|
+
yield yield __await(tail);
|
|
426
|
+
});
|
|
427
|
+
})());
|
|
428
|
+
return {
|
|
429
|
+
type: `multipart/form-data; boundary=${boundary}`,
|
|
430
|
+
length: head.length + size + tail.length,
|
|
431
|
+
stream,
|
|
432
|
+
};
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Install a file of this server's on another server's phone: sent to the
|
|
437
|
+
* node's upload-install as a multipart upload, signed for the caller, and
|
|
438
|
+
* the node's answer relayed. install-repository-app's, for an app from this
|
|
439
|
+
* server's library, which the node doesn't have. The file's name keeps the
|
|
440
|
+
* extension the node's installer reads.
|
|
441
|
+
*/
|
|
442
|
+
function installFileOnNode(req_1, res_1, udid_1, device_1, file_1) {
|
|
443
|
+
return __awaiter(this, arguments, void 0, function* (req, res, udid, device, file, deps = {}) {
|
|
444
|
+
var _a, _b;
|
|
445
|
+
const origin = safeNodeOrigin(device.host);
|
|
446
|
+
if (!origin) {
|
|
447
|
+
res.status(400).json({ error: 'Unsafe device host' });
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const headers = yield signedHeaders(req, udid, device, (_a = deps.controlToken) !== null && _a !== void 0 ? _a : defaultControlToken);
|
|
451
|
+
const upload = yield multipartFile('app', file);
|
|
452
|
+
headers['content-type'] = upload.type;
|
|
453
|
+
headers['content-length'] = upload.length;
|
|
454
|
+
logger_1.default.info(`Sending ${file.name} to node ${origin} to install on ${udid}`);
|
|
455
|
+
yield relayNodeCall(res, {
|
|
456
|
+
url: `${origin}/xenon/api/control/${encodeURIComponent(udid)}/upload-install`,
|
|
457
|
+
method: 'POST',
|
|
458
|
+
headers,
|
|
459
|
+
body: upload.stream,
|
|
460
|
+
timeoutMs: (_b = deps.timeoutMs) !== null && _b !== void 0 ? _b : exports.NODE_INSTALL_TIMEOUT_MS,
|
|
461
|
+
node: origin,
|
|
462
|
+
udid,
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* The node's screenshot of another server's phone, base64, for work this
|
|
468
|
+
* server does on it: Omni-Vision, with this server's AI settings. Throws when
|
|
469
|
+
* the node doesn't give one.
|
|
470
|
+
*/
|
|
471
|
+
function screenshotFromNode(req_1, udid_1, device_1) {
|
|
472
|
+
return __awaiter(this, arguments, void 0, function* (req, udid, device, deps = {}) {
|
|
473
|
+
var _a, _b, _c;
|
|
474
|
+
const origin = safeNodeOrigin(device.host);
|
|
475
|
+
if (!origin)
|
|
476
|
+
throw new Error('Unsafe device host');
|
|
477
|
+
const headers = yield signedHeaders(req, udid, device, (_a = deps.controlToken) !== null && _a !== void 0 ? _a : defaultControlToken);
|
|
478
|
+
headers.accept = 'application/json';
|
|
479
|
+
const answer = yield (0, forwardToNode_1.readAnswer)(yield (0, forwardToNode_1.sendToNode)({
|
|
480
|
+
url: `${origin}/xenon/api/control/${encodeURIComponent(udid)}/screenshot`,
|
|
481
|
+
method: 'GET',
|
|
482
|
+
headers,
|
|
483
|
+
signal: AbortSignal.timeout((_b = deps.timeoutMs) !== null && _b !== void 0 ? _b : exports.NODE_CONTROL_TIMEOUT_MS),
|
|
484
|
+
}));
|
|
485
|
+
if (answer.status !== 200) {
|
|
486
|
+
throw new Error(`Node ${origin} answered ${answer.status} for a screenshot`);
|
|
487
|
+
}
|
|
488
|
+
const shot = (_c = JSON.parse(answer.body.toString('utf8'))) === null || _c === void 0 ? void 0 : _c.screenshot;
|
|
489
|
+
if (typeof shot !== 'string' || !shot)
|
|
490
|
+
throw new Error(`Node ${origin} sent no screenshot`);
|
|
491
|
+
return shot;
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
const HOLDER_REFUSALS = new Set(['device_held_by_another_user', 'device_in_use_by_session']);
|
|
495
|
+
/**
|
|
496
|
+
* A node's 409, with the holder's name filled in when the node could only
|
|
497
|
+
* give their id: its database has no rows for this server's users, so it
|
|
498
|
+
* says "another user". The wording is denyBody's, as for this server's own
|
|
499
|
+
* refusals. Anything else is passed on as it came.
|
|
500
|
+
*/
|
|
501
|
+
function relayRefusal(answer, res, describeHolder) {
|
|
502
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
503
|
+
var _a;
|
|
504
|
+
let refusal;
|
|
505
|
+
try {
|
|
506
|
+
refusal = JSON.parse(answer.body.toString('utf8'));
|
|
507
|
+
}
|
|
508
|
+
catch (_b) {
|
|
509
|
+
return (0, forwardToNode_1.sendAnswer)(res, answer);
|
|
510
|
+
}
|
|
511
|
+
const holderId = (_a = refusal === null || refusal === void 0 ? void 0 : refusal.holder) === null || _a === void 0 ? void 0 : _a.userId;
|
|
512
|
+
if (!HOLDER_REFUSALS.has(refusal === null || refusal === void 0 ? void 0 : refusal.error) || typeof holderId !== 'string' || refusal.holder.name) {
|
|
513
|
+
return (0, forwardToNode_1.sendAnswer)(res, answer);
|
|
514
|
+
}
|
|
515
|
+
const name = yield describeHolder(holderId).catch(() => null);
|
|
516
|
+
if (!name)
|
|
517
|
+
return (0, forwardToNode_1.sendAnswer)(res, answer);
|
|
518
|
+
res.status(409).json(Object.assign(Object.assign({}, refusal), (0, deviceAccessPolicy_1.denyBody)(refusal.error, holderId, name)));
|
|
519
|
+
});
|
|
520
|
+
}
|
|
@@ -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,7 +295,7 @@ class LokiDeviceStore {
|
|
|
293
295
|
return true;
|
|
294
296
|
});
|
|
295
297
|
}
|
|
296
|
-
markNodeBusy(udid, host) {
|
|
298
|
+
markNodeBusy(udid, host, hold) {
|
|
297
299
|
return __awaiter(this, void 0, void 0, function* () {
|
|
298
300
|
const model = yield db_1.XenonDatabase.DeviceModel;
|
|
299
301
|
const device = model.findOne({ udid, host });
|
|
@@ -301,6 +303,8 @@ class LokiDeviceStore {
|
|
|
301
303
|
return;
|
|
302
304
|
device.nodeBusy = true;
|
|
303
305
|
device.busy = true;
|
|
306
|
+
if (hold)
|
|
307
|
+
device.nodeHold = hold;
|
|
304
308
|
model.update(device);
|
|
305
309
|
});
|
|
306
310
|
}
|
|
@@ -7,7 +7,9 @@ exports.claimWhere = claimWhere;
|
|
|
7
7
|
exports.holdsClaim = holdsClaim;
|
|
8
8
|
exports.hasNoClaim = hasNoClaim;
|
|
9
9
|
exports.isUnheld = isUnheld;
|
|
10
|
+
exports.nodeHoldOf = nodeHoldOf;
|
|
10
11
|
exports.heldHere = heldHere;
|
|
12
|
+
const manualLock_1 = require("../services/recording/manualLock");
|
|
11
13
|
/**
|
|
12
14
|
* Why a phone is busy, on the server that allocates it.
|
|
13
15
|
*
|
|
@@ -95,6 +97,18 @@ exports.UNHELD = Object.assign(Object.assign({}, exports.NO_CLAIM), { session_id
|
|
|
95
97
|
function isUnheld(device) {
|
|
96
98
|
return hasNoClaim(device) && device.session_id == null && device.nodeBusy !== true;
|
|
97
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
|
+
}
|
|
98
112
|
/**
|
|
99
113
|
* Busy for a reason this server has on record: its claim, a session or hold
|
|
100
114
|
* in `session_id`, or plain `busy` on a phone of its own. False for a node's
|
|
@@ -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,11 +386,11 @@ class PrismaDeviceStore {
|
|
|
386
386
|
return cleared.count > 0;
|
|
387
387
|
});
|
|
388
388
|
}
|
|
389
|
-
markNodeBusy(udid, host) {
|
|
389
|
+
markNodeBusy(udid, host, hold) {
|
|
390
390
|
return __awaiter(this, void 0, void 0, function* () {
|
|
391
391
|
yield this.prisma.device.updateMany({
|
|
392
392
|
where: { udid, host },
|
|
393
|
-
data: { nodeBusy: true, busy: true },
|
|
393
|
+
data: Object.assign({ nodeBusy: true, busy: true }, (hold ? { nodeHold: hold } : {})),
|
|
394
394
|
});
|
|
395
395
|
});
|
|
396
396
|
}
|
|
@@ -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.ANDROID_INSTALL_TIMEOUT_MS = void 0;
|
|
57
58
|
exports.includesAndroidDevice = includesAndroidDevice;
|
|
58
59
|
const displayState_1 = require("./android/displayState");
|
|
59
60
|
const networkAddresses_1 = require("../helpers/networkAddresses");
|
|
@@ -81,6 +82,13 @@ const deviceIdentity_1 = require("./deviceIdentity");
|
|
|
81
82
|
const localDeviceHosts_1 = require("./localDeviceHosts");
|
|
82
83
|
const PluginContext_1 = require("../PluginContext");
|
|
83
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;
|
|
84
92
|
/**
|
|
85
93
|
* Whether a server set to `androidDeviceType` serves this phone: real phones,
|
|
86
94
|
* emulators ('simulated'), or both, and with `bootedEmulators` only the
|
|
@@ -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) {
|
|
@@ -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) {
|