@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,572 @@
|
|
|
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 __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
19
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
20
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
21
|
+
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;
|
|
22
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
23
|
+
};
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
42
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
43
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
44
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
45
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
46
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
47
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
51
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
52
|
+
};
|
|
53
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
|
+
exports.SessionLifecycleService = void 0;
|
|
55
|
+
const typedi_1 = require("typedi");
|
|
56
|
+
const uuid_1 = require("uuid");
|
|
57
|
+
const lodash_1 = __importDefault(require("lodash"));
|
|
58
|
+
const axios_1 = __importDefault(require("axios"));
|
|
59
|
+
const https_proxy_agent_1 = require("https-proxy-agent");
|
|
60
|
+
const http_proxy_agent_1 = require("http-proxy-agent");
|
|
61
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
62
|
+
const helpers_1 = require("../helpers");
|
|
63
|
+
const InternalHttpClient_1 = require("../InternalHttpClient");
|
|
64
|
+
const PluginContext_1 = require("../PluginContext");
|
|
65
|
+
const CapabilityValidator_1 = require("../validators/CapabilityValidator");
|
|
66
|
+
const device_service_1 = require("../data-service/device-service");
|
|
67
|
+
const pending_sessions_service_1 = require("../data-service/pending-sessions-service");
|
|
68
|
+
const device_utils_1 = require("../device-utils");
|
|
69
|
+
const TracingService_1 = require("./TracingService");
|
|
70
|
+
const XenonCapabilityManager_1 = require("../XenonCapabilityManager");
|
|
71
|
+
const CircuitBreaker_1 = require("../data-service/CircuitBreaker");
|
|
72
|
+
const wd_command_proxy_1 = require("../proxy/wd-command-proxy");
|
|
73
|
+
const device_store_1 = require("../data-service/device-store");
|
|
74
|
+
const LocalSession_1 = require("../sessions/LocalSession");
|
|
75
|
+
const CloudSession_1 = require("../sessions/CloudSession");
|
|
76
|
+
const RemoteSession_1 = require("../sessions/RemoteSession");
|
|
77
|
+
const SessionManager_1 = require("../sessions/SessionManager");
|
|
78
|
+
const event_manager_1 = require("../dashboard/event-manager");
|
|
79
|
+
const session_service_1 = require("../dashboard/services/session-service");
|
|
80
|
+
const SessionType_1 = __importDefault(require("../enums/SessionType"));
|
|
81
|
+
const async_lock_1 = __importDefault(require("async-lock"));
|
|
82
|
+
const commandsQueueGuard = new async_lock_1.default();
|
|
83
|
+
let SessionLifecycleService = class SessionLifecycleService {
|
|
84
|
+
constructor() {
|
|
85
|
+
this.logger = logger_1.default.scope('SessionLifecycleService');
|
|
86
|
+
}
|
|
87
|
+
createSession(next, driver, caps) {
|
|
88
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
89
|
+
const context = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
90
|
+
const pluginArgs = context.pluginArgs;
|
|
91
|
+
this.logger.debug(`📱 pluginArgs: ${JSON.stringify(pluginArgs)}`);
|
|
92
|
+
this.logger.debug(`Receiving session request at host: ${pluginArgs.bindHostOrIp}`);
|
|
93
|
+
const pendingSessionId = (0, uuid_1.v4)();
|
|
94
|
+
this.logger.debug(`📱 Creating temporary session capability_id: ${pendingSessionId}`);
|
|
95
|
+
const { alwaysMatch: requiredCaps = {}, firstMatch: allFirstMatchCaps = [{}] } = caps;
|
|
96
|
+
const strippedRequiredCaps = (0, helpers_1.stripAppiumPrefixes)(requiredCaps);
|
|
97
|
+
const strippedFirstMatchCaps = (0, helpers_1.stripAppiumPrefixes)(allFirstMatchCaps[0]);
|
|
98
|
+
try {
|
|
99
|
+
const mergedCaps = Object.assign({}, strippedFirstMatchCaps, strippedRequiredCaps);
|
|
100
|
+
typedi_1.Container.get(CapabilityValidator_1.CapabilityValidator).validate(mergedCaps);
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
this.logger.error(`❌ Session validation failed: ${err.message}`);
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
const app = strippedRequiredCaps['app'] || strippedFirstMatchCaps['app'];
|
|
107
|
+
if (app && typeof app === 'string' && !app.includes('/') && !app.includes('\\')) {
|
|
108
|
+
const { APP_SERVICE } = yield Promise.resolve().then(() => __importStar(require('../dashboard/services/app-service')));
|
|
109
|
+
const appDetails = yield APP_SERVICE.getAppById(app);
|
|
110
|
+
if (appDetails) {
|
|
111
|
+
const appUrl = `http://${pluginArgs.bindHostOrIp}:${context.port}/xenon/api/apps/${appDetails.id}/download`;
|
|
112
|
+
this.logger.info(`📱 Resolved app ID ${app} to ${appUrl}`);
|
|
113
|
+
if (requiredCaps['app'])
|
|
114
|
+
requiredCaps['app'] = appUrl;
|
|
115
|
+
if (allFirstMatchCaps[0]['app'])
|
|
116
|
+
allFirstMatchCaps[0]['app'] = appUrl;
|
|
117
|
+
if (caps.alwaysMatch && caps.alwaysMatch['app'])
|
|
118
|
+
caps.alwaysMatch['app'] = appUrl;
|
|
119
|
+
if (caps.alwaysMatch && caps.alwaysMatch['appium:app'])
|
|
120
|
+
caps.alwaysMatch['appium:app'] = appUrl;
|
|
121
|
+
if (caps.firstMatch && caps.firstMatch[0]) {
|
|
122
|
+
if (caps.firstMatch[0]['app'])
|
|
123
|
+
caps.firstMatch[0]['app'] = appUrl;
|
|
124
|
+
if (caps.firstMatch[0]['appium:app'])
|
|
125
|
+
caps.firstMatch[0]['appium:app'] = appUrl;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const firstMatch = Array.isArray(caps.firstMatch) && caps.firstMatch.length > 0 ? caps.firstMatch[0] : {};
|
|
130
|
+
yield (0, pending_sessions_service_1.addNewPendingSession)(Object.assign(Object.assign({}, Object.assign({}, firstMatch, caps.alwaysMatch)), { capability_id: pendingSessionId, createdAt: new Date().getTime() }));
|
|
131
|
+
const lockName = this.getLockName(caps);
|
|
132
|
+
this.logger.debug(`📱 Acquiring lock: ${lockName}`);
|
|
133
|
+
const device = yield commandsQueueGuard.acquire(lockName, () => __awaiter(this, void 0, void 0, function* () {
|
|
134
|
+
try {
|
|
135
|
+
return yield (0, device_utils_1.allocateDeviceForSession)(caps, pluginArgs.deviceAvailabilityTimeoutMs, pluginArgs.deviceAvailabilityQueryIntervalMs, pluginArgs);
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
yield (0, pending_sessions_service_1.removePendingSession)(pendingSessionId);
|
|
139
|
+
throw err;
|
|
140
|
+
}
|
|
141
|
+
}));
|
|
142
|
+
yield (0, device_service_1.updateDeviceProgress)(device.udid, device.host, 'Allocating node resources...');
|
|
143
|
+
let session;
|
|
144
|
+
const isRemoteOrCloudSession = !device.nodeId || device.nodeId !== context.nodeId;
|
|
145
|
+
this.logger.debug(`device.host: ${device.host} and pluginArgs.bindHostOrIp: ${pluginArgs.bindHostOrIp}`);
|
|
146
|
+
if (isRemoteOrCloudSession) {
|
|
147
|
+
this.logger.debug(`📱 Forwarding session request to ${device.host}`);
|
|
148
|
+
yield (0, device_service_1.updateDeviceProgress)(device.udid, device.host, 'Forwarding to remote node...');
|
|
149
|
+
session = yield this.forwardSessionRequest(device, caps);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
this.logger.debug('📱 Creating session on the same node');
|
|
153
|
+
yield this.handleLocalWDAProvisioning(device, caps);
|
|
154
|
+
yield (0, device_service_1.updateDeviceProgress)(device.udid, device.host, 'Finalizing session bootstrap...');
|
|
155
|
+
session = yield next();
|
|
156
|
+
}
|
|
157
|
+
this.logger.debug('📱 Session response: ', JSON.stringify(session));
|
|
158
|
+
this.logger.debug(`📱 Removing pending session with capability_id: ${pendingSessionId}`);
|
|
159
|
+
yield (0, pending_sessions_service_1.removePendingSession)(pendingSessionId);
|
|
160
|
+
if (this.isCreateSessionResponseInternal(session)) {
|
|
161
|
+
yield this.finalizeSession(session, device, caps, driver, isRemoteOrCloudSession);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
yield this.handleSessionFailure(session, device, isRemoteOrCloudSession);
|
|
165
|
+
}
|
|
166
|
+
return session;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
handleLocalWDAProvisioning(device, caps) {
|
|
170
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
171
|
+
if (device.platform === 'ios' && device.realDevice) {
|
|
172
|
+
const { APP_SERVICE } = yield Promise.resolve().then(() => __importStar(require('../dashboard/services/app-service')));
|
|
173
|
+
const wdaApp = yield APP_SERVICE.getWDAApp();
|
|
174
|
+
const { default: IOSStreamService } = yield Promise.resolve().then(() => __importStar(require('../device-managers/ios/IOSStreamService')));
|
|
175
|
+
const streamService = typedi_1.Container.get(IOSStreamService);
|
|
176
|
+
const streamStatus = streamService.getStreamStatus(device.udid);
|
|
177
|
+
const isWdaActive = streamStatus && (streamStatus.status === 'running' || streamStatus.status === 'starting');
|
|
178
|
+
if (!isWdaActive && wdaApp && (yield Promise.resolve().then(() => __importStar(require('fs-extra')))).existsSync(wdaApp.filepath)) {
|
|
179
|
+
this.logger.info(`📱 Artisan WDA: Signed artifact found for ${device.udid}`);
|
|
180
|
+
yield (0, device_service_1.updateDeviceProgress)(device.udid, device.host, 'Provisioning WDA artifact...');
|
|
181
|
+
if (device.derivedDataPath) {
|
|
182
|
+
try {
|
|
183
|
+
const fs = yield Promise.resolve().then(() => __importStar(require('fs-extra')));
|
|
184
|
+
if (fs.existsSync(device.derivedDataPath)) {
|
|
185
|
+
yield fs.remove(device.derivedDataPath);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch (e) {
|
|
189
|
+
this.logger.warn(`⚠️ Artisan WDA: Failed to clear cache: ${e.message}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
const streamInfo = yield streamService.startStream(device.udid);
|
|
194
|
+
const wdaUrl = `http://127.0.0.1:${streamInfo.wdaPort}`;
|
|
195
|
+
yield (0, device_service_1.updateDeviceProgress)(device.udid, device.host, 'WDA active, finalizing session...');
|
|
196
|
+
yield new Promise((resolve) => setTimeout(resolve, 2000));
|
|
197
|
+
const bundleId = yield streamService.detectWDABundleId(device.udid);
|
|
198
|
+
this.injectWDAUrl(caps, wdaUrl, bundleId);
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
this.logger.warn(`⚠️ Artisan WDA: Pre-session boot failed: ${err.message}`);
|
|
202
|
+
yield (0, device_service_1.updateDeviceProgress)(device.udid, device.host, 'Provisioning failed, falling back...');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else if (isWdaActive) {
|
|
206
|
+
const wdaUrl = `http://127.0.0.1:${streamStatus.wdaPort}`;
|
|
207
|
+
this.injectWDAUrl(caps, wdaUrl);
|
|
208
|
+
}
|
|
209
|
+
const hasWdaUrl = lodash_1.default.has(caps.alwaysMatch, 'appium:webDriverAgentUrl') ||
|
|
210
|
+
lodash_1.default.has(caps.firstMatch[0], 'appium:webDriverAgentUrl');
|
|
211
|
+
if (!hasWdaUrl) {
|
|
212
|
+
yield (0, device_service_1.updateDeviceProgress)(device.udid, device.host, 'Initializing WebDriverAgent (Xcode)...');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
else if (device.platform === 'android') {
|
|
216
|
+
yield (0, device_service_1.updateDeviceProgress)(device.udid, device.host, 'Initializing UIAutomator2...');
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
injectWDAUrl(caps, wdaUrl, bundleId) {
|
|
221
|
+
if (caps.alwaysMatch) {
|
|
222
|
+
caps.alwaysMatch['appium:webDriverAgentUrl'] = wdaUrl;
|
|
223
|
+
caps.alwaysMatch['appium:usePreinstalledWDA'] = true;
|
|
224
|
+
if (bundleId)
|
|
225
|
+
caps.alwaysMatch['appium:updatedWDABundleId'] = bundleId;
|
|
226
|
+
delete caps.alwaysMatch['appium:derivedDataPath'];
|
|
227
|
+
delete caps.alwaysMatch['appium:usePrebuiltWDA'];
|
|
228
|
+
delete caps.alwaysMatch['appium:wdaLocalPort'];
|
|
229
|
+
delete caps.alwaysMatch['appium:mjpegServerPort'];
|
|
230
|
+
}
|
|
231
|
+
if (caps.firstMatch && caps.firstMatch[0]) {
|
|
232
|
+
caps.firstMatch[0]['appium:webDriverAgentUrl'] = wdaUrl;
|
|
233
|
+
caps.firstMatch[0]['appium:usePreinstalledWDA'] = true;
|
|
234
|
+
if (bundleId)
|
|
235
|
+
caps.firstMatch[0]['appium:updatedWDABundleId'] = bundleId;
|
|
236
|
+
delete caps.firstMatch[0]['appium:derivedDataPath'];
|
|
237
|
+
delete caps.firstMatch[0]['appium:usePrebuiltWDA'];
|
|
238
|
+
delete caps.firstMatch[0]['appium:wdaLocalPort'];
|
|
239
|
+
delete caps.firstMatch[0]['appium:mjpegServerPort'];
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
finalizeSession(session, device, caps, driver, isRemote) {
|
|
243
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
244
|
+
var _a;
|
|
245
|
+
const sessionId = session.value[0];
|
|
246
|
+
const sessionResponse = session.value[1];
|
|
247
|
+
const requestedCaps = Object.assign({}, ((_a = caps.firstMatch) === null || _a === void 0 ? void 0 : _a[0]) || {}, caps.alwaysMatch || {});
|
|
248
|
+
sessionResponse.desired = requestedCaps;
|
|
249
|
+
const xenonCapabilities = (0, XenonCapabilityManager_1.getXenonCapabilities)(caps);
|
|
250
|
+
const tracingService = typedi_1.Container.get(TracingService_1.TracingService);
|
|
251
|
+
const context = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
252
|
+
if (this.isHub(context.pluginArgs)) {
|
|
253
|
+
const sessionName = (xenonCapabilities[XenonCapabilityManager_1.XENON_CAPABILITIES.SESSION_NAME] ||
|
|
254
|
+
sessionId);
|
|
255
|
+
tracingService.startSessionSpan(sessionId, sessionName, {
|
|
256
|
+
'xenon.build_name': xenonCapabilities[XenonCapabilityManager_1.XENON_CAPABILITIES.BUILD_NAME],
|
|
257
|
+
'xenon.platform': device.platform,
|
|
258
|
+
'xenon.udid': device.udid,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
yield (0, device_service_1.updatedAllocatedDevice)(device, {
|
|
262
|
+
busy: true,
|
|
263
|
+
session_id: sessionId,
|
|
264
|
+
lastCmdExecutedAt: new Date().getTime(),
|
|
265
|
+
sessionStartTime: new Date().getTime(),
|
|
266
|
+
sessionProgress: 'Session Active',
|
|
267
|
+
});
|
|
268
|
+
if (isRemote) {
|
|
269
|
+
typedi_1.Container.get(CircuitBreaker_1.CircuitBreaker).recordSuccess(device.host);
|
|
270
|
+
(0, wd_command_proxy_1.addProxyHandler)(sessionId, device.host);
|
|
271
|
+
}
|
|
272
|
+
const freshDevice = yield this.getFreshDevice(device);
|
|
273
|
+
const sessionInstance = this.createSessionInstance(sessionId, freshDevice, sessionResponse, xenonCapabilities, driver);
|
|
274
|
+
yield this.applyPostSessionLogic(sessionInstance, xenonCapabilities, freshDevice);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
getFreshDevice(device) {
|
|
278
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
279
|
+
try {
|
|
280
|
+
const store = device_store_1.DeviceStoreFactory.getStore();
|
|
281
|
+
const updatedDevice = yield store.findDevice({ udid: device.udid, host: device.host });
|
|
282
|
+
return updatedDevice || device;
|
|
283
|
+
}
|
|
284
|
+
catch (err) {
|
|
285
|
+
this.logger.debug(`📱 Could not refresh device: ${err.message}`);
|
|
286
|
+
return device;
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
createSessionInstance(sessionId, device, response, caps, driver) {
|
|
291
|
+
const context = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
292
|
+
const sessionOptions = {
|
|
293
|
+
sessionId,
|
|
294
|
+
device,
|
|
295
|
+
sessionResponse: response,
|
|
296
|
+
xenonOption: caps,
|
|
297
|
+
};
|
|
298
|
+
const nodeWebdriverUrl = (0, helpers_1.nodeUrl)(device, context.nodeBasePath);
|
|
299
|
+
if (device.nodeId === context.nodeId) {
|
|
300
|
+
return new LocalSession_1.LocalSession(Object.assign(Object.assign({}, sessionOptions), { driver }));
|
|
301
|
+
}
|
|
302
|
+
else if (device.cloud) {
|
|
303
|
+
return new CloudSession_1.CloudSession(Object.assign(Object.assign({}, sessionOptions), { baseUrl: nodeWebdriverUrl }));
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
return new RemoteSession_1.RemoteSession(Object.assign(Object.assign({}, sessionOptions), { baseUrl: nodeWebdriverUrl }));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
applyPostSessionLogic(session, caps, device) {
|
|
310
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
311
|
+
const context = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
312
|
+
const networkProfile = caps[XenonCapabilityManager_1.XENON_CAPABILITIES.NETWORK_PROFILE];
|
|
313
|
+
if (networkProfile) {
|
|
314
|
+
const { NetworkConditioningService } = yield Promise.resolve().then(() => __importStar(require('./NetworkConditioningService')));
|
|
315
|
+
yield typedi_1.Container.get(NetworkConditioningService).applyProfile(session.getId(), device, networkProfile);
|
|
316
|
+
}
|
|
317
|
+
const isDashboardEnabled = !!context.pluginArgs.enableDashboard;
|
|
318
|
+
const shouldSaveLogs = session.getType() !== SessionType_1.default.CLOUD;
|
|
319
|
+
const isVideoRecordingEnabled = caps[XenonCapabilityManager_1.XENON_CAPABILITIES.VIDEO_RECORDING];
|
|
320
|
+
this.logger.info(`📹 Video recording enabled for session ${session.getId()}: ${isVideoRecordingEnabled}`);
|
|
321
|
+
if (isVideoRecordingEnabled) {
|
|
322
|
+
const resolution = caps[XenonCapabilityManager_1.XENON_CAPABILITIES.VIDEO_RESOLUTION] || undefined;
|
|
323
|
+
try {
|
|
324
|
+
this.logger.info(`📹 Starting video recording for session ${session.getId()}...`);
|
|
325
|
+
yield session.startVideoRecording({ resolution });
|
|
326
|
+
this.logger.info(`📹 Video recording started successfully for ${session.getId()}`);
|
|
327
|
+
}
|
|
328
|
+
catch (err) {
|
|
329
|
+
this.logger.warn(`⚠️ Failed to start video recording for ${session.getId()}: ${err.message}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if ((isDashboardEnabled && shouldSaveLogs) || (isVideoRecordingEnabled && shouldSaveLogs)) {
|
|
333
|
+
SessionManager_1.SESSION_MANAGER.addSession(session.getId(), session);
|
|
334
|
+
if (this.isHub(context.pluginArgs) && isDashboardEnabled) {
|
|
335
|
+
yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStarted(caps, session, device);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
handleSessionFailure(session, device, isRemote) {
|
|
341
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
342
|
+
yield (0, device_service_1.unblockDevice)(device.udid, device.host);
|
|
343
|
+
yield (0, device_service_1.updateDeviceProgress)(device.udid, device.host, '');
|
|
344
|
+
if (isRemote) {
|
|
345
|
+
typedi_1.Container.get(CircuitBreaker_1.CircuitBreaker).recordFailure(device.host);
|
|
346
|
+
}
|
|
347
|
+
this.throwProperError(session, device.host);
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
forwardSessionRequest(device, caps) {
|
|
351
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
352
|
+
const context = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
353
|
+
const remoteUrl = `${(0, helpers_1.nodeUrl)(device, context.nodeBasePath)}/session`;
|
|
354
|
+
const config = {
|
|
355
|
+
method: 'post',
|
|
356
|
+
url: remoteUrl,
|
|
357
|
+
headers: { 'Content-Type': 'application/json' },
|
|
358
|
+
data: { capabilities: caps },
|
|
359
|
+
};
|
|
360
|
+
if (context.pluginArgs.proxy) {
|
|
361
|
+
this.logger.info(`Added proxy to axios config: ${JSON.stringify(context.pluginArgs.proxy)}`);
|
|
362
|
+
const proxyUrl = typeof context.pluginArgs.proxy === 'string'
|
|
363
|
+
? context.pluginArgs.proxy
|
|
364
|
+
: `http://${context.pluginArgs.proxy.host}:${context.pluginArgs.proxy.port}`;
|
|
365
|
+
config.httpsAgent = new https_proxy_agent_1.HttpsProxyAgent(proxyUrl, {
|
|
366
|
+
rejectUnauthorized: context.pluginArgs.tlsRejectUnauthorized,
|
|
367
|
+
});
|
|
368
|
+
config.httpAgent = new http_proxy_agent_1.HttpProxyAgent(proxyUrl, {
|
|
369
|
+
rejectUnauthorized: context.pluginArgs.tlsRejectUnauthorized,
|
|
370
|
+
});
|
|
371
|
+
config.proxy = false;
|
|
372
|
+
}
|
|
373
|
+
const createdSession = yield this.invokeSessionRequest(config, context.pluginArgs.tlsRejectUnauthorized);
|
|
374
|
+
if (createdSession instanceof Error) {
|
|
375
|
+
return createdSession;
|
|
376
|
+
}
|
|
377
|
+
// Detect W3C-style error payloads that invokeSessionRequest returned as data
|
|
378
|
+
const val = createdSession === null || createdSession === void 0 ? void 0 : createdSession.value;
|
|
379
|
+
if (Object.prototype.hasOwnProperty.call(createdSession, 'error') ||
|
|
380
|
+
(val && val.error) ||
|
|
381
|
+
(val && typeof val.message === 'string' && !val.sessionId)) {
|
|
382
|
+
const errorDetail = createdSession.error || (val === null || val === void 0 ? void 0 : val.error) || (val === null || val === void 0 ? void 0 : val.message) || 'Unknown W3C error';
|
|
383
|
+
return new Error(`W3C session creation failed on ${device.host}: ${typeof errorDetail === 'object' ? JSON.stringify(errorDetail) : errorDetail}`);
|
|
384
|
+
}
|
|
385
|
+
// Only build the success tuple for genuine W3C success payloads
|
|
386
|
+
if (!(val === null || val === void 0 ? void 0 : val.sessionId)) {
|
|
387
|
+
return new Error(`Invalid session response from ${device.host}: missing sessionId`);
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
protocol: 'W3C',
|
|
391
|
+
value: [val.sessionId, val.capabilities, 'W3C'],
|
|
392
|
+
};
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
invokeSessionRequest(config, tlsRejectUnauthorized) {
|
|
396
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
397
|
+
try {
|
|
398
|
+
const client = InternalHttpClient_1.InternalHttpClient.getClient(tlsRejectUnauthorized);
|
|
399
|
+
const response = yield client.request(config);
|
|
400
|
+
return response.data;
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
if (axios_1.default.isAxiosError(error)) {
|
|
404
|
+
const axiosError = error;
|
|
405
|
+
if (axiosError.response) {
|
|
406
|
+
return axiosError.response.data;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return error;
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
isW3CNewSessionResponse(something) {
|
|
414
|
+
return (something &&
|
|
415
|
+
typeof something === 'object' &&
|
|
416
|
+
(Object.prototype.hasOwnProperty.call(something, 'value') ||
|
|
417
|
+
Object.prototype.hasOwnProperty.call(something, 'error')));
|
|
418
|
+
}
|
|
419
|
+
isCreateSessionResponseInternal(something) {
|
|
420
|
+
return (something &&
|
|
421
|
+
typeof something === 'object' &&
|
|
422
|
+
Object.prototype.hasOwnProperty.call(something, 'value') &&
|
|
423
|
+
Array.isArray(something.value) &&
|
|
424
|
+
(something.value.length === 2 || something.value.length === 3) &&
|
|
425
|
+
typeof something.value[0] === 'string' &&
|
|
426
|
+
typeof something.value[1] === 'object' &&
|
|
427
|
+
(something.value.length === 2 ||
|
|
428
|
+
typeof something.value[2] === 'string' ||
|
|
429
|
+
something.value[2] === undefined));
|
|
430
|
+
}
|
|
431
|
+
throwProperError(session, host) {
|
|
432
|
+
if (session instanceof Error) {
|
|
433
|
+
throw session;
|
|
434
|
+
}
|
|
435
|
+
else if (session &&
|
|
436
|
+
typeof session === 'object' &&
|
|
437
|
+
Object.prototype.hasOwnProperty.call(session, 'error')) {
|
|
438
|
+
let errorMessage = session.error;
|
|
439
|
+
if (typeof errorMessage === 'object') {
|
|
440
|
+
errorMessage = JSON.stringify(errorMessage);
|
|
441
|
+
}
|
|
442
|
+
throw new Error(`Failed to create session on node ${host}. Error: ${errorMessage}`);
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
throw new Error(`Failed to create session on node ${host}. Unknown error.`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
getLockName(caps) {
|
|
449
|
+
var _a, _b, _c, _d;
|
|
450
|
+
const platform = (_d = (((_a = caps.alwaysMatch) === null || _a === void 0 ? void 0 : _a.platformName) || ((_c = (_b = caps.firstMatch) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.platformName))) === null || _d === void 0 ? void 0 : _d.toLowerCase();
|
|
451
|
+
if (platform === 'ios')
|
|
452
|
+
return 'ios-lock';
|
|
453
|
+
if (platform === 'android')
|
|
454
|
+
return 'android-lock';
|
|
455
|
+
return 'default-lock';
|
|
456
|
+
}
|
|
457
|
+
deleteSession(next, sessionId, status, reason) {
|
|
458
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
459
|
+
if (sessionId) {
|
|
460
|
+
yield (0, device_service_1.unblockDeviceMatchingFilter)({ session_id: sessionId });
|
|
461
|
+
this.logger.info(`📱 Unblocking the device that is blocked for session ${sessionId}`);
|
|
462
|
+
}
|
|
463
|
+
const session = sessionId ? SessionManager_1.SESSION_MANAGER.getSession(sessionId) : undefined;
|
|
464
|
+
if (session) {
|
|
465
|
+
session.isStopping = true;
|
|
466
|
+
session.stoppedAt = Date.now();
|
|
467
|
+
yield this.finalizeCleanup(session, status, reason);
|
|
468
|
+
}
|
|
469
|
+
let timeoutId;
|
|
470
|
+
try {
|
|
471
|
+
// Principal Protection: Wrap the driver deletion with a timeout.
|
|
472
|
+
// If the underlying driver (WDA/ADB) hangs during shutdown, we don't want
|
|
473
|
+
// our session lifecycle to be stuck 'RUNNING' forever.
|
|
474
|
+
const deletionTimeout = new Promise((_, reject) => {
|
|
475
|
+
timeoutId = setTimeout(() => reject(new Error('Deletion timed out')), 30000);
|
|
476
|
+
});
|
|
477
|
+
const response = yield Promise.race([next(), deletionTimeout]);
|
|
478
|
+
clearTimeout(timeoutId);
|
|
479
|
+
return response;
|
|
480
|
+
}
|
|
481
|
+
catch (err) {
|
|
482
|
+
clearTimeout(timeoutId);
|
|
483
|
+
this.logger.warn(`⚠️ Internal deleteSession failed or timed out: ${err.message}. Continuing cleanup anyway.`);
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
finally {
|
|
487
|
+
if (sessionId) {
|
|
488
|
+
const session = SessionManager_1.SESSION_MANAGER.getSession(sessionId);
|
|
489
|
+
if (session) {
|
|
490
|
+
const device = session.getDevice();
|
|
491
|
+
try {
|
|
492
|
+
const { NetworkConditioningService } = yield Promise.resolve().then(() => __importStar(require('./NetworkConditioningService')));
|
|
493
|
+
yield typedi_1.Container.get(NetworkConditioningService).reset(sessionId, device);
|
|
494
|
+
}
|
|
495
|
+
catch (resetErr) {
|
|
496
|
+
this.logger.warn(`⚠️ NetworkConditioningService.reset failed for session ${sessionId}: ${resetErr.message}`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStopped(sessionId, status, reason);
|
|
500
|
+
SessionManager_1.SESSION_MANAGER.removeSession(sessionId);
|
|
501
|
+
try {
|
|
502
|
+
const { getSessionById } = yield Promise.resolve().then(() => __importStar(require('../dashboard/services/session-service')));
|
|
503
|
+
const sessionData = yield getSessionById(sessionId);
|
|
504
|
+
if (sessionData && (sessionData.status === 'failed' || sessionData.failure_reason)) {
|
|
505
|
+
const { NotificationService } = yield Promise.resolve().then(() => __importStar(require('./NotificationService')));
|
|
506
|
+
yield typedi_1.Container.get(NotificationService).dispatchEvent('session_failed', sessionData);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
catch (err) {
|
|
510
|
+
/* ignore notification errors */
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
finalizeCleanup(session, status, reason) {
|
|
517
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
518
|
+
var _a;
|
|
519
|
+
const sessionId = session.getId();
|
|
520
|
+
const device = session.getDevice();
|
|
521
|
+
// 1. iOS Profiling Archival
|
|
522
|
+
if (device && ((_a = device.platform) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'ios') {
|
|
523
|
+
this.logger.info(`[${sessionId}] Stopping iOS profiling for asset archival`);
|
|
524
|
+
try {
|
|
525
|
+
const traceBase64 = yield session.stopPerformanceRecording();
|
|
526
|
+
if (traceBase64) {
|
|
527
|
+
const { savePerformanceTrace } = yield Promise.resolve().then(() => __importStar(require('../dashboard/asset-manager')));
|
|
528
|
+
const tracePath = savePerformanceTrace(sessionId, traceBase64);
|
|
529
|
+
yield (0, session_service_1.updateSessionDetails)(sessionId, { performance_trace: tracePath });
|
|
530
|
+
this.logger.info(`✅ [${sessionId}] iOS profiling trace saved at ${tracePath}`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
catch (err) {
|
|
534
|
+
this.logger.warn(`⚠️ [${sessionId}] iOS profiling capture failed: ${err.message}`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
// 2. Intelligent Video Archival
|
|
538
|
+
if (session.isVideoRecordingInProgress()) {
|
|
539
|
+
this.logger.info(`[${sessionId}] Stopping video recording for asset archival`);
|
|
540
|
+
try {
|
|
541
|
+
const videoData = yield session.stopVideoRecording();
|
|
542
|
+
if (videoData) {
|
|
543
|
+
try {
|
|
544
|
+
const { saveVideoRecording } = yield Promise.resolve().then(() => __importStar(require('../dashboard/asset-manager')));
|
|
545
|
+
let videoPath = videoData;
|
|
546
|
+
// Principal Efficiency: If it's a relative path from our pipeline, use it.
|
|
547
|
+
// If it's base64 (older Appium drivers), save it to disk.
|
|
548
|
+
if (videoData.length > 1000) {
|
|
549
|
+
videoPath = saveVideoRecording(sessionId, videoData);
|
|
550
|
+
}
|
|
551
|
+
yield (0, session_service_1.updateSessionDetails)(sessionId, { video_recording: videoPath });
|
|
552
|
+
this.logger.info(`✅ [${sessionId}] Video recording archived at ${videoPath}`);
|
|
553
|
+
}
|
|
554
|
+
catch (saveErr) {
|
|
555
|
+
this.logger.error(`❌ [${sessionId}] Failed to process video asset: ${saveErr.message}`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
catch (error) {
|
|
560
|
+
this.logger.warn(`⚠️ [${sessionId}] Failed to stop video recording: ${error.message}`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
isHub(args) {
|
|
566
|
+
return !args.hub;
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
exports.SessionLifecycleService = SessionLifecycleService;
|
|
570
|
+
exports.SessionLifecycleService = SessionLifecycleService = __decorate([
|
|
571
|
+
(0, typedi_1.Service)()
|
|
572
|
+
], SessionLifecycleService);
|
|
@@ -0,0 +1,71 @@
|
|
|
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 __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.SocketClient = void 0;
|
|
13
|
+
const socket_io_client_1 = require("socket.io-client");
|
|
14
|
+
const typedi_1 = require("typedi");
|
|
15
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
16
|
+
const SocketEvents_1 = require("../enums/SocketEvents");
|
|
17
|
+
let SocketClient = class SocketClient {
|
|
18
|
+
constructor() {
|
|
19
|
+
this.socket = null;
|
|
20
|
+
this.hubUrl = null;
|
|
21
|
+
this.nodeHost = null;
|
|
22
|
+
}
|
|
23
|
+
initialize(hubUrl, nodeHost) {
|
|
24
|
+
this.hubUrl = hubUrl;
|
|
25
|
+
this.nodeHost = nodeHost;
|
|
26
|
+
// Remove wd/hub if present in hubUrl
|
|
27
|
+
const normalizedHubUrl = hubUrl.replace(/\/wd\/hub$/, '');
|
|
28
|
+
logger_1.default.info(`[SocketClient] Connecting to Hub WebSocket: ${normalizedHubUrl}`);
|
|
29
|
+
this.socket = (0, socket_io_client_1.io)(normalizedHubUrl, {
|
|
30
|
+
reconnection: true,
|
|
31
|
+
reconnectionAttempts: Infinity,
|
|
32
|
+
reconnectionDelay: 1000,
|
|
33
|
+
reconnectionDelayMax: 5000,
|
|
34
|
+
});
|
|
35
|
+
this.socket.on('connect', () => {
|
|
36
|
+
var _a, _b, _c;
|
|
37
|
+
logger_1.default.info(`[SocketClient] Connected to Hub: ${(_a = this.socket) === null || _a === void 0 ? void 0 : _a.id}`);
|
|
38
|
+
// 1. Send Handshake
|
|
39
|
+
(_b = this.socket) === null || _b === void 0 ? void 0 : _b.emit(SocketEvents_1.SocketEvents.HANDSHAKE, {
|
|
40
|
+
version: SocketEvents_1.XENON_PROTOCOL_VERSION,
|
|
41
|
+
host: this.nodeHost,
|
|
42
|
+
timestamp: Date.now(),
|
|
43
|
+
});
|
|
44
|
+
// 2. Register Node
|
|
45
|
+
(_c = this.socket) === null || _c === void 0 ? void 0 : _c.emit(SocketEvents_1.SocketEvents.REGISTER_NODE, { host: this.nodeHost });
|
|
46
|
+
});
|
|
47
|
+
this.socket.on('disconnect', (reason) => {
|
|
48
|
+
logger_1.default.warn(`[SocketClient] Disconnected from Hub: ${reason}`);
|
|
49
|
+
});
|
|
50
|
+
this.socket.on('connect_error', (error) => {
|
|
51
|
+
logger_1.default.error(`[SocketClient] Connection error: ${error.message}`);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
emit(event, data) {
|
|
55
|
+
var _a;
|
|
56
|
+
if ((_a = this.socket) === null || _a === void 0 ? void 0 : _a.connected) {
|
|
57
|
+
this.socket.emit(event, data);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
logger_1.default.debug(`[SocketClient] Cannot emit ${event}: Socket not connected`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
isConnected() {
|
|
64
|
+
var _a;
|
|
65
|
+
return ((_a = this.socket) === null || _a === void 0 ? void 0 : _a.connected) || false;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
exports.SocketClient = SocketClient;
|
|
69
|
+
exports.SocketClient = SocketClient = __decorate([
|
|
70
|
+
(0, typedi_1.Service)()
|
|
71
|
+
], SocketClient);
|