@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.
Files changed (228) hide show
  1. package/README.md +446 -0
  2. package/lib/package.json +207 -0
  3. package/lib/public/assets/Layouts-7IT8aFLI.js +11 -0
  4. package/lib/public/assets/Layouts-DPMls9vh.css +1 -0
  5. package/lib/public/assets/ai-settings-BbnfgdEx.js +11 -0
  6. package/lib/public/assets/apps-CRMrI4_p.js +16 -0
  7. package/lib/public/assets/apps-CcM77dgg.css +1 -0
  8. package/lib/public/assets/badge-B1nKs8zj.css +1 -0
  9. package/lib/public/assets/badge-CSvl5xIU.js +11 -0
  10. package/lib/public/assets/button-CJlKn4PZ.css +1 -0
  11. package/lib/public/assets/button-CvLaGFYj.js +26 -0
  12. package/lib/public/assets/calendar-6w-D6Oaw.js +6 -0
  13. package/lib/public/assets/clock-DcdeWBPr.js +6 -0
  14. package/lib/public/assets/cpu-DiSoXT9n.js +6 -0
  15. package/lib/public/assets/device-explorer-CajM63OJ.js +193 -0
  16. package/lib/public/assets/device-explorer-CxdUAoTL.css +1 -0
  17. package/lib/public/assets/index-ByQwMN5T.js +174 -0
  18. package/lib/public/assets/index-C1DBaoSh.js +1 -0
  19. package/lib/public/assets/index-qzCez_kk.css +1 -0
  20. package/lib/public/assets/lock-B23ibZmo.js +6 -0
  21. package/lib/public/assets/maintenance-settings-CirzA6yG.js +6 -0
  22. package/lib/public/assets/mouse-pointer-2-Cz76SHFb.js +6 -0
  23. package/lib/public/assets/plus-BBwlIevt.js +6 -0
  24. package/lib/public/assets/session-dashboard-C2k7FFv_.css +1 -0
  25. package/lib/public/assets/session-dashboard-HPDtwPOZ.js +62 -0
  26. package/lib/public/assets/settings-DrZsZwdc.js +1 -0
  27. package/lib/public/assets/trash-2-DQpvzJec.js +6 -0
  28. package/lib/public/assets/useSocket-Dxsqae2a.js +16 -0
  29. package/lib/public/assets/webhook-settings-CDPgsgkb.css +1 -0
  30. package/lib/public/assets/webhook-settings-Cp-B4Nrw.js +1 -0
  31. package/lib/public/assets/zap-DovP6iow.js +6 -0
  32. package/lib/public/favicon.ico +0 -0
  33. package/lib/public/favicon.png +0 -0
  34. package/lib/public/favicon.svg +9 -0
  35. package/lib/public/index.html +46 -0
  36. package/lib/public/logo.svg +17 -0
  37. package/lib/public/logo192.png +0 -0
  38. package/lib/public/logo512.png +0 -0
  39. package/lib/public/manifest.json +25 -0
  40. package/lib/public/robots.txt +3 -0
  41. package/lib/schema.json +348 -0
  42. package/lib/src/InternalHttpClient.js +212 -0
  43. package/lib/src/PluginContext.js +29 -0
  44. package/lib/src/XenonCapabilityManager.js +199 -0
  45. package/lib/src/app/index.js +167 -0
  46. package/lib/src/app/routers/apps.js +79 -0
  47. package/lib/src/app/routers/config.js +131 -0
  48. package/lib/src/app/routers/control.js +835 -0
  49. package/lib/src/app/routers/dashboard.js +301 -0
  50. package/lib/src/app/routers/grid.js +352 -0
  51. package/lib/src/app/routers/reservation.js +190 -0
  52. package/lib/src/app/routers/webhook.js +83 -0
  53. package/lib/src/app/swagger-docs.js +203 -0
  54. package/lib/src/app/swagger.js +366 -0
  55. package/lib/src/chromeUtils.js +148 -0
  56. package/lib/src/commands/handle.js +19 -0
  57. package/lib/src/commands/index.js +8 -0
  58. package/lib/src/config.js +73 -0
  59. package/lib/src/dashboard/asset-manager.js +84 -0
  60. package/lib/src/dashboard/commands.js +284 -0
  61. package/lib/src/dashboard/event-manager.js +699 -0
  62. package/lib/src/dashboard/services/app-service.js +134 -0
  63. package/lib/src/dashboard/services/failure-analysis-service.js +173 -0
  64. package/lib/src/dashboard/services/session-service.js +113 -0
  65. package/lib/src/data-service/CircuitBreaker.js +83 -0
  66. package/lib/src/data-service/config-service.js +155 -0
  67. package/lib/src/data-service/db.js +122 -0
  68. package/lib/src/data-service/device-service.js +320 -0
  69. package/lib/src/data-service/device-store.interface.js +2 -0
  70. package/lib/src/data-service/device-store.js +345 -0
  71. package/lib/src/data-service/pending-sessions-service.js +25 -0
  72. package/lib/src/data-service/pluginArgs.js +25 -0
  73. package/lib/src/data-service/prisma-service.js +31 -0
  74. package/lib/src/data-service/prisma-store.js +385 -0
  75. package/lib/src/data-service/queue-service.js +150 -0
  76. package/lib/src/data-service/web-config-service.js +130 -0
  77. package/lib/src/device-managers/AndroidDeviceManager.js +1155 -0
  78. package/lib/src/device-managers/ChromeDriverManager.js +68 -0
  79. package/lib/src/device-managers/HealthMonitorService.js +325 -0
  80. package/lib/src/device-managers/IOSDeviceManager.js +351 -0
  81. package/lib/src/device-managers/NodeDevices.js +82 -0
  82. package/lib/src/device-managers/android/AndroidStreamService.js +370 -0
  83. package/lib/src/device-managers/android/DeviceLockManager.js +45 -0
  84. package/lib/src/device-managers/cloud/CapabilityManager.js +26 -0
  85. package/lib/src/device-managers/cloud/Devices.js +86 -0
  86. package/lib/src/device-managers/iOSTracker.js +44 -0
  87. package/lib/src/device-managers/index.js +89 -0
  88. package/lib/src/device-managers/ios/IOSDiscoveryService.js +268 -0
  89. package/lib/src/device-managers/ios/IOSStreamService.js +893 -0
  90. package/lib/src/device-managers/ios/WDAClient.js +866 -0
  91. package/lib/src/device-utils.js +663 -0
  92. package/lib/src/enums/Capabilities.js +8 -0
  93. package/lib/src/enums/Cloud.js +11 -0
  94. package/lib/src/enums/Platform.js +9 -0
  95. package/lib/src/enums/SessionType.js +9 -0
  96. package/lib/src/enums/SocketEvents.js +15 -0
  97. package/lib/src/helpers/UniversalMjpegProxy.js +273 -0
  98. package/lib/src/helpers/index.js +229 -0
  99. package/lib/src/index.js +95 -0
  100. package/lib/src/interceptors/CommandInterceptor.js +524 -0
  101. package/lib/src/interfaces/ICloudManager.js +2 -0
  102. package/lib/src/interfaces/IDevice.js +2 -0
  103. package/lib/src/interfaces/IDeviceFilterOptions.js +2 -0
  104. package/lib/src/interfaces/IDeviceManager.js +2 -0
  105. package/lib/src/interfaces/IOptions.js +2 -0
  106. package/lib/src/interfaces/IPluginArgs.js +55 -0
  107. package/lib/src/interfaces/ISessionCapability.js +2 -0
  108. package/lib/src/logger.js +225 -0
  109. package/lib/src/plugin.js +244 -0
  110. package/lib/src/prisma.js +12 -0
  111. package/lib/src/profiling/AndroidAppProfiler.js +213 -0
  112. package/lib/src/proxy/wd-command-proxy.js +221 -0
  113. package/lib/src/scripts/generate-database-migration.js +59 -0
  114. package/lib/src/scripts/initialize-database.js +55 -0
  115. package/lib/src/scripts/install-go-ios.js +66 -0
  116. package/lib/src/scripts/prepare-prisma.js +89 -0
  117. package/lib/src/services/AICommandService.js +143 -0
  118. package/lib/src/services/AIService.js +466 -0
  119. package/lib/src/services/CleanupService.js +141 -0
  120. package/lib/src/services/EventBus.js +74 -0
  121. package/lib/src/services/InspectorService.js +395 -0
  122. package/lib/src/services/MetricsService.js +134 -0
  123. package/lib/src/services/NetworkConditioningService.js +173 -0
  124. package/lib/src/services/NotificationService.js +163 -0
  125. package/lib/src/services/RequestLogService.js +252 -0
  126. package/lib/src/services/ResourceIsolationService.js +122 -0
  127. package/lib/src/services/SecurityService.js +120 -0
  128. package/lib/src/services/ServerManager.js +284 -0
  129. package/lib/src/services/SessionHeartbeatService.js +158 -0
  130. package/lib/src/services/SessionLifecycleService.js +572 -0
  131. package/lib/src/services/SocketClient.js +71 -0
  132. package/lib/src/services/SocketServer.js +87 -0
  133. package/lib/src/services/TracingService.js +132 -0
  134. package/lib/src/services/VideoPipelineService.js +220 -0
  135. package/lib/src/services/healing/FuzzyXmlHealingProvider.js +333 -0
  136. package/lib/src/services/healing/HealEtalonService.js +98 -0
  137. package/lib/src/services/healing/HealedLocatorGenerator.js +132 -0
  138. package/lib/src/services/healing/HealingOrchestrator.js +165 -0
  139. package/lib/src/services/healing/LlmHealingProvider.js +77 -0
  140. package/lib/src/services/healing/OcrHealingProvider.js +119 -0
  141. package/lib/src/services/healing/ResilioTreeHealingProvider.js +100 -0
  142. package/lib/src/services/healing/VisualAiHealingProvider.js +90 -0
  143. package/lib/src/services/healing/types.js +12 -0
  144. package/lib/src/services/omni-vision/OmniVisionService.js +718 -0
  145. package/lib/src/services/omni-vision/VisionAssertionService.js +68 -0
  146. package/lib/src/sessions/CloudSession.js +42 -0
  147. package/lib/src/sessions/LocalSession.js +313 -0
  148. package/lib/src/sessions/RemoteSession.js +287 -0
  149. package/lib/src/sessions/SessionManager.js +238 -0
  150. package/lib/src/sessions/XenonSession.js +44 -0
  151. package/lib/src/types/CLIArgs.js +2 -0
  152. package/lib/src/types/CloudArgs.js +2 -0
  153. package/lib/src/types/CloudSchema.js +131 -0
  154. package/lib/src/types/DeviceType.js +2 -0
  155. package/lib/src/types/DeviceUpdate.js +2 -0
  156. package/lib/src/types/IOSDevice.js +2 -0
  157. package/lib/src/types/Platform.js +2 -0
  158. package/lib/src/types/SessionStatus.js +11 -0
  159. package/lib/src/validators/CapabilityValidator.js +93 -0
  160. package/lib/test/e2e/android/conf.spec.js +43 -0
  161. package/lib/test/e2e/android/conf2.spec.js +44 -0
  162. package/lib/test/e2e/android/conf3.spec.js +44 -0
  163. package/lib/test/e2e/e2ehelper.js +113 -0
  164. package/lib/test/e2e/hubnode/forward-request.spec.js +224 -0
  165. package/lib/test/e2e/hubnode/hubnode.spec.js +214 -0
  166. package/lib/test/e2e/ios/conf1.spec.js +39 -0
  167. package/lib/test/e2e/ios/conf2.spec.js +39 -0
  168. package/lib/test/e2e/plugin-harness.js +236 -0
  169. package/lib/test/e2e/plugin.spec.js +97 -0
  170. package/lib/test/e2e/telemetry_verification.spec.js +83 -0
  171. package/lib/test/e2e/video-recording-test.spec.js +63 -0
  172. package/lib/test/helpers/test-container.js +112 -0
  173. package/lib/test/integration/androidDevices.spec.js +137 -0
  174. package/lib/test/integration/cliArgs.js +73 -0
  175. package/lib/test/integration/ios/01iOSSimulator.spec.js +291 -0
  176. package/lib/test/integration/ios/02iOSDevices.spec.js +75 -0
  177. package/lib/test/integration/testHelpers.js +74 -0
  178. package/lib/test/unit/AndroidDeviceManager.spec.js +178 -0
  179. package/lib/test/unit/ChromeDriverManager.spec.js +26 -0
  180. package/lib/test/unit/CleanupService.spec.js +21 -0
  181. package/lib/test/unit/DeviceModel.spec.js +157 -0
  182. package/lib/test/unit/FuzzyXmlHealingProvider.test.js +294 -0
  183. package/lib/test/unit/GetAdbOriginal.js +42 -0
  184. package/lib/test/unit/HealingCascade.test.js +128 -0
  185. package/lib/test/unit/IOSDeviceManager.spec.js +261 -0
  186. package/lib/test/unit/RemoteIOs.spec.js +78 -0
  187. package/lib/test/unit/ResilioTreeHealingProvider.test.js +96 -0
  188. package/lib/test/unit/commands.spec.js +27 -0
  189. package/lib/test/unit/config.spec.js +27 -0
  190. package/lib/test/unit/device-service.spec.js +307 -0
  191. package/lib/test/unit/device-utils.spec.js +313 -0
  192. package/lib/test/unit/fixtures/device.config.js +4 -0
  193. package/lib/test/unit/fixtures/devices.js +89 -0
  194. package/lib/test/unit/helpers.spec.js +62 -0
  195. package/lib/test/unit/omni-vision.spec.js +100 -0
  196. package/lib/test/unit/plugin.spec.js +133 -0
  197. package/lib/tsconfig.tsbuildinfo +1 -0
  198. package/package.json +207 -0
  199. package/prisma/data.db +0 -0
  200. package/prisma/dev.db +0 -0
  201. package/prisma/dev.db-journal +0 -0
  202. package/prisma/migrations/20231011074725_initial_tables/migration.sql +47 -0
  203. package/prisma/migrations/20231226115334_update_session_log/migration.sql +2 -0
  204. package/prisma/migrations/20251204113710_add_video_recording_enabled/migration.sql +29 -0
  205. package/prisma/migrations/20251204132449_add_log_table/migration.sql +11 -0
  206. package/prisma/migrations/20251205050111_add_profiling_support/migration.sql +47 -0
  207. package/prisma/migrations/20251205050947_add_is_error_field/migration.sql +24 -0
  208. package/prisma/migrations/20260126201337_add_app_model/migration.sql +18 -0
  209. package/prisma/migrations/20260130115722_add_performance_trace_and_xenon_sync/migration.sql +2 -0
  210. package/prisma/migrations/20260130135114_add_device_models/migration.sql +57 -0
  211. package/prisma/migrations/20260130140655_make_systemport_optional/migration.sql +45 -0
  212. package/prisma/migrations/20260130140932_make_device_fields_optional/migration.sql +45 -0
  213. package/prisma/migrations/20260130141040_final_schema_fix/migration.sql +45 -0
  214. package/prisma/migrations/20260130143234_add_device_health_fields/migration.sql +4 -0
  215. package/prisma/migrations/20260130144921_add_failure_category/migration.sql +2 -0
  216. package/prisma/migrations/20260131151456_add_webhook_config/migration.sql +10 -0
  217. package/prisma/migrations/20260201094507_add_device_tags/migration.sql +11 -0
  218. package/prisma/migrations/20260201103410_add_managed_process/migration.sql +15 -0
  219. package/prisma/migrations/20260201140637_add_web_config/migration.sql +22 -0
  220. package/prisma/migrations/20260201162232_add_session_progress/migration.sql +2 -0
  221. package/prisma/migrations/20260201174231_add_total_healed_count/migration.sql +2 -0
  222. package/prisma/migrations/migration_lock.toml +3 -0
  223. package/prisma/schema.prisma +210 -0
  224. package/schema.json +348 -0
  225. package/scripts/build-xenon.sh +32 -0
  226. package/scripts/dev/debug-gemini.ts +44 -0
  227. package/scripts/generate-types-from-schema.js +86 -0
  228. package/scripts/install-compatible-driver.js +39 -0
@@ -0,0 +1,287 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.RemoteSession = void 0;
16
+ const axios_1 = __importDefault(require("axios"));
17
+ const logger_1 = __importDefault(require("../logger"));
18
+ const SessionType_1 = __importDefault(require("../enums/SessionType"));
19
+ const XenonSession_1 = require("./XenonSession");
20
+ class RemoteSession extends XenonSession_1.XenonSession {
21
+ constructor(options) {
22
+ super(options);
23
+ this.isVideoAvailable = false;
24
+ this.baseUrl = options.baseUrl;
25
+ }
26
+ isVideoRecordingInProgress() {
27
+ return this.isVideoAvailable;
28
+ }
29
+ getType() {
30
+ return SessionType_1.default.REMOTE;
31
+ }
32
+ getScreenShot() {
33
+ return (0, axios_1.default)({
34
+ method: 'get',
35
+ url: `${this.baseUrl}/session/${this.sessionId}/screenshot`,
36
+ }).then((response) => (response.data ? response.data.value : ''));
37
+ }
38
+ stopVideoRecording(_driver) {
39
+ return __awaiter(this, void 0, void 0, function* () {
40
+ var _a, _b, _c, _d, _e, _f, _g;
41
+ logger_1.default.info(`[RemoteSession] stopVideoRecording called for session ${this.sessionId}. isVideoAvailable: ${this.isVideoAvailable}`);
42
+ try {
43
+ const response = yield (0, axios_1.default)({
44
+ method: 'post',
45
+ url: `${this.baseUrl}/session/${this.sessionId}/appium/stop_recording_screen`,
46
+ data: {},
47
+ });
48
+ const dataLen = ((_b = (_a = response === null || response === void 0 ? void 0 : response.data) === null || _a === void 0 ? void 0 : _a.value) === null || _b === void 0 ? void 0 : _b.length) || 0;
49
+ logger_1.default.info(`[RemoteSession] stopVideoRecording response status: ${response.status}, data length: ${dataLen}`);
50
+ return response.status === 200 && ((_c = response === null || response === void 0 ? void 0 : response.data) === null || _c === void 0 ? void 0 : _c.value) ? (_d = response === null || response === void 0 ? void 0 : response.data) === null || _d === void 0 ? void 0 : _d.value : '';
51
+ }
52
+ catch (err) {
53
+ logger_1.default.warn(`[RemoteSession] stopVideoRecording error: ${err.message}, trying anyway to retrieve video...`);
54
+ // Even if there's an error, try to get the video data
55
+ try {
56
+ const retryResponse = yield (0, axios_1.default)({
57
+ method: 'post',
58
+ url: `${this.baseUrl}/session/${this.sessionId}/appium/stop_recording_screen`,
59
+ data: {},
60
+ });
61
+ const retryDataLen = ((_f = (_e = retryResponse === null || retryResponse === void 0 ? void 0 : retryResponse.data) === null || _e === void 0 ? void 0 : _e.value) === null || _f === void 0 ? void 0 : _f.length) || 0;
62
+ logger_1.default.info(`[RemoteSession] stopVideoRecording retry succeeded, data length: ${retryDataLen}`);
63
+ return ((_g = retryResponse === null || retryResponse === void 0 ? void 0 : retryResponse.data) === null || _g === void 0 ? void 0 : _g.value) || '';
64
+ }
65
+ catch (retryErr) {
66
+ logger_1.default.error(`[RemoteSession] stopVideoRecording retry also failed: ${retryErr.message}`);
67
+ return '';
68
+ }
69
+ }
70
+ });
71
+ }
72
+ stopPerformanceRecording() {
73
+ return __awaiter(this, void 0, void 0, function* () {
74
+ var _a, _b;
75
+ logger_1.default.info(`[RemoteSession] stopPerformanceRecording called for session ${this.sessionId}`);
76
+ try {
77
+ const response = yield (0, axios_1.default)({
78
+ method: 'post',
79
+ url: `${this.baseUrl}/session/${this.sessionId}/execute/sync`,
80
+ data: {
81
+ script: 'mobile: stopPerfRecord',
82
+ args: [
83
+ {
84
+ profileName: 'Time Profiler',
85
+ },
86
+ ],
87
+ },
88
+ });
89
+ return response.status === 200 && ((_a = response === null || response === void 0 ? void 0 : response.data) === null || _a === void 0 ? void 0 : _a.value) ? (_b = response === null || response === void 0 ? void 0 : response.data) === null || _b === void 0 ? void 0 : _b.value : null;
90
+ }
91
+ catch (err) {
92
+ logger_1.default.warn(`[RemoteSession] stopPerformanceRecording failed: ${err.message}`);
93
+ return null;
94
+ }
95
+ });
96
+ }
97
+ startPerformanceRecording() {
98
+ return __awaiter(this, void 0, void 0, function* () {
99
+ logger_1.default.info(`[RemoteSession] startPerformanceRecording called for session ${this.sessionId}`);
100
+ try {
101
+ yield (0, axios_1.default)({
102
+ method: 'post',
103
+ url: `${this.baseUrl}/session/${this.sessionId}/execute/sync`,
104
+ data: {
105
+ script: 'mobile: startPerfRecord',
106
+ args: [
107
+ {
108
+ profileName: 'Time Profiler',
109
+ timeout: 1800000, // 30 mins
110
+ },
111
+ ],
112
+ },
113
+ });
114
+ }
115
+ catch (err) {
116
+ logger_1.default.warn(`[RemoteSession] startPerformanceRecording failed: ${err.message}`);
117
+ }
118
+ });
119
+ }
120
+ startVideoRecording(options, _driver) {
121
+ return __awaiter(this, void 0, void 0, function* () {
122
+ const device = this.getDevice();
123
+ let resolution = (options === null || options === void 0 ? void 0 : options.resolution) ? options.resolution.replace('x', ':') : undefined;
124
+ let size = (options === null || options === void 0 ? void 0 : options.resolution) ? options.resolution.replace(':', 'x') : undefined;
125
+ // Principal Intelligence: Auto-detect orientation based on device dimensions
126
+ // to prevent squashed/stretched videos.
127
+ if (!resolution && device.screenWidth && device.screenHeight) {
128
+ const w = parseInt(device.screenWidth);
129
+ const h = parseInt(device.screenHeight);
130
+ logger_1.default.info(`[RemoteSession] Auto-detected device dimensions: ${w}x${h} for session ${this.sessionId}`);
131
+ if (h > w) {
132
+ // Portrait device: Use vertical 720p equivalent
133
+ resolution = '720:1280';
134
+ size = '720x1280';
135
+ }
136
+ else {
137
+ // Landscape device: Use standard 720p
138
+ resolution = '1280:720';
139
+ size = '1280x720';
140
+ }
141
+ }
142
+ else if (!resolution) {
143
+ // Fallback: Default to portrait 720p if dimensions unknown, as most mobile tests are portrait
144
+ resolution = '720:1280';
145
+ size = '720x1280';
146
+ }
147
+ logger_1.default.info(`[RemoteSession] Starting recording with resolution: ${resolution}, size: ${size}`);
148
+ return (0, axios_1.default)({
149
+ method: 'post',
150
+ url: `${this.baseUrl}/session/${this.sessionId}/appium/start_recording_screen`,
151
+ data: {
152
+ options: {
153
+ videoType: 'libx264',
154
+ videoFps: 10,
155
+ /* Force video scale to ensure width/height are divisible by 2 (ffmpeg requirement) */
156
+ videoScale: resolution,
157
+ videoSize: size,
158
+ timeLimit: 1800, // 30 min
159
+ },
160
+ },
161
+ })
162
+ .then((response) => {
163
+ // Set flag to true if response is successful (status 200 or 2xx)
164
+ this.isVideoAvailable = response.status >= 200 && response.status < 300;
165
+ const status = response.status;
166
+ logger_1.default.info(`[RemoteSession] startVideoRecording response status: ${status}, isVideoAvailable: ${this.isVideoAvailable}`);
167
+ })
168
+ .catch((error) => {
169
+ logger_1.default.error('[RemoteSession] startVideoRecording error:', error.message);
170
+ this.isVideoAvailable = false;
171
+ throw error;
172
+ });
173
+ });
174
+ }
175
+ getLiveVideoUrl() {
176
+ const url = new URL(this.baseUrl);
177
+ const capabilities = this.getCapabilities();
178
+ if (capabilities['mjpegServerPort'] && !isNaN(capabilities['mjpegServerPort'])) {
179
+ return `${url.origin}/xenon/api/session/${this.sessionId}/live_video`;
180
+ }
181
+ else {
182
+ return null;
183
+ }
184
+ }
185
+ checkHealth() {
186
+ return __awaiter(this, void 0, void 0, function* () {
187
+ var _a, _b, _c, _d;
188
+ if (!this.sessionId) {
189
+ return {
190
+ isHealthy: false,
191
+ errorType: XenonSession_1.HealthErrorType.NONE,
192
+ message: 'No session ID assigned',
193
+ };
194
+ }
195
+ // Identify the root Appium URL (strip /wd-internal if present)
196
+ const appiumUrl = this.baseUrl.replace(/\/wd-internal$/, '');
197
+ try {
198
+ // 1️⃣ Safe, read-only, lightweight probe: GET /session/{id}/timeouts
199
+ // This is a W3C standard command that all Appium drivers should register.
200
+ const response = yield (0, axios_1.default)({
201
+ method: 'get',
202
+ url: `${this.baseUrl}/session/${this.sessionId}/timeouts`,
203
+ timeout: 5000,
204
+ });
205
+ return {
206
+ isHealthy: response.status >= 200 && response.status < 300,
207
+ errorType: XenonSession_1.HealthErrorType.NONE,
208
+ statusCode: response.status,
209
+ };
210
+ }
211
+ catch (err) {
212
+ const status = (_a = err.response) === null || _a === void 0 ? void 0 : _a.status;
213
+ const message = err.message || 'Unknown error';
214
+ const errorData = ((_c = (_b = err.response) === null || _b === void 0 ? void 0 : _b.data) === null || _c === void 0 ? void 0 : _c.value) || ((_d = err.response) === null || _d === void 0 ? void 0 : _d.data);
215
+ const errorJson = JSON.stringify(errorData || {}).toLowerCase();
216
+ // 2️⃣ Failure Classification & Fallback Logic
217
+ if (status === 404) {
218
+ // Distinguish between "Session Not Found" and "Command Not Found"
219
+ // Appium 2 returns "No route found" or "unknown command" for unsupported endpoints.
220
+ const isUnsupported = errorJson.includes('unknown command') || errorJson.includes('no route found');
221
+ const isInvalidSession = errorJson.includes('invalid session id');
222
+ if (isUnsupported) {
223
+ logger_1.default.info('[RemoteSession] Probe endpoint unsupported on this driver, but driver is responsive. Classifying as HEALTHY.');
224
+ return {
225
+ isHealthy: true,
226
+ errorType: XenonSession_1.HealthErrorType.UNSUPPORTED_ENDPOINT,
227
+ message: 'Probe endpoint unsupported but driver is alive',
228
+ };
229
+ }
230
+ if (isInvalidSession) {
231
+ logger_1.default.error(`[RemoteSession] Session ${this.sessionId} definitively GONE (Invalid Session ID).`);
232
+ return {
233
+ isHealthy: false,
234
+ errorType: XenonSession_1.HealthErrorType.SESSION_NOT_FOUND,
235
+ message: 'Invalid Session ID',
236
+ statusCode: 404,
237
+ };
238
+ }
239
+ logger_1.default.warn(`[RemoteSession] Session ${this.sessionId} probe returned 404. Performing server status fallback...`);
240
+ // Fallback: Verify Appium server is alive
241
+ try {
242
+ const statusRes = yield axios_1.default.get(`${appiumUrl}/status`, { timeout: 3000 });
243
+ if (statusRes.status === 200) {
244
+ // If server is alive but we got a raw 404 with no specific error,
245
+ // it's likely the session is gone in Appium 2 (where /sessions list check is unsupported).
246
+ return {
247
+ isHealthy: false,
248
+ errorType: XenonSession_1.HealthErrorType.SESSION_NOT_FOUND,
249
+ message: 'Session likely gone (Server alive but 404 on session path)',
250
+ statusCode: 404,
251
+ };
252
+ }
253
+ }
254
+ catch (serverErr) {
255
+ logger_1.default.error(`[RemoteSession] Appium server REACHABILITY FAILED: ${serverErr.message}`);
256
+ return {
257
+ isHealthy: false,
258
+ errorType: XenonSession_1.HealthErrorType.SERVER_UNREACHABLE,
259
+ message: `Server unreachable: ${serverErr.message}`,
260
+ };
261
+ }
262
+ }
263
+ if (status >= 500) {
264
+ return {
265
+ isHealthy: false,
266
+ errorType: XenonSession_1.HealthErrorType.DRIVER_ERROR,
267
+ message: `Driver internal error: ${message}`,
268
+ statusCode: status,
269
+ };
270
+ }
271
+ if (err.code === 'ECONNREFUSED' || err.code === 'ETIMEDOUT') {
272
+ return {
273
+ isHealthy: false,
274
+ errorType: XenonSession_1.HealthErrorType.SERVER_UNREACHABLE,
275
+ message: `Connection failed: ${err.code}`,
276
+ };
277
+ }
278
+ return {
279
+ isHealthy: false,
280
+ errorType: XenonSession_1.HealthErrorType.TIMEOUT,
281
+ message: `Probe timed out: ${message}`,
282
+ };
283
+ }
284
+ });
285
+ }
286
+ }
287
+ exports.RemoteSession = RemoteSession;
@@ -0,0 +1,238 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
9
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
10
+ return new (P || (P = Promise))(function (resolve, reject) {
11
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
12
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
13
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
14
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
15
+ });
16
+ };
17
+ var __importDefault = (this && this.__importDefault) || function (mod) {
18
+ return (mod && mod.__esModule) ? mod : { "default": mod };
19
+ };
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.SESSION_MANAGER = exports.SessionManager = void 0;
22
+ const typedi_1 = require("typedi");
23
+ const prisma_1 = require("../prisma");
24
+ const logger_1 = __importDefault(require("../logger"));
25
+ const RemoteSession_1 = require("./RemoteSession");
26
+ const CloudSession_1 = require("./CloudSession");
27
+ const device_store_1 = require("../data-service/device-store");
28
+ const helpers_1 = require("../helpers");
29
+ const XenonCapabilityManager_1 = require("../XenonCapabilityManager");
30
+ /**
31
+ * SessionManager with persistence and recovery capabilities.
32
+ *
33
+ * Key Design Decisions:
34
+ * 1. LocalSessions CANNOT be recovered (Appium driver dies with the hub).
35
+ * On recovery, orphaned local sessions are marked as failed.
36
+ * 2. RemoteSessions and CloudSessions CAN be recovered because they only
37
+ * need sessionId and baseUrl to continue proxying commands.
38
+ * 3. Session state is persisted to SQLite via Prisma for durability.
39
+ */
40
+ let SessionManager = class SessionManager {
41
+ constructor() {
42
+ this.sessionMap = new Map();
43
+ this.log = logger_1.default.scope('SessionManager');
44
+ }
45
+ /**
46
+ * Add a session to the in-memory map.
47
+ * Note: Session is already persisted to DB via event-manager.onSessionCreated
48
+ */
49
+ addSession(sessionId, session) {
50
+ this.sessionMap.set(sessionId, session);
51
+ this.log.debug(`Added session ${sessionId} to memory (total: ${this.sessionMap.size})`);
52
+ }
53
+ /**
54
+ * Remove a session from the in-memory map.
55
+ */
56
+ removeSession(sessionId) {
57
+ this.sessionMap.delete(sessionId);
58
+ this.log.debug(`Removed session ${sessionId} from memory (total: ${this.sessionMap.size})`);
59
+ }
60
+ isValidSession(sessionId) {
61
+ return this.sessionMap.has(sessionId);
62
+ }
63
+ getSession(sessionId) {
64
+ return this.sessionMap.get(sessionId);
65
+ }
66
+ getAllSessions() {
67
+ return Array.from(this.sessionMap.values());
68
+ }
69
+ getSessionCount() {
70
+ return this.sessionMap.size;
71
+ }
72
+ /**
73
+ * Recover active sessions from the database after hub restart.
74
+ *
75
+ * @param currentNodeId - The ID of the current hub node
76
+ * @param nodeBasePath - The base path for WebDriver URLs
77
+ *
78
+ * Strategy:
79
+ * - Sessions on THIS node (LocalSessions) are marked as failed because
80
+ * the Appium driver is gone.
81
+ * - Sessions on REMOTE nodes are recovered as RemoteSessions.
82
+ * - Sessions on CLOUD providers are recovered as CloudSessions.
83
+ */
84
+ recoverActiveSessions(currentNodeId, nodeBasePath) {
85
+ return __awaiter(this, void 0, void 0, function* () {
86
+ this.log.info('🔄 Attempting to recover active sessions from database...');
87
+ try {
88
+ // Find all sessions that are still "running" (not ended)
89
+ const activeSessions = yield prisma_1.prisma.session.findMany({
90
+ where: {
91
+ status: 'running',
92
+ endTime: null,
93
+ },
94
+ });
95
+ if (activeSessions.length === 0) {
96
+ this.log.info('✅ No active sessions to recover.');
97
+ return 0;
98
+ }
99
+ this.log.info(`📋 Found ${activeSessions.length} active sessions in database.`);
100
+ let recoveredCount = 0;
101
+ let failedCount = 0;
102
+ for (const dbSession of activeSessions) {
103
+ try {
104
+ // Get the device associated with this session
105
+ const device = yield device_store_1.DeviceStoreFactory.getStore().findDevice({
106
+ udid: dbSession.device_udid,
107
+ });
108
+ if (!device) {
109
+ this.log.warn(`⚠️ Session ${dbSession.id}: Device ${dbSession.device_udid} not found. Marking as failed.`);
110
+ yield this.markSessionAsFailed(dbSession.id, 'Device not found after hub restart');
111
+ failedCount++;
112
+ continue;
113
+ }
114
+ // Check if this session was on THE CURRENT NODE (LocalSession)
115
+ const isLocalSession = dbSession.node_id === currentNodeId || device.nodeId === currentNodeId;
116
+ if (isLocalSession) {
117
+ // LocalSessions cannot be recovered - the Appium driver is gone
118
+ this.log.warn(`⏹️ Session ${dbSession.id}: Was a LocalSession on this node. Marking as failed (driver lost).`);
119
+ yield this.markSessionAsFailed(dbSession.id, 'Hub restarted - local Appium driver session was lost');
120
+ // Also unblock the device
121
+ yield device_store_1.DeviceStoreFactory.getStore().updateDevice(device.udid, device.host, {
122
+ busy: false,
123
+ session_id: undefined,
124
+ });
125
+ failedCount++;
126
+ continue;
127
+ }
128
+ // Parse session capabilities from JSON
129
+ let sessionResponse = {};
130
+ try {
131
+ sessionResponse = JSON.parse(dbSession.session_capabilities || '{}');
132
+ }
133
+ catch (e) {
134
+ this.log.warn(`Session ${dbSession.id}: Could not parse session_capabilities`);
135
+ }
136
+ let desiredCaps = {};
137
+ try {
138
+ desiredCaps = JSON.parse(dbSession.desired_capabilities || '{}');
139
+ }
140
+ catch (e) {
141
+ this.log.warn(`Session ${dbSession.id}: Could not parse desired_capabilities`);
142
+ }
143
+ const xenonCapabilities = (0, XenonCapabilityManager_1.getXenonCapabilities)({
144
+ alwaysMatch: desiredCaps,
145
+ firstMatch: [{}],
146
+ });
147
+ const baseUrl = (0, helpers_1.nodeUrl)(device, nodeBasePath);
148
+ // Create the appropriate session type
149
+ let recoveredSession;
150
+ if (device.cloud) {
151
+ // CloudSession
152
+ recoveredSession = new CloudSession_1.CloudSession({
153
+ sessionId: dbSession.id,
154
+ device: device,
155
+ sessionResponse: sessionResponse,
156
+ xenonOption: xenonCapabilities,
157
+ baseUrl: baseUrl,
158
+ });
159
+ this.log.info(`☁️ Recovered CloudSession ${dbSession.id} on ${device.cloud}`);
160
+ }
161
+ else {
162
+ // RemoteSession
163
+ recoveredSession = new RemoteSession_1.RemoteSession({
164
+ sessionId: dbSession.id,
165
+ device: device,
166
+ sessionResponse: sessionResponse,
167
+ xenonOption: xenonCapabilities,
168
+ baseUrl: baseUrl,
169
+ });
170
+ this.log.info(`🌐 Recovered RemoteSession ${dbSession.id} on node ${device.nodeId}`);
171
+ }
172
+ // Add to in-memory map
173
+ this.addSession(dbSession.id, recoveredSession);
174
+ recoveredCount++;
175
+ }
176
+ catch (sessionErr) {
177
+ this.log.error(`❌ Failed to recover session ${dbSession.id}: ${sessionErr.message}`);
178
+ yield this.markSessionAsFailed(dbSession.id, `Recovery failed: ${sessionErr.message}`);
179
+ failedCount++;
180
+ }
181
+ }
182
+ this.log.info(`✅ Session recovery complete. Recovered: ${recoveredCount}, Failed: ${failedCount}`);
183
+ return recoveredCount;
184
+ }
185
+ catch (err) {
186
+ this.log.error(`❌ Session recovery error: ${err.message}`);
187
+ return 0;
188
+ }
189
+ });
190
+ }
191
+ /**
192
+ * Mark a session as failed in the database
193
+ */
194
+ markSessionAsFailed(sessionId, reason) {
195
+ return __awaiter(this, void 0, void 0, function* () {
196
+ try {
197
+ yield prisma_1.prisma.session.update({
198
+ where: { id: sessionId },
199
+ data: {
200
+ status: 'failed',
201
+ endTime: new Date(),
202
+ failure_reason: reason,
203
+ failure_category: 'HUB_RESTART',
204
+ },
205
+ });
206
+ }
207
+ catch (err) {
208
+ this.log.error(`Failed to mark session ${sessionId} as failed: ${err.message}`);
209
+ }
210
+ });
211
+ }
212
+ /**
213
+ * Get statistics about current sessions
214
+ */
215
+ getStats() {
216
+ const byType = {
217
+ local: 0,
218
+ remote: 0,
219
+ cloud: 0,
220
+ };
221
+ for (const session of Array.from(this.sessionMap.values())) {
222
+ const type = session.getType().toLowerCase();
223
+ byType[type] = (byType[type] || 0) + 1;
224
+ }
225
+ return {
226
+ total: this.sessionMap.size,
227
+ byType,
228
+ };
229
+ }
230
+ };
231
+ exports.SessionManager = SessionManager;
232
+ exports.SessionManager = SessionManager = __decorate([
233
+ (0, typedi_1.Service)()
234
+ ], SessionManager);
235
+ // Export singleton for backward compatibility
236
+ // New code should use Container.get(SessionManager)
237
+ const typedi_2 = require("typedi");
238
+ exports.SESSION_MANAGER = typedi_2.Container.get(SessionManager);
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.XenonSession = exports.HealthErrorType = exports.SessionHealthState = void 0;
4
+ var SessionHealthState;
5
+ (function (SessionHealthState) {
6
+ SessionHealthState["HEALTHY"] = "HEALTHY";
7
+ SessionHealthState["DEGRADED"] = "DEGRADED";
8
+ SessionHealthState["SUSPECT"] = "SUSPECT";
9
+ SessionHealthState["DEAD"] = "DEAD";
10
+ })(SessionHealthState || (exports.SessionHealthState = SessionHealthState = {}));
11
+ var HealthErrorType;
12
+ (function (HealthErrorType) {
13
+ HealthErrorType["NONE"] = "NONE";
14
+ HealthErrorType["UNSUPPORTED_ENDPOINT"] = "UNSUPPORTED_ENDPOINT";
15
+ HealthErrorType["SESSION_NOT_FOUND"] = "SESSION_NOT_FOUND";
16
+ HealthErrorType["SERVER_UNREACHABLE"] = "SERVER_UNREACHABLE";
17
+ HealthErrorType["DRIVER_ERROR"] = "DRIVER_ERROR";
18
+ HealthErrorType["TIMEOUT"] = "TIMEOUT";
19
+ })(HealthErrorType || (exports.HealthErrorType = HealthErrorType = {}));
20
+ class XenonSession {
21
+ constructor(options) {
22
+ this.options = options;
23
+ this.isStopping = false;
24
+ this.healthState = SessionHealthState.HEALTHY;
25
+ this.sessionId = options.sessionId;
26
+ this.xenonOption = options.xenonOption;
27
+ }
28
+ getId() {
29
+ return this.sessionId;
30
+ }
31
+ getDevice() {
32
+ return this.options.device;
33
+ }
34
+ getDeviceFarmOptions() {
35
+ return this.xenonOption;
36
+ }
37
+ getCapabilities() {
38
+ return this.options.sessionResponse;
39
+ }
40
+ getXenonOption(option, defaultValue = undefined) {
41
+ return this.xenonOption[option] ? this.xenonOption[option] : defaultValue;
42
+ }
43
+ }
44
+ exports.XenonSession = XenonSession;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });