@xenon-device-management/xenon 1.1.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 +446 -0
- package/lib/package.json +207 -0
- package/lib/public/assets/Layouts-7IT8aFLI.js +11 -0
- package/lib/public/assets/Layouts-DPMls9vh.css +1 -0
- package/lib/public/assets/ai-settings-BbnfgdEx.js +11 -0
- package/lib/public/assets/apps-CRMrI4_p.js +16 -0
- package/lib/public/assets/apps-CcM77dgg.css +1 -0
- package/lib/public/assets/badge-B1nKs8zj.css +1 -0
- package/lib/public/assets/badge-CSvl5xIU.js +11 -0
- package/lib/public/assets/button-CJlKn4PZ.css +1 -0
- package/lib/public/assets/button-CvLaGFYj.js +26 -0
- package/lib/public/assets/calendar-6w-D6Oaw.js +6 -0
- package/lib/public/assets/clock-DcdeWBPr.js +6 -0
- package/lib/public/assets/cpu-DiSoXT9n.js +6 -0
- package/lib/public/assets/device-explorer-CajM63OJ.js +193 -0
- package/lib/public/assets/device-explorer-CxdUAoTL.css +1 -0
- package/lib/public/assets/index-ByQwMN5T.js +174 -0
- package/lib/public/assets/index-C1DBaoSh.js +1 -0
- package/lib/public/assets/index-qzCez_kk.css +1 -0
- package/lib/public/assets/lock-B23ibZmo.js +6 -0
- package/lib/public/assets/maintenance-settings-CirzA6yG.js +6 -0
- package/lib/public/assets/mouse-pointer-2-Cz76SHFb.js +6 -0
- package/lib/public/assets/plus-BBwlIevt.js +6 -0
- package/lib/public/assets/session-dashboard-C2k7FFv_.css +1 -0
- package/lib/public/assets/session-dashboard-HPDtwPOZ.js +62 -0
- package/lib/public/assets/settings-DrZsZwdc.js +1 -0
- package/lib/public/assets/trash-2-DQpvzJec.js +6 -0
- package/lib/public/assets/useSocket-Dxsqae2a.js +16 -0
- package/lib/public/assets/webhook-settings-CDPgsgkb.css +1 -0
- package/lib/public/assets/webhook-settings-Cp-B4Nrw.js +1 -0
- package/lib/public/assets/zap-DovP6iow.js +6 -0
- package/lib/public/favicon.ico +0 -0
- package/lib/public/favicon.png +0 -0
- package/lib/public/favicon.svg +9 -0
- package/lib/public/index.html +46 -0
- package/lib/public/logo.svg +17 -0
- package/lib/public/logo192.png +0 -0
- package/lib/public/logo512.png +0 -0
- package/lib/public/manifest.json +25 -0
- package/lib/public/robots.txt +3 -0
- package/lib/schema.json +348 -0
- package/lib/src/InternalHttpClient.js +212 -0
- package/lib/src/PluginContext.js +29 -0
- package/lib/src/XenonCapabilityManager.js +199 -0
- package/lib/src/app/index.js +167 -0
- package/lib/src/app/routers/apps.js +79 -0
- package/lib/src/app/routers/config.js +131 -0
- package/lib/src/app/routers/control.js +835 -0
- package/lib/src/app/routers/dashboard.js +301 -0
- package/lib/src/app/routers/grid.js +352 -0
- package/lib/src/app/routers/reservation.js +190 -0
- package/lib/src/app/routers/webhook.js +83 -0
- package/lib/src/app/swagger-docs.js +203 -0
- package/lib/src/app/swagger.js +366 -0
- package/lib/src/chromeUtils.js +148 -0
- package/lib/src/commands/handle.js +19 -0
- package/lib/src/commands/index.js +8 -0
- package/lib/src/config.js +73 -0
- package/lib/src/dashboard/asset-manager.js +84 -0
- package/lib/src/dashboard/commands.js +284 -0
- package/lib/src/dashboard/event-manager.js +699 -0
- package/lib/src/dashboard/services/app-service.js +134 -0
- package/lib/src/dashboard/services/failure-analysis-service.js +173 -0
- package/lib/src/dashboard/services/session-service.js +113 -0
- package/lib/src/data-service/CircuitBreaker.js +83 -0
- package/lib/src/data-service/config-service.js +155 -0
- package/lib/src/data-service/db.js +122 -0
- package/lib/src/data-service/device-service.js +320 -0
- package/lib/src/data-service/device-store.interface.js +2 -0
- package/lib/src/data-service/device-store.js +345 -0
- package/lib/src/data-service/pending-sessions-service.js +25 -0
- package/lib/src/data-service/pluginArgs.js +25 -0
- package/lib/src/data-service/prisma-service.js +31 -0
- package/lib/src/data-service/prisma-store.js +385 -0
- package/lib/src/data-service/queue-service.js +150 -0
- package/lib/src/data-service/web-config-service.js +130 -0
- package/lib/src/device-managers/AndroidDeviceManager.js +1155 -0
- package/lib/src/device-managers/ChromeDriverManager.js +68 -0
- package/lib/src/device-managers/HealthMonitorService.js +325 -0
- package/lib/src/device-managers/IOSDeviceManager.js +351 -0
- package/lib/src/device-managers/NodeDevices.js +82 -0
- package/lib/src/device-managers/android/AndroidStreamService.js +370 -0
- package/lib/src/device-managers/android/DeviceLockManager.js +45 -0
- package/lib/src/device-managers/cloud/CapabilityManager.js +26 -0
- package/lib/src/device-managers/cloud/Devices.js +86 -0
- package/lib/src/device-managers/iOSTracker.js +44 -0
- package/lib/src/device-managers/index.js +89 -0
- package/lib/src/device-managers/ios/IOSDiscoveryService.js +268 -0
- package/lib/src/device-managers/ios/IOSStreamService.js +893 -0
- package/lib/src/device-managers/ios/WDAClient.js +866 -0
- package/lib/src/device-utils.js +663 -0
- package/lib/src/enums/Capabilities.js +8 -0
- package/lib/src/enums/Cloud.js +11 -0
- package/lib/src/enums/Platform.js +9 -0
- package/lib/src/enums/SessionType.js +9 -0
- package/lib/src/enums/SocketEvents.js +15 -0
- package/lib/src/helpers/UniversalMjpegProxy.js +273 -0
- package/lib/src/helpers/index.js +229 -0
- package/lib/src/index.js +95 -0
- package/lib/src/interceptors/CommandInterceptor.js +524 -0
- package/lib/src/interfaces/ICloudManager.js +2 -0
- package/lib/src/interfaces/IDevice.js +2 -0
- package/lib/src/interfaces/IDeviceFilterOptions.js +2 -0
- package/lib/src/interfaces/IDeviceManager.js +2 -0
- package/lib/src/interfaces/IOptions.js +2 -0
- package/lib/src/interfaces/IPluginArgs.js +55 -0
- package/lib/src/interfaces/ISessionCapability.js +2 -0
- package/lib/src/logger.js +225 -0
- package/lib/src/plugin.js +244 -0
- package/lib/src/prisma.js +12 -0
- package/lib/src/profiling/AndroidAppProfiler.js +213 -0
- package/lib/src/proxy/wd-command-proxy.js +221 -0
- package/lib/src/scripts/generate-database-migration.js +59 -0
- package/lib/src/scripts/initialize-database.js +55 -0
- package/lib/src/scripts/install-go-ios.js +66 -0
- package/lib/src/scripts/prepare-prisma.js +89 -0
- package/lib/src/services/AICommandService.js +143 -0
- package/lib/src/services/AIService.js +466 -0
- package/lib/src/services/CleanupService.js +141 -0
- package/lib/src/services/EventBus.js +74 -0
- package/lib/src/services/InspectorService.js +395 -0
- package/lib/src/services/MetricsService.js +134 -0
- package/lib/src/services/NetworkConditioningService.js +173 -0
- package/lib/src/services/NotificationService.js +163 -0
- package/lib/src/services/RequestLogService.js +252 -0
- package/lib/src/services/ResourceIsolationService.js +122 -0
- package/lib/src/services/SecurityService.js +120 -0
- package/lib/src/services/ServerManager.js +284 -0
- package/lib/src/services/SessionHeartbeatService.js +158 -0
- package/lib/src/services/SessionLifecycleService.js +572 -0
- package/lib/src/services/SocketClient.js +71 -0
- package/lib/src/services/SocketServer.js +87 -0
- package/lib/src/services/TracingService.js +132 -0
- package/lib/src/services/VideoPipelineService.js +220 -0
- package/lib/src/services/healing/FuzzyXmlHealingProvider.js +333 -0
- package/lib/src/services/healing/HealEtalonService.js +98 -0
- package/lib/src/services/healing/HealedLocatorGenerator.js +132 -0
- package/lib/src/services/healing/HealingOrchestrator.js +165 -0
- package/lib/src/services/healing/LlmHealingProvider.js +77 -0
- package/lib/src/services/healing/OcrHealingProvider.js +119 -0
- package/lib/src/services/healing/ResilioTreeHealingProvider.js +100 -0
- package/lib/src/services/healing/VisualAiHealingProvider.js +90 -0
- package/lib/src/services/healing/types.js +12 -0
- package/lib/src/services/omni-vision/OmniVisionService.js +718 -0
- package/lib/src/services/omni-vision/VisionAssertionService.js +68 -0
- package/lib/src/sessions/CloudSession.js +42 -0
- package/lib/src/sessions/LocalSession.js +313 -0
- package/lib/src/sessions/RemoteSession.js +287 -0
- package/lib/src/sessions/SessionManager.js +238 -0
- package/lib/src/sessions/XenonSession.js +44 -0
- package/lib/src/types/CLIArgs.js +2 -0
- package/lib/src/types/CloudArgs.js +2 -0
- package/lib/src/types/CloudSchema.js +131 -0
- package/lib/src/types/DeviceType.js +2 -0
- package/lib/src/types/DeviceUpdate.js +2 -0
- package/lib/src/types/IOSDevice.js +2 -0
- package/lib/src/types/Platform.js +2 -0
- package/lib/src/types/SessionStatus.js +11 -0
- package/lib/src/validators/CapabilityValidator.js +93 -0
- package/lib/test/e2e/android/conf.spec.js +43 -0
- package/lib/test/e2e/android/conf2.spec.js +44 -0
- package/lib/test/e2e/android/conf3.spec.js +44 -0
- package/lib/test/e2e/e2ehelper.js +113 -0
- package/lib/test/e2e/hubnode/forward-request.spec.js +224 -0
- package/lib/test/e2e/hubnode/hubnode.spec.js +214 -0
- package/lib/test/e2e/ios/conf1.spec.js +39 -0
- package/lib/test/e2e/ios/conf2.spec.js +39 -0
- package/lib/test/e2e/plugin-harness.js +236 -0
- package/lib/test/e2e/plugin.spec.js +97 -0
- package/lib/test/e2e/telemetry_verification.spec.js +83 -0
- package/lib/test/e2e/video-recording-test.spec.js +63 -0
- package/lib/test/helpers/test-container.js +112 -0
- package/lib/test/integration/androidDevices.spec.js +137 -0
- package/lib/test/integration/cliArgs.js +73 -0
- package/lib/test/integration/ios/01iOSSimulator.spec.js +291 -0
- package/lib/test/integration/ios/02iOSDevices.spec.js +75 -0
- package/lib/test/integration/testHelpers.js +74 -0
- package/lib/test/unit/AndroidDeviceManager.spec.js +178 -0
- package/lib/test/unit/ChromeDriverManager.spec.js +26 -0
- package/lib/test/unit/CleanupService.spec.js +21 -0
- package/lib/test/unit/DeviceModel.spec.js +157 -0
- package/lib/test/unit/FuzzyXmlHealingProvider.test.js +294 -0
- package/lib/test/unit/GetAdbOriginal.js +42 -0
- package/lib/test/unit/HealingCascade.test.js +128 -0
- package/lib/test/unit/IOSDeviceManager.spec.js +261 -0
- package/lib/test/unit/RemoteIOs.spec.js +78 -0
- package/lib/test/unit/ResilioTreeHealingProvider.test.js +96 -0
- package/lib/test/unit/commands.spec.js +27 -0
- package/lib/test/unit/config.spec.js +27 -0
- package/lib/test/unit/device-service.spec.js +307 -0
- package/lib/test/unit/device-utils.spec.js +313 -0
- package/lib/test/unit/fixtures/device.config.js +4 -0
- package/lib/test/unit/fixtures/devices.js +89 -0
- package/lib/test/unit/helpers.spec.js +62 -0
- package/lib/test/unit/omni-vision.spec.js +100 -0
- package/lib/test/unit/plugin.spec.js +133 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/package.json +207 -0
- package/prisma/data.db +0 -0
- package/prisma/dev.db +0 -0
- package/prisma/dev.db-journal +0 -0
- package/prisma/migrations/20231011074725_initial_tables/migration.sql +47 -0
- package/prisma/migrations/20231226115334_update_session_log/migration.sql +2 -0
- package/prisma/migrations/20251204113710_add_video_recording_enabled/migration.sql +29 -0
- package/prisma/migrations/20251204132449_add_log_table/migration.sql +11 -0
- package/prisma/migrations/20251205050111_add_profiling_support/migration.sql +47 -0
- package/prisma/migrations/20251205050947_add_is_error_field/migration.sql +24 -0
- package/prisma/migrations/20260126201337_add_app_model/migration.sql +18 -0
- package/prisma/migrations/20260130115722_add_performance_trace_and_xenon_sync/migration.sql +2 -0
- package/prisma/migrations/20260130135114_add_device_models/migration.sql +57 -0
- package/prisma/migrations/20260130140655_make_systemport_optional/migration.sql +45 -0
- package/prisma/migrations/20260130140932_make_device_fields_optional/migration.sql +45 -0
- package/prisma/migrations/20260130141040_final_schema_fix/migration.sql +45 -0
- package/prisma/migrations/20260130143234_add_device_health_fields/migration.sql +4 -0
- package/prisma/migrations/20260130144921_add_failure_category/migration.sql +2 -0
- package/prisma/migrations/20260131151456_add_webhook_config/migration.sql +10 -0
- package/prisma/migrations/20260201094507_add_device_tags/migration.sql +11 -0
- package/prisma/migrations/20260201103410_add_managed_process/migration.sql +15 -0
- package/prisma/migrations/20260201140637_add_web_config/migration.sql +22 -0
- package/prisma/migrations/20260201162232_add_session_progress/migration.sql +2 -0
- package/prisma/migrations/20260201174231_add_total_healed_count/migration.sql +2 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +210 -0
- package/schema.json +348 -0
- package/scripts/build-xenon.sh +32 -0
- package/scripts/dev/debug-gemini.ts +44 -0
- package/scripts/generate-types-from-schema.js +86 -0
- package/scripts/install-compatible-driver.js +39 -0
|
@@ -0,0 +1,835 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
36
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
37
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
38
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
39
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
40
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
41
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
45
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
|
+
};
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
const express_1 = require("express");
|
|
49
|
+
const device_store_1 = require("../../data-service/device-store");
|
|
50
|
+
const device_managers_1 = require("../../device-managers");
|
|
51
|
+
const typedi_1 = require("typedi");
|
|
52
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
53
|
+
const InternalHttpClient_1 = require("../../InternalHttpClient");
|
|
54
|
+
const device_service_1 = require("../../data-service/device-service");
|
|
55
|
+
const UniversalMjpegProxy_1 = require("../../helpers/UniversalMjpegProxy");
|
|
56
|
+
const IOSStreamService_1 = __importDefault(require("../../device-managers/ios/IOSStreamService"));
|
|
57
|
+
const AndroidStreamService_1 = __importDefault(require("../../device-managers/android/AndroidStreamService"));
|
|
58
|
+
const path_1 = __importDefault(require("path"));
|
|
59
|
+
const os_1 = __importDefault(require("os"));
|
|
60
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
61
|
+
const OmniVisionService_1 = require("../../services/omni-vision/OmniVisionService");
|
|
62
|
+
const InspectorService_1 = require("../../services/InspectorService");
|
|
63
|
+
const router = (0, express_1.Router)();
|
|
64
|
+
function getDeviceInfo(udid) {
|
|
65
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
66
|
+
return yield device_store_1.DeviceStoreFactory.getStore().findDevice({ udid });
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function getDeviceManagerForPlatform(platform) {
|
|
70
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
71
|
+
const dfm = typedi_1.Container.get(device_managers_1.XenonManager);
|
|
72
|
+
const instances = yield dfm.deviceInstances();
|
|
73
|
+
return instances.find((instance) => {
|
|
74
|
+
if (platform === 'android' && instance.constructor.name === 'AndroidDeviceManager')
|
|
75
|
+
return true;
|
|
76
|
+
if ((platform === 'ios' || platform === 'tvos') &&
|
|
77
|
+
instance.constructor.name === 'IOSDeviceManager')
|
|
78
|
+
return true;
|
|
79
|
+
return false;
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
router.post('/:udid/tap', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
84
|
+
var _a, _b;
|
|
85
|
+
const { udid } = req.params;
|
|
86
|
+
const { x, y } = req.body;
|
|
87
|
+
const device = yield getDeviceInfo(udid);
|
|
88
|
+
if (!device)
|
|
89
|
+
return res.status(404).send('Device not found');
|
|
90
|
+
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
91
|
+
if (device.host && !device.host.includes(req.get('host') || '')) {
|
|
92
|
+
logger_1.default.info(`Proxying tap for ${udid} to ${device.host}`);
|
|
93
|
+
try {
|
|
94
|
+
yield InternalHttpClient_1.InternalHttpClient.post(`${device.host}${req.originalUrl}`, req.body);
|
|
95
|
+
return res.status(200).send({ success: true });
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
return res.status(((_a = err.response) === null || _a === void 0 ? void 0 : _a.status) || 500).send(((_b = err.response) === null || _b === void 0 ? void 0 : _b.data) || err.message);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (manager && manager.tap) {
|
|
102
|
+
try {
|
|
103
|
+
yield manager.tap(udid, x, y);
|
|
104
|
+
return res.status(200).send({ success: true });
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
logger_1.default.error(`Manual control Tap failed: ${err.message}`);
|
|
108
|
+
return res.status(500).send({ error: err.message });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else if (device.platform === 'ios' && device.wdaLocalPort) {
|
|
112
|
+
// Fallback/Direct WDA call for iOS
|
|
113
|
+
try {
|
|
114
|
+
yield InternalHttpClient_1.InternalHttpClient.post(`http://localhost:${device.wdaLocalPort}/session/None/wda/tap`, { x, y });
|
|
115
|
+
return res.status(200).send({ success: true });
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
logger_1.default.error(`WDA Tap failed: ${err.message}`);
|
|
119
|
+
return res.status(500).send({ error: err.message });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
res.status(400).send('Manager not found or tap not supported');
|
|
123
|
+
}));
|
|
124
|
+
router.post('/:udid/swipe', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
125
|
+
var _a, _b;
|
|
126
|
+
const { udid } = req.params;
|
|
127
|
+
const { x, y, endX, endY, duration = 1000 } = req.body;
|
|
128
|
+
const device = yield getDeviceInfo(udid);
|
|
129
|
+
if (!device)
|
|
130
|
+
return res.status(404).send('Device not found');
|
|
131
|
+
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
132
|
+
if (device.host && !device.host.includes(req.get('host') || '')) {
|
|
133
|
+
logger_1.default.info(`Proxying swipe for ${udid} to ${device.host}`);
|
|
134
|
+
try {
|
|
135
|
+
yield InternalHttpClient_1.InternalHttpClient.post(`${device.host}${req.originalUrl}`, req.body);
|
|
136
|
+
return res.status(200).send({ success: true });
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
return res.status(((_a = err.response) === null || _a === void 0 ? void 0 : _a.status) || 500).send(((_b = err.response) === null || _b === void 0 ? void 0 : _b.data) || err.message);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (manager && manager.swipe) {
|
|
143
|
+
try {
|
|
144
|
+
yield manager.swipe(udid, x, y, endX, endY, duration);
|
|
145
|
+
return res.status(200).send({ success: true });
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
logger_1.default.error(`Manual control Swipe failed: ${err.message}`);
|
|
149
|
+
return res.status(500).send({ error: err.message });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
res.status(400).send('Manager not found or swipe not supported');
|
|
153
|
+
}));
|
|
154
|
+
router.post('/:udid/text', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
155
|
+
var _a, _b;
|
|
156
|
+
const { udid } = req.params;
|
|
157
|
+
const { text } = req.body;
|
|
158
|
+
const device = yield getDeviceInfo(udid);
|
|
159
|
+
if (!device)
|
|
160
|
+
return res.status(404).send('Device not found');
|
|
161
|
+
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
162
|
+
if (device.host && !device.host.includes(req.get('host') || '')) {
|
|
163
|
+
logger_1.default.info(`Proxying typeText for ${udid} to ${device.host}`);
|
|
164
|
+
try {
|
|
165
|
+
yield InternalHttpClient_1.InternalHttpClient.post(`${device.host}${req.originalUrl}`, req.body);
|
|
166
|
+
return res.status(200).send({ success: true });
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
return res.status(((_a = err.response) === null || _a === void 0 ? void 0 : _a.status) || 500).send(((_b = err.response) === null || _b === void 0 ? void 0 : _b.data) || err.message);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (manager && manager.typeText) {
|
|
173
|
+
try {
|
|
174
|
+
yield manager.typeText(udid, text);
|
|
175
|
+
return res.status(200).send({ success: true });
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
logger_1.default.error(`❌ typeText failed for ${udid}: ${err.message}`);
|
|
179
|
+
return res.status(500).send({ error: err.message });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
res.status(400).send('Manager not found or typeText not supported');
|
|
183
|
+
}));
|
|
184
|
+
router.post('/:udid/keyevent', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
185
|
+
var _a, _b;
|
|
186
|
+
const { udid } = req.params;
|
|
187
|
+
const { keyCode } = req.body;
|
|
188
|
+
const device = yield getDeviceInfo(udid);
|
|
189
|
+
if (!device)
|
|
190
|
+
return res.status(404).send('Device not found');
|
|
191
|
+
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
192
|
+
if (device.host && !device.host.includes(req.get('host') || '')) {
|
|
193
|
+
logger_1.default.info(`Proxying keyevent for ${udid} to ${device.host}`);
|
|
194
|
+
try {
|
|
195
|
+
yield InternalHttpClient_1.InternalHttpClient.post(`${device.host}${req.originalUrl}`, req.body);
|
|
196
|
+
return res.status(200).send({ success: true });
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
return res.status(((_a = err.response) === null || _a === void 0 ? void 0 : _a.status) || 500).send(((_b = err.response) === null || _b === void 0 ? void 0 : _b.data) || err.message);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (manager && manager.pressKey) {
|
|
203
|
+
try {
|
|
204
|
+
yield manager.pressKey(udid, keyCode);
|
|
205
|
+
return res.status(200).send({ success: true });
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
logger_1.default.error(`❌ pressKey failed for ${udid}: ${err.message}`);
|
|
209
|
+
return res.status(500).send({ error: err.message });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
res.status(400).send('Manager not found or pressKey not supported');
|
|
213
|
+
}));
|
|
214
|
+
router.get('/:udid/screenshot', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
215
|
+
const { udid } = req.params;
|
|
216
|
+
const device = yield getDeviceInfo(udid);
|
|
217
|
+
if (!device)
|
|
218
|
+
return res.status(404).send('Device not found');
|
|
219
|
+
// Principal Engineer Optimization: If a high-speed stream is already running for Android,
|
|
220
|
+
// we should grab the latest frame instead of triggering a heavy ADB screencap.
|
|
221
|
+
if (device.platform === 'android') {
|
|
222
|
+
const streamSession = typedi_1.Container.get(AndroidStreamService_1.default).getStreamStatus(udid);
|
|
223
|
+
if ((streamSession === null || streamSession === void 0 ? void 0 : streamSession.status) === 'running' && streamSession.latestFrame) {
|
|
224
|
+
logger_1.default.info(`Manual Control: Using cached stream frame for ${udid} screenshot.`);
|
|
225
|
+
return res.status(200).send({ screenshot: streamSession.latestFrame.toString('base64') });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
229
|
+
if (manager && manager.getScreenshot) {
|
|
230
|
+
const base64 = yield manager.getScreenshot(udid);
|
|
231
|
+
// CRITICAL: Validate screenshot is not empty before returning success
|
|
232
|
+
if (base64 && base64.length > 100) {
|
|
233
|
+
return res.status(200).send({ screenshot: base64 });
|
|
234
|
+
}
|
|
235
|
+
logger_1.default.error(`Screenshot capture failed for ${udid}: returned empty or invalid data (${(base64 === null || base64 === void 0 ? void 0 : base64.length) || 0} bytes)`);
|
|
236
|
+
return res.status(502).send({
|
|
237
|
+
error: 'Screenshot capture failed. Device may be busy or WDA is unresponsive. Try again.',
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
res.status(400).send('Manager not found or screenshot not supported');
|
|
241
|
+
}));
|
|
242
|
+
router.get('/:udid/clipboard', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
243
|
+
const { udid } = req.params;
|
|
244
|
+
const device = yield getDeviceInfo(udid);
|
|
245
|
+
if (!device)
|
|
246
|
+
return res.status(404).send('Device not found');
|
|
247
|
+
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
248
|
+
if (manager && manager.getClipboard) {
|
|
249
|
+
try {
|
|
250
|
+
const content = yield manager.getClipboard(udid);
|
|
251
|
+
logger_1.default.info(`Fetched clipboard for ${udid} (${device.platform}): ${(content === null || content === void 0 ? void 0 : content.length) || 0} chars`);
|
|
252
|
+
return res.status(200).send({ content });
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
logger_1.default.error(`Failed to fetch clipboard for ${udid}: ${err.message}`);
|
|
256
|
+
return res.status(500).send({ error: err.message });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
res.status(400).send('Manager not found or getClipboard not supported');
|
|
260
|
+
}));
|
|
261
|
+
router.post('/:udid/clipboard', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
262
|
+
const { udid } = req.params;
|
|
263
|
+
const { content } = req.body;
|
|
264
|
+
const device = yield getDeviceInfo(udid);
|
|
265
|
+
if (!device)
|
|
266
|
+
return res.status(404).send('Device not found');
|
|
267
|
+
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
268
|
+
if (manager && manager.setClipboard) {
|
|
269
|
+
try {
|
|
270
|
+
yield manager.setClipboard(udid, content);
|
|
271
|
+
return res.status(200).send({ success: true });
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
logger_1.default.error(`Manual control setClipboard failed: ${err.message}`);
|
|
275
|
+
return res.status(500).send({ error: err.message });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
res.status(400).send('Manager not found or setClipboard not supported');
|
|
279
|
+
}));
|
|
280
|
+
router.post('/:udid/touchAndHold', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
281
|
+
var _a, _b;
|
|
282
|
+
const { udid } = req.params;
|
|
283
|
+
const { x, y, duration = 1000 } = req.body;
|
|
284
|
+
const device = yield getDeviceInfo(udid);
|
|
285
|
+
if (!device)
|
|
286
|
+
return res.status(404).send('Device not found');
|
|
287
|
+
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
288
|
+
if (device.host && !device.host.includes(req.get('host') || '')) {
|
|
289
|
+
logger_1.default.info(`Proxying touchAndHold for ${udid} to ${device.host}`);
|
|
290
|
+
try {
|
|
291
|
+
yield InternalHttpClient_1.InternalHttpClient.post(`${device.host}${req.originalUrl}`, req.body);
|
|
292
|
+
return res.status(200).send({ success: true });
|
|
293
|
+
}
|
|
294
|
+
catch (err) {
|
|
295
|
+
return res.status(((_a = err.response) === null || _a === void 0 ? void 0 : _a.status) || 500).send(((_b = err.response) === null || _b === void 0 ? void 0 : _b.data) || err.message);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (manager && manager.touchAndHold) {
|
|
299
|
+
try {
|
|
300
|
+
yield manager.touchAndHold(udid, x, y, duration);
|
|
301
|
+
return res.status(200).send({ success: true });
|
|
302
|
+
}
|
|
303
|
+
catch (err) {
|
|
304
|
+
logger_1.default.error(`Manual control touchAndHold failed: ${err.message}`);
|
|
305
|
+
return res.status(500).send({ error: err.message });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
res.status(400).send('Manager not found or touchAndHold not supported');
|
|
309
|
+
}));
|
|
310
|
+
router.post('/:udid/lock', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
311
|
+
const { udid } = req.params;
|
|
312
|
+
const device = yield getDeviceInfo(udid);
|
|
313
|
+
if (!device)
|
|
314
|
+
return res.status(404).send('Device not found');
|
|
315
|
+
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
316
|
+
if (manager && manager.lock) {
|
|
317
|
+
try {
|
|
318
|
+
yield manager.lock(udid);
|
|
319
|
+
return res.status(200).send({ success: true });
|
|
320
|
+
}
|
|
321
|
+
catch (err) {
|
|
322
|
+
logger_1.default.error(`Manual control lock failed: ${err.message}`);
|
|
323
|
+
return res.status(500).send({ error: err.message });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
res.status(400).send('Manager not found or lock not supported');
|
|
327
|
+
}));
|
|
328
|
+
router.post('/:udid/unlock', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
329
|
+
const { udid } = req.params;
|
|
330
|
+
const device = yield getDeviceInfo(udid);
|
|
331
|
+
if (!device)
|
|
332
|
+
return res.status(404).send('Device not found');
|
|
333
|
+
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
334
|
+
if (manager && manager.unlock) {
|
|
335
|
+
try {
|
|
336
|
+
yield manager.unlock(udid);
|
|
337
|
+
return res.status(200).send({ success: true });
|
|
338
|
+
}
|
|
339
|
+
catch (err) {
|
|
340
|
+
logger_1.default.error(`Manual control unlock failed: ${err.message}`);
|
|
341
|
+
return res.status(500).send({ error: err.message });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
res.status(400).send('Manager not found or unlock not supported');
|
|
345
|
+
}));
|
|
346
|
+
router.post('/:udid/install', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
347
|
+
const { udid } = req.params;
|
|
348
|
+
const { appPath } = req.body;
|
|
349
|
+
const device = yield getDeviceInfo(udid);
|
|
350
|
+
if (!device)
|
|
351
|
+
return res.status(404).send('Device not found');
|
|
352
|
+
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
353
|
+
if (manager && manager.installApp) {
|
|
354
|
+
try {
|
|
355
|
+
yield manager.installApp(udid, appPath);
|
|
356
|
+
return res.status(200).send({ success: true });
|
|
357
|
+
}
|
|
358
|
+
catch (err) {
|
|
359
|
+
logger_1.default.error(`Manual control installApp failed: ${err.message}`);
|
|
360
|
+
return res.status(500).send({ error: err.message });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
res.status(400).send('Manager not found or installApp not supported');
|
|
364
|
+
}));
|
|
365
|
+
router.post('/:udid/install-repository-app', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
366
|
+
const { udid } = req.params;
|
|
367
|
+
const { appId } = req.body;
|
|
368
|
+
const device = yield getDeviceInfo(udid);
|
|
369
|
+
if (!device)
|
|
370
|
+
return res.status(404).send('Device not found');
|
|
371
|
+
try {
|
|
372
|
+
const { APP_SERVICE } = yield Promise.resolve().then(() => __importStar(require('../../dashboard/services/app-service')));
|
|
373
|
+
const app = yield APP_SERVICE.getAppById(appId);
|
|
374
|
+
if (!app)
|
|
375
|
+
return res.status(404).send('App not found in repository');
|
|
376
|
+
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
377
|
+
if (manager && manager.installApp) {
|
|
378
|
+
// If it's a local file, pass the path.
|
|
379
|
+
// In a distributed setup, the node would ideally download it.
|
|
380
|
+
// For now, we assume hub-node shared storage or hub-local execution.
|
|
381
|
+
yield manager.installApp(udid, app.filepath);
|
|
382
|
+
return res.status(200).send({ success: true, message: `Installed ${app.name}` });
|
|
383
|
+
}
|
|
384
|
+
res.status(400).send('Manager not found or installApp not supported');
|
|
385
|
+
}
|
|
386
|
+
catch (err) {
|
|
387
|
+
logger_1.default.error(`Installation from repository failed: ${err.message}`);
|
|
388
|
+
res.status(500).send({ error: err.message });
|
|
389
|
+
}
|
|
390
|
+
}));
|
|
391
|
+
router.post('/:udid/upload-install', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
392
|
+
const { udid } = req.params;
|
|
393
|
+
const device = yield getDeviceInfo(udid);
|
|
394
|
+
if (!device)
|
|
395
|
+
return res.status(404).send('Device not found');
|
|
396
|
+
if (!req.files || Object.keys(req.files).length === 0) {
|
|
397
|
+
return res.status(400).send('No files were uploaded.');
|
|
398
|
+
}
|
|
399
|
+
const appFile = req.files.app;
|
|
400
|
+
if (!appFile)
|
|
401
|
+
return res.status(400).send('File "app" is required');
|
|
402
|
+
const tmpDir = path_1.default.join(os_1.default.tmpdir(), 'xenon-uploads');
|
|
403
|
+
if (!fs_extra_1.default.existsSync(tmpDir))
|
|
404
|
+
fs_extra_1.default.mkdirSync(tmpDir, { recursive: true });
|
|
405
|
+
const appPath = path_1.default.join(tmpDir, `${Date.now()}-${appFile.name}`);
|
|
406
|
+
try {
|
|
407
|
+
yield appFile.mv(appPath);
|
|
408
|
+
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
409
|
+
if (manager && manager.installApp) {
|
|
410
|
+
yield manager.installApp(udid, appPath);
|
|
411
|
+
// Clean up after installation
|
|
412
|
+
setTimeout(() => fs_extra_1.default.remove(appPath).catch(() => { }), 10000);
|
|
413
|
+
return res
|
|
414
|
+
.status(200)
|
|
415
|
+
.send({ success: true, message: `App ${appFile.name} installed successfully` });
|
|
416
|
+
}
|
|
417
|
+
res.status(400).send('Manager not found or installApp not supported');
|
|
418
|
+
}
|
|
419
|
+
catch (err) {
|
|
420
|
+
logger_1.default.error(`Installation failed for ${udid}: ${err.message}`);
|
|
421
|
+
res.status(500).send({ error: err.message });
|
|
422
|
+
}
|
|
423
|
+
}));
|
|
424
|
+
router.post('/:udid/uninstall', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
425
|
+
const { udid } = req.params;
|
|
426
|
+
const { bundleId } = req.body;
|
|
427
|
+
const device = yield getDeviceInfo(udid);
|
|
428
|
+
if (!device)
|
|
429
|
+
return res.status(404).send('Device not found');
|
|
430
|
+
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
431
|
+
if (manager && manager.uninstallApp) {
|
|
432
|
+
try {
|
|
433
|
+
yield manager.uninstallApp(udid, bundleId);
|
|
434
|
+
return res.status(200).send({ success: true });
|
|
435
|
+
}
|
|
436
|
+
catch (err) {
|
|
437
|
+
logger_1.default.error(`Manual control uninstallApp failed: ${err.message}`);
|
|
438
|
+
return res.status(500).send({ error: err.message });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
res.status(400).send('Manager not found or uninstallApp not supported');
|
|
442
|
+
}));
|
|
443
|
+
router.get('/:udid/apps', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
444
|
+
const { udid } = req.params;
|
|
445
|
+
const device = yield getDeviceInfo(udid);
|
|
446
|
+
if (!device)
|
|
447
|
+
return res.status(404).send('Device not found');
|
|
448
|
+
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
449
|
+
if (manager && manager.listApps) {
|
|
450
|
+
try {
|
|
451
|
+
const apps = yield manager.listApps(udid);
|
|
452
|
+
logger_1.default.info(`Found ${apps.length} apps installed on device ${udid}`);
|
|
453
|
+
return res.status(200).send(apps);
|
|
454
|
+
}
|
|
455
|
+
catch (err) {
|
|
456
|
+
logger_1.default.error(`Failed to list apps for ${udid}: ${err.message}`);
|
|
457
|
+
return res.status(500).send({ error: err.message });
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
res.status(400).send('Manager not found or listApps not supported');
|
|
461
|
+
}));
|
|
462
|
+
router.get('/:udid/logs', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
463
|
+
const { udid } = req.params;
|
|
464
|
+
const device = yield getDeviceInfo(udid);
|
|
465
|
+
if (!device)
|
|
466
|
+
return res.status(404).send('Device not found');
|
|
467
|
+
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
468
|
+
if (manager && manager.getLogs) {
|
|
469
|
+
try {
|
|
470
|
+
const logs = yield manager.getLogs(udid);
|
|
471
|
+
return res.status(200).send({ logs });
|
|
472
|
+
}
|
|
473
|
+
catch (err) {
|
|
474
|
+
logger_1.default.error(`Failed to fetch logs for ${udid}: ${err.message}`);
|
|
475
|
+
return res.status(500).send({ error: err.message });
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
res.status(400).send('Manager not found or getLogs not supported');
|
|
479
|
+
}));
|
|
480
|
+
const MJPEG_PROXY_CACHE = new Map();
|
|
481
|
+
/**
|
|
482
|
+
* Start stream endpoint - Initiates WDA and MJPEG streaming for iOS devices
|
|
483
|
+
*/
|
|
484
|
+
router.post('/:udid/stream/start', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
485
|
+
const { udid } = req.params;
|
|
486
|
+
const device = yield getDeviceInfo(udid);
|
|
487
|
+
if (!device)
|
|
488
|
+
return res.status(404).send('Device not found');
|
|
489
|
+
// Principal Insight: Automation Protection
|
|
490
|
+
// If the device is already busy (e.g., Appium session or another Control session)
|
|
491
|
+
// we must block new manual control requests.
|
|
492
|
+
const isCurrentlyControlledManually = (device.platform === 'ios' || device.platform === 'tvos'
|
|
493
|
+
? typedi_1.Container.get(IOSStreamService_1.default).getStreamStatus(udid)
|
|
494
|
+
: typedi_1.Container.get(AndroidStreamService_1.default).getStreamStatus(udid)) !== undefined;
|
|
495
|
+
if (device.busy && !isCurrentlyControlledManually) {
|
|
496
|
+
logger_1.default.warn(`Manual Control refused for ${udid}: Device is already busy.`);
|
|
497
|
+
return res.status(409).send({
|
|
498
|
+
success: false,
|
|
499
|
+
error: 'Device is currently busy with another session.',
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
try {
|
|
503
|
+
let result;
|
|
504
|
+
if (device.platform === 'ios' || device.platform === 'tvos') {
|
|
505
|
+
const iosStreamService = typedi_1.Container.get(IOSStreamService_1.default);
|
|
506
|
+
result = yield iosStreamService.startStream(udid);
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
const androidStreamService = typedi_1.Container.get(AndroidStreamService_1.default);
|
|
510
|
+
result = yield androidStreamService.startStream(udid);
|
|
511
|
+
}
|
|
512
|
+
// Refresh device info (Lazy loading of dimensions if missing)
|
|
513
|
+
try {
|
|
514
|
+
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
515
|
+
if (manager &&
|
|
516
|
+
manager.getAdditionalDeviceInfo &&
|
|
517
|
+
(!device.screenWidth || !device.screenHeight)) {
|
|
518
|
+
logger_1.default.info(`Fetching missing dimensions for ${udid} on stream start`);
|
|
519
|
+
const additionalInfo = yield manager.getAdditionalDeviceInfo(device);
|
|
520
|
+
if (additionalInfo && Object.keys(additionalInfo).length > 0) {
|
|
521
|
+
yield device_store_1.DeviceStoreFactory.getStore().updateDevice(udid, device.host, additionalInfo);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
catch (e) {
|
|
526
|
+
logger_1.default.warn(`Non-critical: Failed to fetch additional device info during stream start: ${e}`);
|
|
527
|
+
}
|
|
528
|
+
// Principal Insight: Concurrency Protection
|
|
529
|
+
// Mark device as "Busy" so automation sessions don't pick it up
|
|
530
|
+
const manualSid = `manual_${udid}`;
|
|
531
|
+
yield (0, device_service_1.blockDevice)(udid, device.host, manualSid);
|
|
532
|
+
logger_1.default.info(`Manual Control: Device ${udid} locked for active UI session (${manualSid}).`);
|
|
533
|
+
logger_1.default.info(`Stream started for ${udid} - Port: ${result.mjpegPort}`);
|
|
534
|
+
return res.status(200).send({
|
|
535
|
+
success: true,
|
|
536
|
+
mjpegPort: result.mjpegPort,
|
|
537
|
+
streamUrl: `/xenon/api/control/${udid}/stream`,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
catch (err) {
|
|
541
|
+
logger_1.default.error(`Failed to start stream for ${udid}: ${err.message}`);
|
|
542
|
+
return res.status(500).send({
|
|
543
|
+
success: false,
|
|
544
|
+
error: err.message,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}));
|
|
548
|
+
/**
|
|
549
|
+
* Stop stream endpoint
|
|
550
|
+
*/
|
|
551
|
+
router.post('/:udid/stream/stop', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
552
|
+
const { udid } = req.params;
|
|
553
|
+
const device = yield getDeviceInfo(udid);
|
|
554
|
+
if (!device)
|
|
555
|
+
return res.status(404).send('Device not found');
|
|
556
|
+
try {
|
|
557
|
+
if (device.platform === 'ios' || device.platform === 'tvos') {
|
|
558
|
+
yield typedi_1.Container.get(IOSStreamService_1.default).stopStream(udid);
|
|
559
|
+
}
|
|
560
|
+
else {
|
|
561
|
+
yield typedi_1.Container.get(AndroidStreamService_1.default).stopStream(udid);
|
|
562
|
+
}
|
|
563
|
+
// Clear and stop MJPEG proxy
|
|
564
|
+
const existingProxy = MJPEG_PROXY_CACHE.get(udid);
|
|
565
|
+
if (existingProxy) {
|
|
566
|
+
existingProxy.stop();
|
|
567
|
+
MJPEG_PROXY_CACHE.delete(udid);
|
|
568
|
+
}
|
|
569
|
+
logger_1.default.info(`Stream stopped for ${udid}`);
|
|
570
|
+
return res.status(200).send({ success: true });
|
|
571
|
+
}
|
|
572
|
+
catch (err) {
|
|
573
|
+
logger_1.default.error(`Failed to stop stream for ${udid}: ${err.message}`);
|
|
574
|
+
return res.status(500).send({ success: false, error: err.message });
|
|
575
|
+
}
|
|
576
|
+
}));
|
|
577
|
+
/**
|
|
578
|
+
* Stream status endpoint - Get current stream status for a device
|
|
579
|
+
*/
|
|
580
|
+
router.get('/:udid/stream/status', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
581
|
+
const { udid } = req.params;
|
|
582
|
+
const device = yield getDeviceInfo(udid);
|
|
583
|
+
if (!device)
|
|
584
|
+
return res.status(404).send('Device not found');
|
|
585
|
+
if (device.platform === 'ios' || device.platform === 'tvos') {
|
|
586
|
+
const iosStreamService = typedi_1.Container.get(IOSStreamService_1.default);
|
|
587
|
+
const session = iosStreamService.getStreamStatus(udid);
|
|
588
|
+
if (session) {
|
|
589
|
+
return res.status(200).send({
|
|
590
|
+
udid,
|
|
591
|
+
status: session.status,
|
|
592
|
+
wdaPort: session.wdaPort,
|
|
593
|
+
mjpegPort: session.mjpegPort,
|
|
594
|
+
startedAt: session.startedAt,
|
|
595
|
+
lastError: session.lastError,
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
const androidStreamService = typedi_1.Container.get(AndroidStreamService_1.default);
|
|
601
|
+
const session = androidStreamService.getStreamStatus(udid);
|
|
602
|
+
if (session) {
|
|
603
|
+
return res.status(200).send({
|
|
604
|
+
udid,
|
|
605
|
+
status: session.status,
|
|
606
|
+
mjpegPort: session.mjpegPort,
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return res.status(200).send({
|
|
611
|
+
udid,
|
|
612
|
+
status: 'stopped',
|
|
613
|
+
mjpegPort: device.mjpegServerPort,
|
|
614
|
+
});
|
|
615
|
+
}));
|
|
616
|
+
/**
|
|
617
|
+
* MJPEG Stream endpoint - Proxies the MJPEG stream from WDA
|
|
618
|
+
* Auto-starts the stream if not running (for iOS devices)
|
|
619
|
+
*/
|
|
620
|
+
router.get('/:udid/stream', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
621
|
+
const { udid } = req.params;
|
|
622
|
+
const device = yield getDeviceInfo(udid);
|
|
623
|
+
if (!device)
|
|
624
|
+
return res.status(404).send('Device not found');
|
|
625
|
+
let mjpegPort = device.mjpegServerPort;
|
|
626
|
+
// For iOS devices, try to auto-start the stream if not available
|
|
627
|
+
if (device.platform === 'ios' || device.platform === 'tvos') {
|
|
628
|
+
const iosStreamService = typedi_1.Container.get(IOSStreamService_1.default);
|
|
629
|
+
const session = iosStreamService.getStreamStatus(udid);
|
|
630
|
+
if (session && session.status === 'running') {
|
|
631
|
+
mjpegPort = session.mjpegPort;
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
try {
|
|
635
|
+
logger_1.default.info(`Stream for iOS device ${udid} requested (Status: ${(session === null || session === void 0 ? void 0 : session.status) || 'idle'})...`);
|
|
636
|
+
const result = yield iosStreamService.startStream(udid);
|
|
637
|
+
mjpegPort = result.mjpegPort;
|
|
638
|
+
}
|
|
639
|
+
catch (err) {
|
|
640
|
+
logger_1.default.error(`Failed to start stream for ${udid}: ${err.message}`);
|
|
641
|
+
return res.status(503).send({
|
|
642
|
+
error: 'Stream not available',
|
|
643
|
+
message: err.message,
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
else {
|
|
649
|
+
// Android auto-start
|
|
650
|
+
const androidStreamService = typedi_1.Container.get(AndroidStreamService_1.default);
|
|
651
|
+
const session = androidStreamService.getStreamStatus(udid);
|
|
652
|
+
if (session && session.status === 'running') {
|
|
653
|
+
mjpegPort = session.mjpegPort;
|
|
654
|
+
}
|
|
655
|
+
else {
|
|
656
|
+
try {
|
|
657
|
+
const result = yield androidStreamService.startStream(udid);
|
|
658
|
+
mjpegPort = result.mjpegPort;
|
|
659
|
+
}
|
|
660
|
+
catch (err) {
|
|
661
|
+
return res.status(503).send({ error: 'Android stream failed', message: err.message });
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (!mjpegPort) {
|
|
666
|
+
return res.status(404).send({
|
|
667
|
+
error: 'MJPEG port not found for device',
|
|
668
|
+
hint: 'For iOS: Use POST /stream/start to begin streaming. For Android: Start an Appium session first.',
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
const videoUrl = `http://127.0.0.1:${mjpegPort}`;
|
|
672
|
+
// UniversalMjpegProxy will handle connectivity and retries internally
|
|
673
|
+
if (!MJPEG_PROXY_CACHE.has(udid)) {
|
|
674
|
+
MJPEG_PROXY_CACHE.set(udid, new UniversalMjpegProxy_1.UniversalMjpegProxy(videoUrl));
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
677
|
+
// Check if URL changed and update proxy
|
|
678
|
+
const existingProxy = MJPEG_PROXY_CACHE.get(udid);
|
|
679
|
+
if (existingProxy.url !== videoUrl) {
|
|
680
|
+
existingProxy.stop();
|
|
681
|
+
MJPEG_PROXY_CACHE.set(udid, new UniversalMjpegProxy_1.UniversalMjpegProxy(videoUrl));
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
try {
|
|
685
|
+
const proxy = MJPEG_PROXY_CACHE.get(udid);
|
|
686
|
+
if (proxy) {
|
|
687
|
+
const streamService = device.platform === 'ios' || device.platform === 'tvos'
|
|
688
|
+
? typedi_1.Container.get(IOSStreamService_1.default)
|
|
689
|
+
: typedi_1.Container.get(AndroidStreamService_1.default);
|
|
690
|
+
// Register this specific browser connection
|
|
691
|
+
streamService.updateViewerCount(udid, 1);
|
|
692
|
+
req.on('close', () => {
|
|
693
|
+
// Clean up when this browser tab/connection closes
|
|
694
|
+
streamService.updateViewerCount(udid, -1);
|
|
695
|
+
});
|
|
696
|
+
proxy.proxyRequest(req, res);
|
|
697
|
+
}
|
|
698
|
+
else {
|
|
699
|
+
res.status(404).send('Proxy not created');
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
catch (err) {
|
|
703
|
+
logger_1.default.error(`MJPEG proxy error for ${udid}: ${err.message}`);
|
|
704
|
+
res.status(500).send({ error: 'Stream proxy error', message: err.message });
|
|
705
|
+
}
|
|
706
|
+
}));
|
|
707
|
+
/**
|
|
708
|
+
* Interactive Shell Endpoint
|
|
709
|
+
* Executes simple, safe shell commands on the device
|
|
710
|
+
*/
|
|
711
|
+
router.post('/:udid/shell', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
712
|
+
const { udid } = req.params;
|
|
713
|
+
const { command } = req.body;
|
|
714
|
+
const device = yield getDeviceInfo(udid);
|
|
715
|
+
if (!device)
|
|
716
|
+
return res.status(404).send('Device not found');
|
|
717
|
+
if (!command)
|
|
718
|
+
return res.status(400).send('Command is required');
|
|
719
|
+
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
720
|
+
if (manager && manager.executeShell) {
|
|
721
|
+
try {
|
|
722
|
+
const output = yield manager.executeShell(udid, command);
|
|
723
|
+
return res.status(200).send({ output });
|
|
724
|
+
}
|
|
725
|
+
catch (err) {
|
|
726
|
+
logger_1.default.error(`Shell execution failed for ${udid}: ${err.message}`);
|
|
727
|
+
// Return 200 with error property so frontend displays it in terminal
|
|
728
|
+
return res.status(200).send({ error: err.message });
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
res.status(400).send('Manager not found or executeShell not supported');
|
|
732
|
+
}));
|
|
733
|
+
/**
|
|
734
|
+
* Omni-Scan for manual control (No Appium Session)
|
|
735
|
+
*/
|
|
736
|
+
router.get('/:udid/omni-scan', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
737
|
+
const { udid } = req.params;
|
|
738
|
+
const device = yield getDeviceInfo(udid);
|
|
739
|
+
if (!device)
|
|
740
|
+
return res.status(404).send('Device not found');
|
|
741
|
+
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
742
|
+
if (!manager)
|
|
743
|
+
return res.status(400).send('Manager not found');
|
|
744
|
+
try {
|
|
745
|
+
const omniService = typedi_1.Container.get(OmniVisionService_1.OmniVisionService);
|
|
746
|
+
// Create a Mock Driver that OmniVisionService can use
|
|
747
|
+
const mockDriver = {
|
|
748
|
+
sessionId: `manual_${udid}`,
|
|
749
|
+
getScreenshot: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
750
|
+
if (manager.getScreenshot) {
|
|
751
|
+
return yield manager.getScreenshot(udid);
|
|
752
|
+
}
|
|
753
|
+
throw new Error('Screenshot not supported for this device');
|
|
754
|
+
}),
|
|
755
|
+
// OmniVision might need page source for some analysis later
|
|
756
|
+
getPageSource: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
757
|
+
if (manager.getPageSource) {
|
|
758
|
+
return yield manager.getPageSource(udid);
|
|
759
|
+
}
|
|
760
|
+
return '';
|
|
761
|
+
}),
|
|
762
|
+
};
|
|
763
|
+
const result = yield omniService.analyzeScreen(mockDriver);
|
|
764
|
+
return res.status(200).send({ status: 'success', value: result });
|
|
765
|
+
}
|
|
766
|
+
catch (err) {
|
|
767
|
+
logger_1.default.error(`Manual Omni-Scan failed for ${udid}: ${err.message}`);
|
|
768
|
+
return res.status(500).send({ status: 'error', message: err.message });
|
|
769
|
+
}
|
|
770
|
+
}));
|
|
771
|
+
/**
|
|
772
|
+
* Native-First Inspector Snapshot
|
|
773
|
+
*/
|
|
774
|
+
router.get('/:udid/inspector/snapshot', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
775
|
+
const { udid } = req.params;
|
|
776
|
+
try {
|
|
777
|
+
const inspectorService = typedi_1.Container.get(InspectorService_1.InspectorService);
|
|
778
|
+
const snapshot = yield inspectorService.getSnapshot(udid);
|
|
779
|
+
return res.status(200).send(snapshot);
|
|
780
|
+
}
|
|
781
|
+
catch (err) {
|
|
782
|
+
logger_1.default.error(`Inspector snapshot failed for ${udid}: ${err.message}`);
|
|
783
|
+
return res.status(500).send({ error: err.message });
|
|
784
|
+
}
|
|
785
|
+
}));
|
|
786
|
+
/**
|
|
787
|
+
* AI Locator test for manual control
|
|
788
|
+
*/
|
|
789
|
+
router.post('/:udid/test-locator', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
790
|
+
const { udid } = req.params;
|
|
791
|
+
const { strategy, selector } = req.body;
|
|
792
|
+
const device = yield getDeviceInfo(udid);
|
|
793
|
+
if (!device)
|
|
794
|
+
return res.status(404).send('Device not found');
|
|
795
|
+
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
796
|
+
if (!manager)
|
|
797
|
+
return res.status(400).send('Manager not found');
|
|
798
|
+
try {
|
|
799
|
+
const omniService = typedi_1.Container.get(OmniVisionService_1.OmniVisionService);
|
|
800
|
+
const mockDriver = {
|
|
801
|
+
sessionId: `manual_${udid}`,
|
|
802
|
+
getScreenshot: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
803
|
+
if (manager.getScreenshot) {
|
|
804
|
+
return yield manager.getScreenshot(udid);
|
|
805
|
+
}
|
|
806
|
+
throw new Error('Screenshot not supported for this device');
|
|
807
|
+
}),
|
|
808
|
+
};
|
|
809
|
+
let value = [];
|
|
810
|
+
if (strategy === '-custom:ai-text') {
|
|
811
|
+
value = yield omniService.findByText(mockDriver, selector);
|
|
812
|
+
}
|
|
813
|
+
else if (strategy === '-custom:ai-icon') {
|
|
814
|
+
const match = yield omniService.findByIcon(mockDriver, selector);
|
|
815
|
+
if (match)
|
|
816
|
+
value = [match];
|
|
817
|
+
}
|
|
818
|
+
else {
|
|
819
|
+
return res
|
|
820
|
+
.status(400)
|
|
821
|
+
.send({ status: 'error', message: `Unsupported strategy: ${strategy}` });
|
|
822
|
+
}
|
|
823
|
+
return res.status(200).send({ status: 'success', value });
|
|
824
|
+
}
|
|
825
|
+
catch (err) {
|
|
826
|
+
logger_1.default.error(`Manual test-locator failed for ${udid}: ${err.message}`);
|
|
827
|
+
return res.status(500).send({ status: 'error', message: err.message });
|
|
828
|
+
}
|
|
829
|
+
}));
|
|
830
|
+
function register(parentRouter) {
|
|
831
|
+
parentRouter.use('/control', router);
|
|
832
|
+
}
|
|
833
|
+
exports.default = {
|
|
834
|
+
register,
|
|
835
|
+
};
|