@xenon-device-management/xenon 1.2.0 → 1.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/README.md +74 -0
- package/lib/package.json +1 -1
- package/lib/public/assets/{Layouts-D0WSzKOh.js → Layouts-D6IPfwoe.js} +1 -1
- package/lib/public/assets/{ai-settings-DQWDdNd7.js → ai-settings-CflyFKan.js} +1 -1
- package/lib/public/assets/{apps-1sLWHOGO.js → apps-Da4dvQ1J.js} +1 -1
- package/lib/public/assets/{badge-BiR1gmMm.js → badge-BNR9umdu.js} +1 -1
- package/lib/public/assets/{button-BVazt4Z1.js → button-hZFV1ypT.js} +1 -1
- package/lib/public/assets/{calendar-yMyP2_Nc.js → calendar-fehdBtun.js} +1 -1
- package/lib/public/assets/{clock-CsVplnJ2.js → clock-DrpxSvCL.js} +1 -1
- package/lib/public/assets/{cpu-DNC8n7kK.js → cpu-tuyMVZ4I.js} +1 -1
- package/lib/public/assets/{device-explorer-DFu8Gxj4.js → device-explorer-DOfRH3zm.js} +1 -1
- package/lib/public/assets/{index-S71J2rWg.js → index-BaTiUCeH.js} +18 -18
- package/lib/public/assets/{lock-BstCxnX6.js → lock-C6CoqSr2.js} +1 -1
- package/lib/public/assets/{maintenance-settings-BwfG9cu2.js → maintenance-settings-CM2oC7-i.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CSn_Wnc9.js → mouse-pointer-2-CXdnjXIg.js} +1 -1
- package/lib/public/assets/{plus-DfjM7G6e.js → plus-B4B1Hukt.js} +1 -1
- package/lib/public/assets/{session-dashboard-C6ek4z65.js → session-dashboard-B5OPMTz5.js} +1 -1
- package/lib/public/assets/{settings-BDYP8ULf.js → settings-BTHP7fj3.js} +1 -1
- package/lib/public/assets/{trash-2-CZWUMK5b.js → trash-2-NJMZJ2Ol.js} +1 -1
- package/lib/public/assets/{useSocket-CliVeWS3.js → useSocket-Ct2wo7P2.js} +2 -2
- package/lib/public/assets/{webhook-settings-tPiwWf8y.js → webhook-settings-Cz35-QJ7.js} +1 -1
- package/lib/public/assets/{zap-ZrK5B58i.js → zap-CssSMAN5.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +85 -38
- package/lib/src/InternalHttpClient.js +69 -14
- package/lib/src/app/index.js +92 -24
- package/lib/src/app/routers/apikeys.js +33 -0
- package/lib/src/app/routers/apps.js +4 -0
- package/lib/src/app/routers/auth.js +36 -0
- package/lib/src/app/routers/config.js +4 -0
- package/lib/src/app/routers/control.js +61 -10
- package/lib/src/app/routers/dashboard.js +5 -6
- package/lib/src/app/routers/grid.js +30 -12
- package/lib/src/app/routers/processes.js +24 -0
- package/lib/src/app/routers/reservation.js +15 -0
- package/lib/src/app/routers/webhook.js +6 -3
- package/lib/src/auth/nodeSecret.js +33 -0
- package/lib/src/config.js +5 -0
- package/lib/src/data-service/prisma-store.js +17 -1
- package/lib/src/device-managers/AndroidDeviceManager.js +2 -2
- package/lib/src/device-managers/NodeDevices.js +8 -1
- package/lib/src/device-managers/ios/IOSDiscoveryService.js +7 -4
- package/lib/src/device-managers/ios/IOSStreamService.js +7 -0
- package/lib/src/device-managers/ios/WDAClient.js +2 -0
- package/lib/src/device-utils.js +29 -4
- package/lib/src/generated/client/edge.js +2 -2
- package/lib/src/generated/client/index.js +2 -2
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +3 -0
- package/lib/src/helpers/UniversalMjpegProxy.js +23 -0
- package/lib/src/index.js +10 -2
- package/lib/src/interceptors/CommandInterceptor.js +29 -0
- package/lib/src/interfaces/IPluginArgs.js +0 -1
- package/lib/src/logger.js +30 -2
- package/lib/src/logging/sessionContext.js +28 -0
- package/lib/src/middleware/apiKeyMiddleware.js +49 -0
- package/lib/src/middleware/csrfMiddleware.js +73 -0
- package/lib/src/middleware/nodeSecretMiddleware.js +38 -0
- package/lib/src/middleware/rateLimitMiddleware.js +68 -0
- package/lib/src/middleware/scopeGuard.js +41 -0
- package/lib/src/plugin.js +1 -1
- package/lib/src/services/AIService.js +43 -8
- package/lib/src/services/ApiKeyService.js +102 -0
- package/lib/src/services/CircuitBreaker.js +158 -0
- package/lib/src/services/CleanupService.js +137 -39
- package/lib/src/services/DeviceReconciler.js +102 -0
- package/lib/src/services/MetricsService.js +78 -0
- package/lib/src/services/PortAllocator.js +13 -0
- package/lib/src/services/ProcessMetricsService.js +99 -0
- package/lib/src/services/ProcessRegistry.js +123 -0
- package/lib/src/services/ServerManager.js +14 -2
- package/lib/src/services/SessionLifecycleService.js +80 -23
- package/lib/src/services/ShutdownCoordinator.js +89 -0
- package/lib/src/services/SocketClient.js +11 -0
- package/lib/src/services/SocketServer.js +109 -6
- package/lib/src/services/VideoPipelineService.js +2 -0
- package/lib/src/services/healing/HealingMetrics.js +63 -0
- package/lib/src/services/healing/HealingOrchestrator.js +32 -4
- package/lib/src/services/healing/OcrHealingProvider.js +7 -0
- package/lib/test/unit/ApiKeyService.test.js +101 -0
- package/lib/test/unit/PortAllocator.test.js +14 -0
- package/lib/test/unit/ProcessRegistry.test.js +70 -0
- package/lib/test/unit/apiKeyMiddleware.test.js +58 -0
- package/lib/test/unit/nodeSecretMiddleware.test.js +38 -0
- package/lib/test/unit/rateLimitMiddleware.test.js +37 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/prisma/migrations/20260423081701_add_session_indexes/migration.sql +8 -0
- package/prisma/schema.prisma +3 -0
- package/schema.json +85 -38
|
@@ -47,9 +47,13 @@ const config_service_1 = require("../../data-service/config-service");
|
|
|
47
47
|
const typedi_1 = require("typedi");
|
|
48
48
|
const logger_1 = __importStar(require("../../logger"));
|
|
49
49
|
const AIService_1 = require("../../services/AIService");
|
|
50
|
+
const scopeGuard_1 = require("../../middleware/scopeGuard");
|
|
50
51
|
class ConfigRouter {
|
|
51
52
|
static register(router, pluginArgs) {
|
|
52
53
|
const configRouter = (0, express_1.Router)();
|
|
54
|
+
// All mutations under /config require admin scope (global plugin
|
|
55
|
+
// config + AI provider test probes). GET passthrough for read scope.
|
|
56
|
+
configRouter.use((0, scopeGuard_1.mutationScopeGuard)(['admin']));
|
|
53
57
|
function getMaskedConfig(args) {
|
|
54
58
|
const masked = Object.assign({}, args);
|
|
55
59
|
// Boolean flags for secret status
|
|
@@ -60,7 +60,43 @@ const os_1 = __importDefault(require("os"));
|
|
|
60
60
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
61
61
|
const OmniVisionService_1 = require("../../services/omni-vision/OmniVisionService");
|
|
62
62
|
const InspectorService_1 = require("../../services/InspectorService");
|
|
63
|
+
const scopeGuard_1 = require("../../middleware/scopeGuard");
|
|
63
64
|
const router = (0, express_1.Router)();
|
|
65
|
+
// Every mutation under /control (tap, swipe, install, shell, lock, etc.)
|
|
66
|
+
// requires devices scope. Read endpoints (screenshots, page source) stay
|
|
67
|
+
// open to any authenticated key.
|
|
68
|
+
router.use((0, scopeGuard_1.mutationScopeGuard)(['devices']));
|
|
69
|
+
// Cloud metadata endpoints — never proxy to these regardless of caller.
|
|
70
|
+
const FORBIDDEN_PROXY_HOSTS = new Set([
|
|
71
|
+
'169.254.169.254', // AWS/Azure/GCP IMDS
|
|
72
|
+
'metadata.google.internal',
|
|
73
|
+
'metadata.goog',
|
|
74
|
+
'100.100.100.200', // Alibaba ECS metadata
|
|
75
|
+
'fd00:ec2::254', // AWS IMDSv6
|
|
76
|
+
]);
|
|
77
|
+
/**
|
|
78
|
+
* Build a safe proxy URL from a device's reported host. Strips any path,
|
|
79
|
+
* query, or fragment the host string carried, blocks cloud-metadata targets,
|
|
80
|
+
* and refuses non-http(s) schemes. Returns null if the host is unsafe.
|
|
81
|
+
*/
|
|
82
|
+
function buildProxyUrl(deviceHost, req) {
|
|
83
|
+
let parsed;
|
|
84
|
+
try {
|
|
85
|
+
parsed = new URL(deviceHost);
|
|
86
|
+
}
|
|
87
|
+
catch (_a) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
|
91
|
+
return null;
|
|
92
|
+
if (FORBIDDEN_PROXY_HOSTS.has(parsed.hostname))
|
|
93
|
+
return null;
|
|
94
|
+
// Only keep scheme + host + port; discard any attacker-baked path/query/fragment.
|
|
95
|
+
const origin = `${parsed.protocol}//${parsed.host}`;
|
|
96
|
+
// Treat req.originalUrl as a path — reparse to strip any control characters.
|
|
97
|
+
const forwardPath = req.originalUrl.startsWith('/') ? req.originalUrl : `/${req.originalUrl}`;
|
|
98
|
+
return `${origin}${forwardPath}`;
|
|
99
|
+
}
|
|
64
100
|
function getDeviceInfo(udid) {
|
|
65
101
|
return __awaiter(this, void 0, void 0, function* () {
|
|
66
102
|
return yield device_store_1.DeviceStoreFactory.getStore().findDevice({ udid });
|
|
@@ -89,9 +125,12 @@ router.post('/:udid/tap', (req, res) => __awaiter(void 0, void 0, void 0, functi
|
|
|
89
125
|
return res.status(404).send('Device not found');
|
|
90
126
|
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
91
127
|
if (device.host && !device.host.includes(req.get('host') || '')) {
|
|
92
|
-
|
|
128
|
+
const target = buildProxyUrl(device.host, req);
|
|
129
|
+
if (!target)
|
|
130
|
+
return res.status(400).send({ error: 'Unsafe device host' });
|
|
131
|
+
logger_1.default.info(`Proxying tap for ${udid} to ${target}`);
|
|
93
132
|
try {
|
|
94
|
-
yield InternalHttpClient_1.InternalHttpClient.post(
|
|
133
|
+
yield InternalHttpClient_1.InternalHttpClient.post(target, req.body);
|
|
95
134
|
return res.status(200).send({ success: true });
|
|
96
135
|
}
|
|
97
136
|
catch (err) {
|
|
@@ -130,9 +169,12 @@ router.post('/:udid/swipe', (req, res) => __awaiter(void 0, void 0, void 0, func
|
|
|
130
169
|
return res.status(404).send('Device not found');
|
|
131
170
|
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
132
171
|
if (device.host && !device.host.includes(req.get('host') || '')) {
|
|
133
|
-
|
|
172
|
+
const target = buildProxyUrl(device.host, req);
|
|
173
|
+
if (!target)
|
|
174
|
+
return res.status(400).send({ error: 'Unsafe device host' });
|
|
175
|
+
logger_1.default.info(`Proxying swipe for ${udid} to ${target}`);
|
|
134
176
|
try {
|
|
135
|
-
yield InternalHttpClient_1.InternalHttpClient.post(
|
|
177
|
+
yield InternalHttpClient_1.InternalHttpClient.post(target, req.body);
|
|
136
178
|
return res.status(200).send({ success: true });
|
|
137
179
|
}
|
|
138
180
|
catch (err) {
|
|
@@ -160,9 +202,12 @@ router.post('/:udid/text', (req, res) => __awaiter(void 0, void 0, void 0, funct
|
|
|
160
202
|
return res.status(404).send('Device not found');
|
|
161
203
|
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
162
204
|
if (device.host && !device.host.includes(req.get('host') || '')) {
|
|
163
|
-
|
|
205
|
+
const target = buildProxyUrl(device.host, req);
|
|
206
|
+
if (!target)
|
|
207
|
+
return res.status(400).send({ error: 'Unsafe device host' });
|
|
208
|
+
logger_1.default.info(`Proxying typeText for ${udid} to ${target}`);
|
|
164
209
|
try {
|
|
165
|
-
yield InternalHttpClient_1.InternalHttpClient.post(
|
|
210
|
+
yield InternalHttpClient_1.InternalHttpClient.post(target, req.body);
|
|
166
211
|
return res.status(200).send({ success: true });
|
|
167
212
|
}
|
|
168
213
|
catch (err) {
|
|
@@ -190,9 +235,12 @@ router.post('/:udid/keyevent', (req, res) => __awaiter(void 0, void 0, void 0, f
|
|
|
190
235
|
return res.status(404).send('Device not found');
|
|
191
236
|
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
192
237
|
if (device.host && !device.host.includes(req.get('host') || '')) {
|
|
193
|
-
|
|
238
|
+
const target = buildProxyUrl(device.host, req);
|
|
239
|
+
if (!target)
|
|
240
|
+
return res.status(400).send({ error: 'Unsafe device host' });
|
|
241
|
+
logger_1.default.info(`Proxying keyevent for ${udid} to ${target}`);
|
|
194
242
|
try {
|
|
195
|
-
yield InternalHttpClient_1.InternalHttpClient.post(
|
|
243
|
+
yield InternalHttpClient_1.InternalHttpClient.post(target, req.body);
|
|
196
244
|
return res.status(200).send({ success: true });
|
|
197
245
|
}
|
|
198
246
|
catch (err) {
|
|
@@ -286,9 +334,12 @@ router.post('/:udid/touchAndHold', (req, res) => __awaiter(void 0, void 0, void
|
|
|
286
334
|
return res.status(404).send('Device not found');
|
|
287
335
|
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
288
336
|
if (device.host && !device.host.includes(req.get('host') || '')) {
|
|
289
|
-
|
|
337
|
+
const target = buildProxyUrl(device.host, req);
|
|
338
|
+
if (!target)
|
|
339
|
+
return res.status(400).send({ error: 'Unsafe device host' });
|
|
340
|
+
logger_1.default.info(`Proxying touchAndHold for ${udid} to ${target}`);
|
|
290
341
|
try {
|
|
291
|
-
yield InternalHttpClient_1.InternalHttpClient.post(
|
|
342
|
+
yield InternalHttpClient_1.InternalHttpClient.post(target, req.body);
|
|
292
343
|
return res.status(200).send({ success: true });
|
|
293
344
|
}
|
|
294
345
|
catch (err) {
|
|
@@ -47,15 +47,12 @@ const SessionManager_1 = require("../../sessions/SessionManager");
|
|
|
47
47
|
const UniversalMjpegProxy_1 = require("../../helpers/UniversalMjpegProxy");
|
|
48
48
|
const web_config_service_1 = require("../../data-service/web-config-service");
|
|
49
49
|
const typedi_1 = require("typedi");
|
|
50
|
+
const scopeGuard_1 = require("../../middleware/scopeGuard");
|
|
50
51
|
const MJPEG_PROXY_CACHE = new Map();
|
|
51
52
|
//session guard
|
|
52
53
|
function isValidSession(request, response, next) {
|
|
53
54
|
return __awaiter(this, void 0, void 0, function* () {
|
|
54
55
|
const sessionId = request.params.sessionId;
|
|
55
|
-
// Principal Robustness: Allow virtual manual sessions
|
|
56
|
-
if (sessionId && sessionId.startsWith('manual_')) {
|
|
57
|
-
return next();
|
|
58
|
-
}
|
|
59
56
|
const session = yield prisma_1.prisma.session.findFirst({
|
|
60
57
|
where: {
|
|
61
58
|
id: sessionId,
|
|
@@ -293,8 +290,10 @@ function register(router) {
|
|
|
293
290
|
router.get('/session/:sessionId/logs/debug', getDebugLogs);
|
|
294
291
|
router.get('/session/:sessionId/profiling', getProfilingData);
|
|
295
292
|
router.get('/config', getGlobalConfig);
|
|
296
|
-
|
|
297
|
-
|
|
293
|
+
// Config + destructive ops: admin-only. Read-only config stays open to any
|
|
294
|
+
// authenticated key so dashboards using 'read' scope can still populate.
|
|
295
|
+
router.post('/config', (0, scopeGuard_1.scopeGuard)(['admin']), updateGlobalConfig);
|
|
296
|
+
router.post('/config/reset-metrics', (0, scopeGuard_1.scopeGuard)(['admin']), resetMetrics);
|
|
298
297
|
}
|
|
299
298
|
exports.default = {
|
|
300
299
|
register,
|
|
@@ -50,6 +50,7 @@ const queue_service_1 = require("../../data-service/queue-service");
|
|
|
50
50
|
const InternalHttpClient_1 = require("../../InternalHttpClient");
|
|
51
51
|
const lodash_1 = __importDefault(require("lodash"));
|
|
52
52
|
const device_service_1 = require("../../data-service/device-service");
|
|
53
|
+
const scopeGuard_1 = require("../../middleware/scopeGuard");
|
|
53
54
|
const logger_1 = __importDefault(require("../../logger"));
|
|
54
55
|
const device_managers_1 = require("../../device-managers");
|
|
55
56
|
const typedi_1 = require("typedi");
|
|
@@ -135,24 +136,38 @@ function blockDevice(request, response) {
|
|
|
135
136
|
return __awaiter(this, void 0, void 0, function* () {
|
|
136
137
|
const requestBody = request.body;
|
|
137
138
|
const device = yield (0, device_service_1.getDevice)(requestBody);
|
|
138
|
-
if (
|
|
139
|
+
if (lodash_1.default.isNil(device)) {
|
|
140
|
+
return response.status(404).json({ success: false, error: 'Device not found' });
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
139
143
|
yield (0, device_service_1.userBlockDevice)(device.udid, device.host);
|
|
140
144
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
145
|
+
catch (err) {
|
|
146
|
+
logger_1.default.error(`Failed to block device ${device.udid}@${device.host}: ${(err === null || err === void 0 ? void 0 : err.message) || err}`);
|
|
147
|
+
return response
|
|
148
|
+
.status(500)
|
|
149
|
+
.json({ success: false, error: (err === null || err === void 0 ? void 0 : err.message) || 'Failed to block device' });
|
|
150
|
+
}
|
|
151
|
+
response.status(200).send({ success: true });
|
|
144
152
|
});
|
|
145
153
|
}
|
|
146
154
|
function unBlockDevice(request, response) {
|
|
147
155
|
return __awaiter(this, void 0, void 0, function* () {
|
|
148
156
|
const requestBody = request.body;
|
|
149
157
|
const device = yield (0, device_service_1.getDevice)(requestBody);
|
|
150
|
-
if (
|
|
158
|
+
if (lodash_1.default.isNil(device)) {
|
|
159
|
+
return response.status(404).json({ success: false, error: 'Device not found' });
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
151
162
|
yield (0, device_service_1.userUnblockDevice)(device.udid, device.host);
|
|
152
163
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
164
|
+
catch (err) {
|
|
165
|
+
logger_1.default.error(`Failed to unblock device ${device.udid}@${device.host}: ${(err === null || err === void 0 ? void 0 : err.message) || err}`);
|
|
166
|
+
return response
|
|
167
|
+
.status(500)
|
|
168
|
+
.json({ success: false, error: (err === null || err === void 0 ? void 0 : err.message) || 'Failed to unblock device' });
|
|
169
|
+
}
|
|
170
|
+
response.status(200).send({ success: true });
|
|
156
171
|
});
|
|
157
172
|
}
|
|
158
173
|
function getQueuedSessionLength(request, response) {
|
|
@@ -323,10 +338,13 @@ function register(router, pluginArgs) {
|
|
|
323
338
|
router.get('/devices', getDevices);
|
|
324
339
|
router.get('/device', getDevices);
|
|
325
340
|
router.get('/device/:platform', getDeviceByPlatform);
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
router.post('/
|
|
341
|
+
// Node registration + device manipulation all require devices scope.
|
|
342
|
+
// Node bootstrap keys have admin scope so they pass; operator keys
|
|
343
|
+
// need an explicit 'devices' grant.
|
|
344
|
+
router.post('/register', (0, scopeGuard_1.scopeGuard)(['devices']), registerNode);
|
|
345
|
+
router.post('/block', (0, scopeGuard_1.scopeGuard)(['devices']), blockDevice);
|
|
346
|
+
router.post('/unblock', (0, scopeGuard_1.scopeGuard)(['devices']), unBlockDevice);
|
|
347
|
+
router.post('/device/tags', (0, scopeGuard_1.scopeGuard)(['devices']), updateTags);
|
|
330
348
|
// session related
|
|
331
349
|
router.get('/queue/length', getQueuedSessionLength);
|
|
332
350
|
router.get('/queue', getQueuedSessionRequests);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.processesRouter = processesRouter;
|
|
4
|
+
const express_1 = require("express");
|
|
5
|
+
const typedi_1 = require("typedi");
|
|
6
|
+
const ProcessRegistry_1 = require("../../services/ProcessRegistry");
|
|
7
|
+
const scopeGuard_1 = require("../../middleware/scopeGuard");
|
|
8
|
+
function processesRouter() {
|
|
9
|
+
const r = (0, express_1.Router)();
|
|
10
|
+
r.get('/', (0, scopeGuard_1.scopeGuard)(['admin']), (_req, res) => {
|
|
11
|
+
const snapshot = typedi_1.Container.get(ProcessRegistry_1.ProcessRegistry)
|
|
12
|
+
.snapshot()
|
|
13
|
+
.map(({ id, sessionId, udid, kind, pid, startedAt }) => ({
|
|
14
|
+
id,
|
|
15
|
+
sessionId,
|
|
16
|
+
udid,
|
|
17
|
+
kind,
|
|
18
|
+
pid,
|
|
19
|
+
uptimeMs: Date.now() - startedAt,
|
|
20
|
+
}));
|
|
21
|
+
res.json(snapshot);
|
|
22
|
+
});
|
|
23
|
+
return r;
|
|
24
|
+
}
|
|
@@ -15,7 +15,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
const express_1 = __importDefault(require("express"));
|
|
16
16
|
const device_service_1 = require("../../data-service/device-service");
|
|
17
17
|
const logger_1 = __importDefault(require("../../logger"));
|
|
18
|
+
const scopeGuard_1 = require("../../middleware/scopeGuard");
|
|
18
19
|
const router = express_1.default.Router();
|
|
20
|
+
// Reserving / releasing / extending a device hold requires devices scope.
|
|
21
|
+
// GET listings remain open to any authenticated key.
|
|
22
|
+
router.use((0, scopeGuard_1.mutationScopeGuard)(['devices']));
|
|
19
23
|
// Duration options in milliseconds
|
|
20
24
|
const DURATION_OPTIONS = {
|
|
21
25
|
'1h': 60 * 60 * 1000,
|
|
@@ -77,6 +81,17 @@ router.post('/', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
77
81
|
error: `Invalid duration. Use: ${Object.keys(DURATION_OPTIONS).join(', ')} or a number in milliseconds`,
|
|
78
82
|
});
|
|
79
83
|
}
|
|
84
|
+
// Bounds check: refuse negative, zero, NaN, Infinity, and anything over 24h.
|
|
85
|
+
const MIN_DURATION_MS = 60 * 1000; // 1 minute
|
|
86
|
+
const MAX_DURATION_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
87
|
+
if (!Number.isFinite(durationMs) ||
|
|
88
|
+
durationMs < MIN_DURATION_MS ||
|
|
89
|
+
durationMs > MAX_DURATION_MS) {
|
|
90
|
+
return res.status(400).json({
|
|
91
|
+
success: false,
|
|
92
|
+
error: `duration must be between ${MIN_DURATION_MS}ms (1 minute) and ${MAX_DURATION_MS}ms (24 hours)`,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
80
95
|
// Check if device exists
|
|
81
96
|
const device = yield (0, device_service_1.getDevice)({ udid: [udid] });
|
|
82
97
|
if (!device) {
|
|
@@ -15,6 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
const NotificationService_1 = require("../../services/NotificationService");
|
|
16
16
|
const typedi_1 = require("typedi");
|
|
17
17
|
const logger_1 = __importDefault(require("../../logger"));
|
|
18
|
+
const scopeGuard_1 = require("../../middleware/scopeGuard");
|
|
18
19
|
function getConfigs(req, res) {
|
|
19
20
|
return __awaiter(this, void 0, void 0, function* () {
|
|
20
21
|
try {
|
|
@@ -74,9 +75,11 @@ function testWebhook(req, res) {
|
|
|
74
75
|
}
|
|
75
76
|
function register(router) {
|
|
76
77
|
router.get('/webhook', getConfigs);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
router.post('/webhook
|
|
78
|
+
// Webhook mutations are admin-only: adding / removing / test-firing global
|
|
79
|
+
// webhooks is a fleet-wide config change.
|
|
80
|
+
router.post('/webhook', (0, scopeGuard_1.scopeGuard)(['admin']), addConfig);
|
|
81
|
+
router.delete('/webhook/:id', (0, scopeGuard_1.scopeGuard)(['admin']), deleteConfig);
|
|
82
|
+
router.post('/webhook/test', (0, scopeGuard_1.scopeGuard)(['admin']), testWebhook);
|
|
80
83
|
}
|
|
81
84
|
exports.default = {
|
|
82
85
|
register,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.validateNodeSecret = validateNodeSecret;
|
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
9
|
+
function timingSafeEqStr(a, b) {
|
|
10
|
+
const ab = Buffer.from(a, 'utf8');
|
|
11
|
+
const bb = Buffer.from(b, 'utf8');
|
|
12
|
+
return ab.length === bb.length && crypto_1.default.timingSafeEqual(ab, bb);
|
|
13
|
+
}
|
|
14
|
+
let lastPreviousWarnAt = 0;
|
|
15
|
+
// Single source of truth for validating an inbound node secret. Returns
|
|
16
|
+
// 'reject' when no expected secret is configured — callers decide whether
|
|
17
|
+
// that should 401 or fall through. Timing-safe compare so the hub can't be
|
|
18
|
+
// probed by measuring response time differences.
|
|
19
|
+
function validateNodeSecret(given, expected) {
|
|
20
|
+
if (!given)
|
|
21
|
+
return 'reject';
|
|
22
|
+
if (expected.current && timingSafeEqStr(given, expected.current))
|
|
23
|
+
return 'current';
|
|
24
|
+
if (expected.previous && timingSafeEqStr(given, expected.previous)) {
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
if (now - lastPreviousWarnAt > 60000) {
|
|
27
|
+
logger_1.default.warn('[nodeSecret] A caller authenticated with XENON_NODE_SECRET_PREVIOUS. Rotate the remaining callers to the new secret and drop PREVIOUS.');
|
|
28
|
+
lastPreviousWarnAt = now;
|
|
29
|
+
}
|
|
30
|
+
return 'previous';
|
|
31
|
+
}
|
|
32
|
+
return 'reject';
|
|
33
|
+
}
|
package/lib/src/config.js
CHANGED
|
@@ -67,6 +67,11 @@ exports.config = {
|
|
|
67
67
|
openaiModel: process.env.XENON_OPENAI_MODEL,
|
|
68
68
|
anthropicModel: process.env.XENON_ANTHROPIC_MODEL,
|
|
69
69
|
ollamaModel: process.env.XENON_OLLAMA_MODEL,
|
|
70
|
+
bootstrapKeyPath: process.env.XENON_BOOTSTRAP_KEY_PATH ||
|
|
71
|
+
path.join(basePath, 'bootstrap-key.txt'),
|
|
72
|
+
authDisabled: process.env.XENON_AUTH_DISABLED === 'true',
|
|
73
|
+
nodeSecret: process.env.XENON_NODE_SECRET,
|
|
74
|
+
nodeSecretPrevious: process.env.XENON_NODE_SECRET_PREVIOUS,
|
|
70
75
|
};
|
|
71
76
|
function updateConfig(newConfig) {
|
|
72
77
|
Object.assign(exports.config, newConfig);
|
|
@@ -54,9 +54,25 @@ class PrismaDeviceStore {
|
|
|
54
54
|
get prisma() {
|
|
55
55
|
return typedi_1.Container.get(prisma_service_1.PrismaService).client;
|
|
56
56
|
}
|
|
57
|
+
safeParse(raw, field, udid) {
|
|
58
|
+
if (!raw)
|
|
59
|
+
return undefined;
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(raw);
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
// Error (not warn) + truncated raw snippet so the corrupt row is
|
|
65
|
+
// findable. Returning undefined keeps getAllDevices alive, but without
|
|
66
|
+
// this log operators would see the field as "not set" and never know
|
|
67
|
+
// the DB actually has malformed data that needs manual repair.
|
|
68
|
+
const snippet = raw.length > 120 ? `${raw.slice(0, 120)}…` : raw;
|
|
69
|
+
logger_1.default.error(`[PrismaDeviceStore] Corrupt JSON in Device.${field} for udid=${udid} (${(err === null || err === void 0 ? void 0 : err.message) || err}). Raw: ${snippet}`);
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
57
73
|
toIDevice(device) {
|
|
58
74
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
59
|
-
return Object.assign(Object.assign({}, device), { cloud: device.cloud
|
|
75
|
+
return Object.assign(Object.assign({}, device), { cloud: this.safeParse(device.cloud, 'cloud', device.udid), capability: this.safeParse(device.capability, 'capability', device.udid), chromeDriverPath: this.safeParse(device.chromeDriverPath, 'chromeDriverPath', device.udid), tags: this.safeParse(device.tags, 'tags', device.udid), platform: (device.platform || 'android'), name: device.name || 'unknown', state: device.state || 'available', sdk: device.sdk || 'unknown', deviceType: device.deviceType || 'real', busy: (_a = device.busy) !== null && _a !== void 0 ? _a : false, userBlocked: (_b = device.userBlocked) !== null && _b !== void 0 ? _b : false, realDevice: (_c = device.realDevice) !== null && _c !== void 0 ? _c : true, lastHealthCheckAt: (_d = device.lastHealthCheckAt) !== null && _d !== void 0 ? _d : undefined, healthStatus: (_e = device.healthStatus) !== null && _e !== void 0 ? _e : 'Healthy', healthCheckError: (_f = device.healthCheckError) !== null && _f !== void 0 ? _f : undefined, batteryLevel: (_g = device.batteryLevel) !== null && _g !== void 0 ? _g : undefined, thermalStatus: (_h = device.thermalStatus) !== null && _h !== void 0 ? _h : undefined, storageFree: (_j = device.storageFree) !== null && _j !== void 0 ? _j : undefined, sessionProgress: (_k = device.sessionProgress) !== null && _k !== void 0 ? _k : '', totalHealedCount: (_l = device.totalHealedCount) !== null && _l !== void 0 ? _l : 0 });
|
|
60
76
|
}
|
|
61
77
|
fromIDevice(device) {
|
|
62
78
|
const data = Object.assign({}, device);
|
|
@@ -525,7 +525,7 @@ let AndroidDeviceManager = class AndroidDeviceManager {
|
|
|
525
525
|
const deviceTracked = Object.assign(Object.assign({}, trackedDevice), { nodeId: this.nodeId });
|
|
526
526
|
if (this.pluginArgs.hub != undefined) {
|
|
527
527
|
logger_1.default.info(`Updating Hub with device ${newDevice.udid}`);
|
|
528
|
-
const nodeDevices = new NodeDevices_1.default(this.pluginArgs.hub, this.pluginArgs.tlsRejectUnauthorized);
|
|
528
|
+
const nodeDevices = new NodeDevices_1.default(this.pluginArgs.hub, this.pluginArgs.tlsRejectUnauthorized, this.pluginArgs.nodeSecret);
|
|
529
529
|
yield nodeDevices.postDevicesToHub([deviceTracked], 'add');
|
|
530
530
|
}
|
|
531
531
|
// node also need a copy of devices, otherwise it cannot serve requests
|
|
@@ -575,7 +575,7 @@ let AndroidDeviceManager = class AndroidDeviceManager {
|
|
|
575
575
|
state: device.type,
|
|
576
576
|
};
|
|
577
577
|
if (pluginArgs.hub != undefined) {
|
|
578
|
-
const nodeDevices = new NodeDevices_1.default(pluginArgs.hub, pluginArgs.tlsRejectUnauthorized);
|
|
578
|
+
const nodeDevices = new NodeDevices_1.default(pluginArgs.hub, pluginArgs.tlsRejectUnauthorized, pluginArgs.nodeSecret);
|
|
579
579
|
yield nodeDevices.postDevicesToHub([clonedDevice], 'remove');
|
|
580
580
|
}
|
|
581
581
|
// node also need a copy of devices, otherwise it cannot serve requests
|
|
@@ -15,9 +15,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
const logger_1 = __importDefault(require("../logger"));
|
|
16
16
|
const InternalHttpClient_1 = require("../InternalHttpClient");
|
|
17
17
|
class NodeDevices {
|
|
18
|
-
constructor(host, tlsRejectUnauthorized) {
|
|
18
|
+
constructor(host, tlsRejectUnauthorized, nodeSecret) {
|
|
19
19
|
this.host = host;
|
|
20
20
|
this.tlsRejectUnauthorized = tlsRejectUnauthorized;
|
|
21
|
+
this.nodeSecret = nodeSecret;
|
|
22
|
+
}
|
|
23
|
+
nodeHeaders() {
|
|
24
|
+
return this.nodeSecret ? { 'x-xenon-node-secret': this.nodeSecret } : {};
|
|
21
25
|
}
|
|
22
26
|
postDevicesToHub(devices, arg) {
|
|
23
27
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -30,6 +34,7 @@ class NodeDevices {
|
|
|
30
34
|
params: {
|
|
31
35
|
type: arg,
|
|
32
36
|
},
|
|
37
|
+
headers: this.nodeHeaders(),
|
|
33
38
|
});
|
|
34
39
|
if (arg === 'add') {
|
|
35
40
|
logger_1.default.info(`Pushed devices to hub ${JSON.stringify(devices)}`);
|
|
@@ -52,6 +57,7 @@ class NodeDevices {
|
|
|
52
57
|
params: {
|
|
53
58
|
type: 'unblock',
|
|
54
59
|
},
|
|
60
|
+
headers: this.nodeHeaders(),
|
|
55
61
|
});
|
|
56
62
|
logger_1.default.info(`Unblocked device with filter: ${JSON.stringify(filter)}`);
|
|
57
63
|
}
|
|
@@ -70,6 +76,7 @@ class NodeDevices {
|
|
|
70
76
|
type: 'unregister',
|
|
71
77
|
host,
|
|
72
78
|
},
|
|
79
|
+
headers: this.nodeHeaders(),
|
|
73
80
|
});
|
|
74
81
|
logger_1.default.info(`Unregistered node ${host} from hub`);
|
|
75
82
|
}
|
|
@@ -202,7 +202,7 @@ let IOSDiscoveryService = class IOSDiscoveryService {
|
|
|
202
202
|
const simulators = yield this.fetchLocalSimulators();
|
|
203
203
|
simulators.sort((a, b) => (a.state > b.state ? 1 : -1));
|
|
204
204
|
if (this.pluginArgs.hub !== undefined) {
|
|
205
|
-
const nodeDevices = new NodeDevices_1.default(this.pluginArgs.hub, this.pluginArgs.tlsRejectUnauthorized);
|
|
205
|
+
const nodeDevices = new NodeDevices_1.default(this.pluginArgs.hub, this.pluginArgs.tlsRejectUnauthorized, this.pluginArgs.nodeSecret);
|
|
206
206
|
yield nodeDevices.postDevicesToHub(simulators, 'add');
|
|
207
207
|
}
|
|
208
208
|
return simulators;
|
|
@@ -238,9 +238,12 @@ let IOSDiscoveryService = class IOSDiscoveryService {
|
|
|
238
238
|
const allowedSimulators = localPluginArgs.simulators;
|
|
239
239
|
simulators = simulators.filter((d) => allowedSimulators.some((s) => d.name === s.name && d.sdk === s.sdk));
|
|
240
240
|
}
|
|
241
|
+
const store = device_store_1.DeviceStoreFactory.getStore();
|
|
241
242
|
return yield Promise.all(simulators.map((d) => __awaiter(this, void 0, void 0, function* () {
|
|
242
243
|
var _a;
|
|
243
|
-
|
|
244
|
+
const storeDevice = yield store.findDevice({ udid: d.udid });
|
|
245
|
+
return Object.assign(Object.assign({}, d), { wdaLocalPort: (storeDevice === null || storeDevice === void 0 ? void 0 : storeDevice.wdaLocalPort) || (yield typedi_1.Container.get(PortAllocator_1.PortAllocator).acquire('wda', d.udid)), mjpegServerPort: (storeDevice === null || storeDevice === void 0 ? void 0 : storeDevice.mjpegServerPort) ||
|
|
246
|
+
(yield typedi_1.Container.get(PortAllocator_1.PortAllocator).acquire('mjpeg', d.udid)), busy: false, realDevice: false, platform: (((_a = d.name) === null || _a === void 0 ? void 0 : _a.toLowerCase().includes('tv')) ? 'tvos' : 'ios'), deviceType: 'simulator', host: `http://${this.pluginArgs.bindHostOrIp}:${this.hostPort}`, totalUtilizationTimeMilliSec: yield (0, device_utils_1.getUtilizationTime)(d.udid), sessionStartTime: 0 });
|
|
244
247
|
})));
|
|
245
248
|
});
|
|
246
249
|
}
|
|
@@ -253,7 +256,7 @@ let IOSDiscoveryService = class IOSDiscoveryService {
|
|
|
253
256
|
try {
|
|
254
257
|
const device = Object.assign(Object.assign({}, (yield this.getDeviceInfo(udid))), { nodeId: this.nodeId });
|
|
255
258
|
if (this.pluginArgs.hub) {
|
|
256
|
-
yield new NodeDevices_1.default(this.pluginArgs.hub, this.pluginArgs.tlsRejectUnauthorized).postDevicesToHub([device], 'add');
|
|
259
|
+
yield new NodeDevices_1.default(this.pluginArgs.hub, this.pluginArgs.tlsRejectUnauthorized, this.pluginArgs.nodeSecret).postDevicesToHub([device], 'add');
|
|
257
260
|
}
|
|
258
261
|
yield (0, device_service_1.addNewDevice)([device], this.pluginArgs.bindHostOrIp);
|
|
259
262
|
}
|
|
@@ -264,7 +267,7 @@ let IOSDiscoveryService = class IOSDiscoveryService {
|
|
|
264
267
|
tracker.on('detached', (udid) => __awaiter(this, void 0, void 0, function* () {
|
|
265
268
|
const deviceRemoved = [{ udid, host: this.pluginArgs.bindHostOrIp }];
|
|
266
269
|
if (this.pluginArgs.hub) {
|
|
267
|
-
yield new NodeDevices_1.default(this.pluginArgs.hub, this.pluginArgs.tlsRejectUnauthorized).postDevicesToHub(deviceRemoved, 'remove');
|
|
270
|
+
yield new NodeDevices_1.default(this.pluginArgs.hub, this.pluginArgs.tlsRejectUnauthorized, this.pluginArgs.nodeSecret).postDevicesToHub(deviceRemoved, 'remove');
|
|
268
271
|
}
|
|
269
272
|
yield (0, device_service_1.removeDevice)(deviceRemoved);
|
|
270
273
|
}));
|
|
@@ -63,6 +63,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
63
63
|
};
|
|
64
64
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
65
65
|
const typedi_1 = require("typedi");
|
|
66
|
+
const ProcessRegistry_1 = require("../../services/ProcessRegistry");
|
|
66
67
|
const child_process_1 = require("child_process");
|
|
67
68
|
const util_1 = require("util");
|
|
68
69
|
const path_1 = __importDefault(require("path"));
|
|
@@ -217,6 +218,7 @@ let IOSStreamService = class IOSStreamService {
|
|
|
217
218
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
218
219
|
env: Object.assign(Object.assign({}, process.env), { ENABLE_GO_IOS_AGENT: 'yes' }),
|
|
219
220
|
});
|
|
221
|
+
typedi_2.Container.get(ProcessRegistry_1.ProcessRegistry).track({ kind: 'other', udid, process: tunnelProcess });
|
|
220
222
|
(_a = tunnelProcess.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (data) => logger_1.default.debug(`Tunnel [${udid}]: ${data}`));
|
|
221
223
|
(_b = tunnelProcess.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (data) => logger_1.default.debug(`Tunnel Err [${udid}]: ${data}`));
|
|
222
224
|
// Wait for tunnel to establish by checking the go-ios agent port
|
|
@@ -434,7 +436,9 @@ let IOSStreamService = class IOSStreamService {
|
|
|
434
436
|
const wdaIproxy = isolationService.wrapSpawn('iproxy', ['-u', udid, `${session.wdaPort}:8100`], 'Performance');
|
|
435
437
|
const mjpegIproxy = isolationService.wrapSpawn('iproxy', ['-u', udid, `${session.mjpegPort}:9100`], 'Performance');
|
|
436
438
|
session.forwardWDAProcess = (0, child_process_1.spawn)(wdaIproxy.command, wdaIproxy.args);
|
|
439
|
+
typedi_2.Container.get(ProcessRegistry_1.ProcessRegistry).track({ kind: 'other', udid, process: session.forwardWDAProcess });
|
|
437
440
|
session.forwardMJPEGProcess = (0, child_process_1.spawn)(mjpegIproxy.command, mjpegIproxy.args);
|
|
441
|
+
typedi_2.Container.get(ProcessRegistry_1.ProcessRegistry).track({ kind: 'ios-mjpeg', udid, process: session.forwardMJPEGProcess });
|
|
438
442
|
logger_1.default.info(`🛡️ [${udid}] [Watchdog] Tunnels restarted successfully.`);
|
|
439
443
|
});
|
|
440
444
|
}
|
|
@@ -558,7 +562,9 @@ let IOSStreamService = class IOSStreamService {
|
|
|
558
562
|
const wdaIproxy = isolationService.wrapSpawn('iproxy', ['-u', udid, `${wdaPort}:8100`], 'Performance');
|
|
559
563
|
const mjpegIproxy = isolationService.wrapSpawn('iproxy', ['-u', udid, `${mjpegPort}:9100`], 'Performance');
|
|
560
564
|
session.forwardWDAProcess = (0, child_process_1.spawn)(wdaIproxy.command, wdaIproxy.args);
|
|
565
|
+
typedi_2.Container.get(ProcessRegistry_1.ProcessRegistry).track({ kind: 'other', udid, process: session.forwardWDAProcess });
|
|
561
566
|
session.forwardMJPEGProcess = (0, child_process_1.spawn)(mjpegIproxy.command, mjpegIproxy.args);
|
|
567
|
+
typedi_2.Container.get(ProcessRegistry_1.ProcessRegistry).track({ kind: 'ios-mjpeg', udid, process: session.forwardMJPEGProcess });
|
|
562
568
|
const handleIproxyProcess = (p, name) => {
|
|
563
569
|
p.on('error', (err) => logger_1.default.error(`${name} [${udid}] error: ${err.message}`));
|
|
564
570
|
p.on('exit', (code) => {
|
|
@@ -586,6 +592,7 @@ let IOSStreamService = class IOSStreamService {
|
|
|
586
592
|
session.wdaProcess = (0, child_process_1.spawn)(wdaSpawn.command, wdaSpawn.args, {
|
|
587
593
|
env: Object.assign(Object.assign({}, process.env), { ENABLE_GO_IOS_AGENT: 'yes' }),
|
|
588
594
|
});
|
|
595
|
+
typedi_2.Container.get(ProcessRegistry_1.ProcessRegistry).track({ kind: 'wda', udid, process: session.wdaProcess });
|
|
589
596
|
const logDir = path_1.default.join(os_1.default.tmpdir(), 'xenon-logs');
|
|
590
597
|
if (!fs_extra_1.default.existsSync(logDir))
|
|
591
598
|
fs_extra_1.default.mkdirSync(logDir);
|
|
@@ -24,6 +24,7 @@ const axios_1 = __importDefault(require("axios"));
|
|
|
24
24
|
const semver_1 = __importDefault(require("semver"));
|
|
25
25
|
const logger_1 = __importDefault(require("../../logger"));
|
|
26
26
|
const typedi_1 = require("typedi");
|
|
27
|
+
const ProcessRegistry_1 = require("../../services/ProcessRegistry");
|
|
27
28
|
const IOSStreamService_1 = __importDefault(require("./IOSStreamService"));
|
|
28
29
|
const device_store_1 = require("../../data-service/device-store");
|
|
29
30
|
const child_process_1 = require("child_process");
|
|
@@ -765,6 +766,7 @@ let WDAClient = WDAClient_1 = class WDAClient {
|
|
|
765
766
|
env: Object.assign(Object.assign({}, process.env), { ENABLE_GO_IOS_AGENT: 'yes' }),
|
|
766
767
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
767
768
|
});
|
|
769
|
+
typedi_1.Container.get(ProcessRegistry_1.ProcessRegistry).track({ kind: 'log-tailer', udid, process: proc });
|
|
768
770
|
const entry = {
|
|
769
771
|
proc,
|
|
770
772
|
buffer: [],
|
package/lib/src/device-utils.js
CHANGED
|
@@ -71,6 +71,7 @@ exports.cleanPendingSessions = cleanPendingSessions;
|
|
|
71
71
|
exports.setupCronCleanPendingSessions = setupCronCleanPendingSessions;
|
|
72
72
|
exports.setupCronCleanExpiredReservations = setupCronCleanExpiredReservations;
|
|
73
73
|
exports.setupCronCleanupBuilds = setupCronCleanupBuilds;
|
|
74
|
+
exports.setupCronReconcileDevices = setupCronReconcileDevices;
|
|
74
75
|
exports.setupCronSweepOrphanSessions = setupCronSweepOrphanSessions;
|
|
75
76
|
exports.stopAllTimers = stopAllTimers;
|
|
76
77
|
/* eslint-disable no-prototype-builtins */
|
|
@@ -408,7 +409,7 @@ function getBusyDevicesCount() {
|
|
|
408
409
|
}).length;
|
|
409
410
|
});
|
|
410
411
|
}
|
|
411
|
-
function updateDeviceList(host, hubArgument, tlsRejectUnauthorized) {
|
|
412
|
+
function updateDeviceList(host, hubArgument, tlsRejectUnauthorized, nodeSecret) {
|
|
412
413
|
return __awaiter(this, void 0, void 0, function* () {
|
|
413
414
|
const allExistingDevices = yield (0, device_service_1.getAllDevices)();
|
|
414
415
|
const devices = yield getDeviceManager().getDevices(allExistingDevices);
|
|
@@ -427,7 +428,7 @@ function updateDeviceList(host, hubArgument, tlsRejectUnauthorized) {
|
|
|
427
428
|
}
|
|
428
429
|
if (hubArgument) {
|
|
429
430
|
if (yield (0, helpers_1.isXenonRunning)(hubArgument, tlsRejectUnauthorized)) {
|
|
430
|
-
const nodeDevices = new NodeDevices_1.default(hubArgument, tlsRejectUnauthorized);
|
|
431
|
+
const nodeDevices = new NodeDevices_1.default(hubArgument, tlsRejectUnauthorized, nodeSecret);
|
|
431
432
|
try {
|
|
432
433
|
yield nodeDevices.postDevicesToHub(devices, 'add');
|
|
433
434
|
if (staleLocalDevices.length > 0) {
|
|
@@ -566,14 +567,14 @@ function setupCronReleaseBlockedDevices(intervalMs, newCommandTimeoutSec) {
|
|
|
566
567
|
}), intervalMs);
|
|
567
568
|
});
|
|
568
569
|
}
|
|
569
|
-
function setupCronUpdateDeviceList(host, hubArgument, intervalMs, tlsRejectUnauthorized) {
|
|
570
|
+
function setupCronUpdateDeviceList(host, hubArgument, intervalMs, tlsRejectUnauthorized, nodeSecret) {
|
|
570
571
|
return __awaiter(this, void 0, void 0, function* () {
|
|
571
572
|
if (cronTimerToUpdateDevices) {
|
|
572
573
|
clearInterval(cronTimerToUpdateDevices);
|
|
573
574
|
}
|
|
574
575
|
logger_1.default.info(`This node will send device list update to the hub (${hubArgument}) every ${intervalMs} ms`);
|
|
575
576
|
cronTimerToUpdateDevices = setInterval(() => __awaiter(this, void 0, void 0, function* () {
|
|
576
|
-
yield updateDeviceList(host, hubArgument, tlsRejectUnauthorized);
|
|
577
|
+
yield updateDeviceList(host, hubArgument, tlsRejectUnauthorized, nodeSecret);
|
|
577
578
|
}), intervalMs);
|
|
578
579
|
});
|
|
579
580
|
}
|
|
@@ -658,6 +659,28 @@ function setupCronCleanupBuilds(pluginArgs) {
|
|
|
658
659
|
});
|
|
659
660
|
}
|
|
660
661
|
let cronTimerSweepOrphanSessions;
|
|
662
|
+
let cronTimerReconcileDevices;
|
|
663
|
+
/**
|
|
664
|
+
* Cross-checks the device store against SESSION_MANAGER and frees devices
|
|
665
|
+
* that are busy with a session ID the driver layer no longer knows about.
|
|
666
|
+
* Fills the gap between OrphanSweeper (Prisma heartbeat-driven) and
|
|
667
|
+
* releaseBlockedDevices (command-idle-driven).
|
|
668
|
+
*/
|
|
669
|
+
function setupCronReconcileDevices(intervalMs) {
|
|
670
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
671
|
+
const { DeviceReconciler } = require('./services/DeviceReconciler');
|
|
672
|
+
const reconciler = typedi_1.Container.get(DeviceReconciler);
|
|
673
|
+
logger_1.default.info(`Device reconcile scheduled every ${intervalMs}ms`);
|
|
674
|
+
if (cronTimerReconcileDevices) {
|
|
675
|
+
clearInterval(cronTimerReconcileDevices);
|
|
676
|
+
}
|
|
677
|
+
cronTimerReconcileDevices = setInterval(() => {
|
|
678
|
+
reconciler.reconcile().catch((err) => {
|
|
679
|
+
logger_1.default.error(`Device reconcile crashed: ${err.message}`);
|
|
680
|
+
});
|
|
681
|
+
}, intervalMs);
|
|
682
|
+
return cronTimerReconcileDevices;
|
|
683
|
+
}
|
|
661
684
|
/**
|
|
662
685
|
* Periodically sweep sessions whose heartbeat has gone stale, marking them as
|
|
663
686
|
* failed and releasing their devices.
|
|
@@ -698,4 +721,6 @@ function stopAllTimers() {
|
|
|
698
721
|
cronTimerToCleanupBuilds.cancel();
|
|
699
722
|
if (cronTimerSweepOrphanSessions)
|
|
700
723
|
clearInterval(cronTimerSweepOrphanSessions);
|
|
724
|
+
if (cronTimerReconcileDevices)
|
|
725
|
+
clearInterval(cronTimerReconcileDevices);
|
|
701
726
|
}
|