@xenon-device-management/xenon 1.1.17 → 1.1.19

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xenon-device-management/xenon",
3
- "version": "1.1.17",
3
+ "version": "1.1.19",
4
4
  "description": "Xenon - Intelligent Mobile Infrastructure. A self-healing device orchestration platform for Appium.",
5
5
  "main": "./lib/src/index.js",
6
6
  "exports": {
package/lib/schema.json CHANGED
@@ -119,6 +119,10 @@
119
119
  "type": "boolean",
120
120
  "default": false
121
121
  },
122
+ "bootedEmulators": {
123
+ "type": "boolean",
124
+ "default": false
125
+ },
122
126
  "removeDevicesFromDatabaseBeforeRunningThePlugin": {
123
127
  "type": "boolean",
124
128
  "default": false
@@ -140,6 +140,7 @@ const findPublicPath = () => {
140
140
  path_1.default.resolve(__dirname, '../public'), // Alternative structure
141
141
  path_1.default.resolve(__dirname, '../../public'),
142
142
  path_1.default.resolve(__dirname, '../../../public'),
143
+ path_1.default.resolve(__dirname, '../../../../public'),
143
144
  ];
144
145
  for (const p of searchPaths) {
145
146
  if (fs_1.default.existsSync(path_1.default.join(p, 'index.html'))) {
@@ -153,10 +154,18 @@ const findPublicPath = () => {
153
154
  return fallback;
154
155
  };
155
156
  const publicPath = findPublicPath();
156
- logger_1.default.info(`[Xenon] Public assets path resolved to: ${publicPath}`);
157
- staticFilesRouter.use(express_1.default.static(publicPath));
157
+ logger_1.default.info(`[Xenon] Dashboard assets path: ${publicPath}`);
158
+ logger_1.default.info(`[Xenon] Dashboard available at: /xenon/ (e.g. http://localhost:4723/xenon/)`);
159
+ // Principal Security: Add permissive CSP and CORS for the dashboard
160
+ router.use((req, res, next) => {
161
+ res.setHeader('Content-Security-Policy', "default-src * 'unsafe-inline' 'unsafe-eval' data: blob:; frame-ancestors 'self';");
162
+ res.setHeader('Access-Control-Allow-Origin', '*');
163
+ next();
164
+ });
165
+ staticFilesRouter.use(express_1.default.static(publicPath, { index: false }));
158
166
  router.use('/api', apiRouter);
159
- router.use('/assets', express_1.default.static(config_1.config.sessionAssetsPath));
167
+ // Principal Fix: Rename collision route from /assets to /session-recordings to avoid conflict with dashboard's /assets folder
168
+ router.use('/session-recordings', express_1.default.static(config_1.config.sessionAssetsPath));
160
169
  router.use(staticFilesRouter);
161
170
  function createRouter(pluginArgs) {
162
171
  dashboard_1.default.register(apiRouter);
@@ -184,19 +193,24 @@ function createRouter(pluginArgs) {
184
193
  });
185
194
  // Fallback route for client-side routing - serve index.html for all non-API routes
186
195
  // MUST be registered after Swagger to avoid interception
187
- router.get(/^(?!\/api).*/, (req, res) => {
196
+ router.get('*', (req, res) => {
188
197
  const indexPath = path_1.default.resolve(publicPath, 'index.html');
189
198
  const url = req.originalUrl || req.url;
199
+ // Skip if it's an API call that somehow reached here
200
+ if (url.includes('/api/'))
201
+ return res.status(404).json({ error: true, message: 'Not Found' });
190
202
  logger_1.default.debug(`[Xenon] UI Fallback triggered for: ${url}. Targeting: ${indexPath}`);
191
203
  if (fs_1.default.existsSync(indexPath)) {
192
- res.sendFile(indexPath, (err) => {
193
- if (err) {
194
- logger_1.default.error(`[Xenon] res.sendFile failed for ${indexPath}. Error: ${err.message}`);
195
- if (!res.headersSent) {
196
- res.status(404).send(`Xenon UI Asset Error: ${err.message}`);
197
- }
198
- }
199
- });
204
+ try {
205
+ const html = fs_1.default.readFileSync(indexPath, 'utf-8');
206
+ res.set('Content-Type', 'text/html');
207
+ res.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
208
+ return res.send(html);
209
+ }
210
+ catch (err) {
211
+ logger_1.default.error(`[Xenon] UI Fallback read error for ${indexPath}: ${err.message}`);
212
+ return res.status(500).send(`Xenon UI Asset Error: ${err.message}`);
213
+ }
200
214
  }
201
215
  else {
202
216
  logger_1.default.error(`[Xenon] UI Fallback failed: index.html not found at ${indexPath}`);
@@ -208,7 +222,7 @@ function createRouter(pluginArgs) {
208
222
  catch (e) {
209
223
  logger_1.default.error(`[Xenon] Could not even read directory ${publicPath}: ${e.message}`);
210
224
  }
211
- res.status(404).send('Xenon UI assets not found. Check installation.');
225
+ return res.status(404).send('Xenon UI assets not found. Check installation.');
212
226
  }
213
227
  });
214
228
  return router;
@@ -148,12 +148,24 @@ let AndroidDeviceManager = class AndroidDeviceManager {
148
148
  });
149
149
  }
150
150
  else if (deviceTypes.androidDeviceType === 'simulated') {
151
- return devices.filter((device) => {
151
+ const simulated = devices.filter((device) => {
152
152
  return device.deviceType === 'emulator';
153
153
  });
154
+ if (this.pluginArgs.bootedEmulators) {
155
+ return simulated.filter((device) => device.state === 'device');
156
+ }
157
+ return simulated;
154
158
  // return both real and simulated (emulated) devices
155
159
  }
156
160
  else {
161
+ if (this.pluginArgs.bootedEmulators) {
162
+ return devices.filter((device) => {
163
+ if (device.deviceType === 'emulator') {
164
+ return device.state === 'device';
165
+ }
166
+ return true;
167
+ });
168
+ }
157
169
  return devices;
158
170
  }
159
171
  }
@@ -66,6 +66,7 @@ exports.unblockCandidateDevices = unblockCandidateDevices;
66
66
  exports.releaseBlockedDevices = releaseBlockedDevices;
67
67
  exports.setupCronReleaseBlockedDevices = setupCronReleaseBlockedDevices;
68
68
  exports.setupCronUpdateDeviceList = setupCronUpdateDeviceList;
69
+ exports.setupCronLocalDiscovery = setupCronLocalDiscovery;
69
70
  exports.cleanPendingSessions = cleanPendingSessions;
70
71
  exports.setupCronCleanPendingSessions = setupCronCleanPendingSessions;
71
72
  exports.setupCronCleanExpiredReservations = setupCronCleanExpiredReservations;
@@ -578,6 +579,21 @@ function setupCronUpdateDeviceList(host, hubArgument, intervalMs, tlsRejectUnaut
578
579
  }), intervalMs);
579
580
  });
580
581
  }
582
+ /**
583
+ * Principal discovery: Periodically poll for local devices to prune stales/offlines.
584
+ * Critical for standalone Hubs where setupCronUpdateDeviceList isn't called.
585
+ */
586
+ function setupCronLocalDiscovery(host, intervalMs) {
587
+ return __awaiter(this, void 0, void 0, function* () {
588
+ if (timer) {
589
+ clearInterval(timer);
590
+ }
591
+ logger_1.default.info(`Local device discovery poll started every ${intervalMs} ms`);
592
+ timer = setInterval(() => __awaiter(this, void 0, void 0, function* () {
593
+ yield updateDeviceList(host);
594
+ }), intervalMs);
595
+ });
596
+ }
581
597
  function cleanPendingSessions(timeoutMs) {
582
598
  return __awaiter(this, void 0, void 0, function* () {
583
599
  const pendingSessions = yield pendingStore.getAllPendingSessions();
@@ -34,6 +34,7 @@ exports.DefaultPluginArgs = {
34
34
  bindHostOrIp: ip_1.default.address(),
35
35
  enableDashboard: false,
36
36
  bootedSimulators: false,
37
+ bootedEmulators: false,
37
38
  healthCheckIntervalMs: 86400000,
38
39
  healthCheckSchedule: undefined,
39
40
  removeDevicesFromDatabaseBeforeRunningThePlugin: false,
@@ -246,6 +246,11 @@ let ServerManager = ServerManager_1 = class ServerManager {
246
246
  else {
247
247
  ServerManager_1.IS_HUB = true;
248
248
  this.logger.info(`🌐 I'm a hub and I'm listening on ${pluginArgs.bindHostOrIp}:${cliArgs.port}`);
249
+ // Principal discovery: Background poll to prune stale/offline devices on the Hub itself
250
+ (() => __awaiter(this, void 0, void 0, function* () {
251
+ const { setupCronLocalDiscovery } = yield Promise.resolve().then(() => __importStar(require('../device-utils')));
252
+ yield setupCronLocalDiscovery(pluginArgs.bindHostOrIp, pluginArgs.sendNodeDevicesToHubIntervalMs);
253
+ }))();
249
254
  const socketServer = typedi_1.Container.get(SocketServer_1.SocketServer);
250
255
  socketServer.initialize(httpServer);
251
256
  const tracingService = typedi_1.Container.get(TracingService_1.TracingService);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xenon-device-management/xenon",
3
- "version": "1.1.17",
3
+ "version": "1.1.19",
4
4
  "description": "Xenon - Intelligent Mobile Infrastructure. A self-healing device orchestration platform for Appium.",
5
5
  "main": "./lib/src/index.js",
6
6
  "exports": {
package/schema.json CHANGED
@@ -119,6 +119,10 @@
119
119
  "type": "boolean",
120
120
  "default": false
121
121
  },
122
+ "bootedEmulators": {
123
+ "type": "boolean",
124
+ "default": false
125
+ },
122
126
  "removeDevicesFromDatabaseBeforeRunningThePlugin": {
123
127
  "type": "boolean",
124
128
  "default": false