@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,699 @@
|
|
|
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.DASHBORD_EVENT_MANAGER = exports.DashboardEventManager = void 0;
|
|
55
|
+
const SessionManager_1 = require("../sessions/SessionManager");
|
|
56
|
+
const prisma_1 = require("../prisma");
|
|
57
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
58
|
+
const session_service_1 = require("./services/session-service");
|
|
59
|
+
const XenonCapabilityManager_1 = require("../XenonCapabilityManager");
|
|
60
|
+
const lodash_1 = __importDefault(require("lodash"));
|
|
61
|
+
const helpers_1 = require("../helpers");
|
|
62
|
+
const asset_manager_1 = require("./asset-manager");
|
|
63
|
+
const commands_1 = require("./commands");
|
|
64
|
+
const SessionStatus_1 = require("../types/SessionStatus");
|
|
65
|
+
const appium_ios_device_1 = require("appium-ios-device");
|
|
66
|
+
const AndroidAppProfiler_1 = require("../profiling/AndroidAppProfiler");
|
|
67
|
+
const appium_adb_1 = require("appium-adb");
|
|
68
|
+
const config_1 = require("../config");
|
|
69
|
+
const helpers_2 = require("../helpers");
|
|
70
|
+
const typedi_1 = require("typedi");
|
|
71
|
+
const device_managers_1 = require("../device-managers");
|
|
72
|
+
const AndroidDeviceManager_1 = __importDefault(require("../device-managers/AndroidDeviceManager"));
|
|
73
|
+
const SocketServer_1 = require("../services/SocketServer");
|
|
74
|
+
const TracingService_1 = require("../services/TracingService");
|
|
75
|
+
const MetricsService_1 = require("../services/MetricsService");
|
|
76
|
+
const SocketEvents_1 = require("../enums/SocketEvents");
|
|
77
|
+
const typedi_2 = require("typedi");
|
|
78
|
+
let DashboardEventManager = class DashboardEventManager {
|
|
79
|
+
constructor() {
|
|
80
|
+
// private SCREENSHOT_FOR_COMMANDS = ['click', 'setUrl', 'setValue', 'performActions'];
|
|
81
|
+
// Store syslog services for real iOS devices
|
|
82
|
+
this.syslogServices = new Map();
|
|
83
|
+
// Map session ID to UDID for cleanup
|
|
84
|
+
this.sessionToUdid = new Map();
|
|
85
|
+
// Map session ID to device info
|
|
86
|
+
this.sessionToDevice = new Map();
|
|
87
|
+
// Store app profilers for Android sessions
|
|
88
|
+
this.appProfilers = new Map();
|
|
89
|
+
// Track last log line for each session (for device logs)
|
|
90
|
+
this.lastLogLine = new Map();
|
|
91
|
+
// Track start time for each command to calculate duration
|
|
92
|
+
this.commandStartTime = new Map();
|
|
93
|
+
// Idempotency Guard: Prevents double-invocation of onSessionStopped by racing actors
|
|
94
|
+
this.stoppingSessionIds = new Set();
|
|
95
|
+
}
|
|
96
|
+
onSessionStarted(capabilities, session, device) {
|
|
97
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
98
|
+
// Store device info for this session
|
|
99
|
+
this.sessionToDevice.set(session.getId(), device);
|
|
100
|
+
const createOptions = {
|
|
101
|
+
id: session.getId(),
|
|
102
|
+
};
|
|
103
|
+
// create directory to store screenshots, videos and log files for the session
|
|
104
|
+
(0, asset_manager_1.prepareDirectory)(session.getId());
|
|
105
|
+
// Initialize app profiling for Android sessions
|
|
106
|
+
const { is_profiling_available, device_info } = yield this.startAppProfiling(session.getId(), device, session.getCapabilities());
|
|
107
|
+
// If iOS real device, start performance recording (Time Profiler)
|
|
108
|
+
// Note: This only works on real devices with XCUITest driver 4.5+
|
|
109
|
+
let isIosProfilingStarted = false;
|
|
110
|
+
if (device.platform.toLowerCase() === 'ios' && device.realDevice === true) {
|
|
111
|
+
logger_1.default.info(`[Profiling] Starting iOS performance recording for session ${session.getId()}`);
|
|
112
|
+
try {
|
|
113
|
+
yield session.startPerformanceRecording();
|
|
114
|
+
isIosProfilingStarted = true;
|
|
115
|
+
logger_1.default.info(`[Profiling] ✅ iOS performance recording started for ${session.getId()}`);
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
logger_1.default.warn(`[Profiling] Failed to start iOS performance recording: ${err.message}`);
|
|
119
|
+
// Not a fatal error - profiling is optional
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// start video recording is now handled in plugin.ts createSession to avoid double calls
|
|
123
|
+
const videoMsg = `📹 Video recording capability for session ${session.getId()}: ${capabilities[XenonCapabilityManager_1.XENON_CAPABILITIES.VIDEO_RECORDING]}`;
|
|
124
|
+
logger_1.default.info(videoMsg);
|
|
125
|
+
const buildName = capabilities[XenonCapabilityManager_1.XENON_CAPABILITIES.BUILD_NAME] || 'Default Build';
|
|
126
|
+
const build = yield (0, session_service_1.getOrCreateNewBuild)(buildName);
|
|
127
|
+
const sessionResponse = lodash_1.default.assign({}, session.getCapabilities());
|
|
128
|
+
const tracingService = typedi_1.Container.get(TracingService_1.TracingService);
|
|
129
|
+
const traceId = tracingService.getTraceId(session.getId());
|
|
130
|
+
const createData = {
|
|
131
|
+
id: session.getId(),
|
|
132
|
+
build: build.id ? { connect: { id: build.id } } : undefined,
|
|
133
|
+
name: capabilities[XenonCapabilityManager_1.XENON_CAPABILITIES.SESSION_NAME] || undefined,
|
|
134
|
+
desired_capabilities: JSON.stringify(sessionResponse.desired || {}),
|
|
135
|
+
session_capabilities: JSON.stringify(lodash_1.default.omit(sessionResponse, 'desired')),
|
|
136
|
+
node_id: device.nodeId || '',
|
|
137
|
+
has_live_video: session.getLiveVideoUrl() !== null,
|
|
138
|
+
video_recording_enabled: capabilities[XenonCapabilityManager_1.XENON_CAPABILITIES.VIDEO_RECORDING] === true,
|
|
139
|
+
is_profiling_available: is_profiling_available || isIosProfilingStarted,
|
|
140
|
+
device_info: device_info ? JSON.stringify(device_info) : null,
|
|
141
|
+
device_udid: device.udid || '',
|
|
142
|
+
device_platform: device.platform || '',
|
|
143
|
+
device_version: device.sdk || '',
|
|
144
|
+
device_name: device.name,
|
|
145
|
+
trace_id: traceId,
|
|
146
|
+
status: 'running', // Principal Polish: Set status explicitly
|
|
147
|
+
};
|
|
148
|
+
yield prisma_1.prisma.session.create({
|
|
149
|
+
data: createData,
|
|
150
|
+
});
|
|
151
|
+
// Emit session started event
|
|
152
|
+
typedi_1.Container.get(SocketServer_1.SocketServer).emitToDashboard(SocketEvents_1.SocketEvents.SESSION_STARTED, Object.assign(Object.assign({}, createData), { status: 'running', build_name: buildName }));
|
|
153
|
+
// Increment Metrics
|
|
154
|
+
typedi_1.Container.get(MetricsService_1.MetricsService).incrementSessionStart();
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
onSessionStopped(sessionId, status, failureReason) {
|
|
158
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
159
|
+
var _a, _b, _c, _d;
|
|
160
|
+
// Idempotency Guard: If this session is already being stopped by another actor
|
|
161
|
+
// (heartbeat, stream watchdog, plugin.deleteSession), skip to avoid double-cleanup.
|
|
162
|
+
if (this.stoppingSessionIds.has(sessionId)) {
|
|
163
|
+
logger_1.default.info(`⏭️ onSessionStopped already in progress for ${sessionId}. Skipping duplicate call.`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
this.stoppingSessionIds.add(sessionId);
|
|
167
|
+
try {
|
|
168
|
+
logger_1.default.info(`🟢 onSessionStopped called for session ${sessionId}`);
|
|
169
|
+
// Video recording is now handled in plugin.ts deleteSession() before the session is deleted
|
|
170
|
+
// This ensures we can call stop_recording_screen while the session is still active
|
|
171
|
+
// Here we just handle the session status update
|
|
172
|
+
const session = SessionManager_1.SESSION_MANAGER.getSession(sessionId);
|
|
173
|
+
if (session) {
|
|
174
|
+
logger_1.default.info(`Session ${sessionId} found in SESSION_MANAGER`);
|
|
175
|
+
// Save Android profiling data before cleanup
|
|
176
|
+
yield this.saveAppProfilingData(sessionId);
|
|
177
|
+
// iOS profiling is now handled in plugin.ts deleteSession() before the session is deleted
|
|
178
|
+
// This ensures we can call mobile: stopPerfRecord while the driver is still alive
|
|
179
|
+
// Clean up syslog service for real iOS devices
|
|
180
|
+
const udid = this.sessionToUdid.get(sessionId);
|
|
181
|
+
if (udid) {
|
|
182
|
+
try {
|
|
183
|
+
this.syslogServices.delete(udid);
|
|
184
|
+
this.sessionToUdid.delete(sessionId);
|
|
185
|
+
logger_1.default.info(`Cleaned up syslog service for device ${udid}`);
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
logger_1.default.debug(`Error cleaning up syslog service info: ${err}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Clean up last log line tracking
|
|
192
|
+
this.lastLogLine.delete(sessionId);
|
|
193
|
+
// Principal Resource Management: Unblock device immediately
|
|
194
|
+
const device = this.sessionToDevice.get(sessionId);
|
|
195
|
+
if (device) {
|
|
196
|
+
const { unblockDevice } = yield Promise.resolve().then(() => __importStar(require('../data-service/device-service')));
|
|
197
|
+
try {
|
|
198
|
+
yield unblockDevice(device.udid, device.host);
|
|
199
|
+
logger_1.default.info(`🔓 [${sessionId}] Device ${device.udid} released.`);
|
|
200
|
+
}
|
|
201
|
+
catch (unblockErr) {
|
|
202
|
+
const msg = (_a = unblockErr === null || unblockErr === void 0 ? void 0 : unblockErr.message) !== null && _a !== void 0 ? _a : String(unblockErr);
|
|
203
|
+
logger_1.default.error(`⚠️ Failed to unblock device ${device.udid} for session ${sessionId}: ${msg}`, unblockErr);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
const { unblockDeviceMatchingFilter } = yield Promise.resolve().then(() => __importStar(require('../data-service/device-service')));
|
|
208
|
+
try {
|
|
209
|
+
yield unblockDeviceMatchingFilter({ session_id: sessionId });
|
|
210
|
+
logger_1.default.info(`🔓 [${sessionId}] Device released via session_id fallback.`);
|
|
211
|
+
}
|
|
212
|
+
catch (unblockErr) {
|
|
213
|
+
const msg = (_b = unblockErr === null || unblockErr === void 0 ? void 0 : unblockErr.message) !== null && _b !== void 0 ? _b : String(unblockErr);
|
|
214
|
+
logger_1.default.error(`⚠️ Failed to unblock device for session ${sessionId}: ${msg}`, unblockErr);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// Final local cleanup to prevent state leaks
|
|
218
|
+
this.sessionToDevice.delete(sessionId);
|
|
219
|
+
this.lastLogLine.delete(sessionId);
|
|
220
|
+
const orphanUdid = this.sessionToUdid.get(sessionId);
|
|
221
|
+
if (orphanUdid) {
|
|
222
|
+
this.sessionToUdid.delete(sessionId);
|
|
223
|
+
this.syslogServices.delete(orphanUdid);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
logger_1.default.warn(`⚠️ Session ${sessionId} not found in SESSION_MANAGER`);
|
|
228
|
+
// Fallback: If session not in manager, attempt to unblock by session_id in store
|
|
229
|
+
const { unblockDeviceMatchingFilter } = yield Promise.resolve().then(() => __importStar(require('../data-service/device-service')));
|
|
230
|
+
try {
|
|
231
|
+
yield unblockDeviceMatchingFilter({ session_id: sessionId });
|
|
232
|
+
logger_1.default.info(`🔓 [${sessionId}] Orphaned device released via session_id fallback.`);
|
|
233
|
+
}
|
|
234
|
+
catch (unblockErr) {
|
|
235
|
+
const msg = (_c = unblockErr === null || unblockErr === void 0 ? void 0 : unblockErr.message) !== null && _c !== void 0 ? _c : String(unblockErr);
|
|
236
|
+
logger_1.default.error(`⚠️ Failed to unblock device for orphaned session ${sessionId}: ${msg}`, unblockErr);
|
|
237
|
+
}
|
|
238
|
+
finally {
|
|
239
|
+
this.sessionToDevice.delete(sessionId);
|
|
240
|
+
this.lastLogLine.delete(sessionId);
|
|
241
|
+
const orphanUdid = this.sessionToUdid.get(sessionId);
|
|
242
|
+
if (orphanUdid) {
|
|
243
|
+
this.sessionToUdid.delete(sessionId);
|
|
244
|
+
this.syslogServices.delete(orphanUdid);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const sessionEntry = yield (0, session_service_1.getSessionById)(sessionId);
|
|
249
|
+
if (sessionEntry) {
|
|
250
|
+
logger_1.default.info(`Session ${sessionId} current status: ${sessionEntry.status}`);
|
|
251
|
+
const updateData = {
|
|
252
|
+
endTime: new Date(),
|
|
253
|
+
has_live_video: false,
|
|
254
|
+
};
|
|
255
|
+
// Principal Intelligence: Determined final status based on command history
|
|
256
|
+
if (status) {
|
|
257
|
+
updateData['status'] = status;
|
|
258
|
+
if (failureReason)
|
|
259
|
+
updateData['failure_reason'] = failureReason;
|
|
260
|
+
}
|
|
261
|
+
else if (sessionEntry.status === SessionStatus_1.SessionStatus.RUNNING ||
|
|
262
|
+
!sessionEntry.status ||
|
|
263
|
+
sessionEntry.status === SessionStatus_1.SessionStatus.UNMARKED) {
|
|
264
|
+
// Check if any command failed in this session
|
|
265
|
+
const failedCommand = yield prisma_1.prisma.sessionLog.findFirst({
|
|
266
|
+
where: { session_id: sessionId, is_error: true },
|
|
267
|
+
orderBy: { createdAt: 'desc' },
|
|
268
|
+
});
|
|
269
|
+
if (failedCommand) {
|
|
270
|
+
updateData['status'] = SessionStatus_1.SessionStatus.FAILED;
|
|
271
|
+
updateData['failure_reason'] =
|
|
272
|
+
failedCommand.response && failedCommand.response.includes('error')
|
|
273
|
+
? ((_d = (0, helpers_1.safeParseJson)(failedCommand.response).value) === null || _d === void 0 ? void 0 : _d.error) ||
|
|
274
|
+
`Command failed: ${failedCommand.command_name}`
|
|
275
|
+
: `Command failed: ${failedCommand.command_name}`;
|
|
276
|
+
logger_1.default.info(`Session ${sessionId} marked as FAILED due to error in command: ${failedCommand.command_name}`);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
updateData['status'] = SessionStatus_1.SessionStatus.SUCCESS;
|
|
280
|
+
logger_1.default.info(`Session ${sessionId} marked as SUCCESS`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
// Principal Reliability: If the session already has a terminal status,
|
|
285
|
+
// ensure we still use that status for metrics and events below.
|
|
286
|
+
updateData['status'] = sessionEntry.status;
|
|
287
|
+
}
|
|
288
|
+
yield (0, session_service_1.updateSessionDetails)(sessionId, updateData);
|
|
289
|
+
logger_1.default.info(`✅ Session ${sessionId} updated successfully`);
|
|
290
|
+
// 🟢 Socket Events must happen AFTER DB update and MUST include a status
|
|
291
|
+
// to ensure the UI row changes from 'RUNNING' to its final state.
|
|
292
|
+
typedi_1.Container.get(SocketServer_1.SocketServer).emitToDashboard(SocketEvents_1.SocketEvents.SESSION_STOPPED, {
|
|
293
|
+
id: sessionId,
|
|
294
|
+
status: updateData.status || sessionEntry.status || SessionStatus_1.SessionStatus.SUCCESS,
|
|
295
|
+
failure_reason: updateData.failure_reason || sessionEntry.failure_reason,
|
|
296
|
+
});
|
|
297
|
+
// Principal Analytics: Increment Metrics AFTER emission
|
|
298
|
+
if (updateData.status === SessionStatus_1.SessionStatus.SUCCESS) {
|
|
299
|
+
typedi_1.Container.get(MetricsService_1.MetricsService).incrementSessionSuccess();
|
|
300
|
+
}
|
|
301
|
+
else if (updateData.status === SessionStatus_1.SessionStatus.FAILED) {
|
|
302
|
+
typedi_1.Container.get(MetricsService_1.MetricsService).incrementSessionFailure();
|
|
303
|
+
}
|
|
304
|
+
// Principal Triage: If session failed, perform intelligent failure analysis
|
|
305
|
+
if (updateData.status === SessionStatus_1.SessionStatus.FAILED) {
|
|
306
|
+
try {
|
|
307
|
+
const { analyzeSessionFailure } = yield Promise.resolve().then(() => __importStar(require('./services/failure-analysis-service')));
|
|
308
|
+
yield analyzeSessionFailure(sessionId);
|
|
309
|
+
}
|
|
310
|
+
catch (analysisErr) {
|
|
311
|
+
logger_1.default.warn(`⚠️ Failure analysis skipped for ${sessionId}: ${analysisErr.message}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
logger_1.default.warn(`⚠️ Session ${sessionId} not found in database`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
finally {
|
|
320
|
+
// Always release the idempotency lock so future cleanup calls
|
|
321
|
+
// (e.g., manual recovery) can proceed if needed.
|
|
322
|
+
this.stoppingSessionIds.delete(sessionId);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
beforeSessionCommand(sessionId, commandName, request, response) {
|
|
327
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
328
|
+
var _a;
|
|
329
|
+
if (commandName) {
|
|
330
|
+
this.commandStartTime.set(`${sessionId}:${commandName}`, Date.now());
|
|
331
|
+
logger_1.default.debug(`[EventManager] beforeSessionCommand: sessionId=${sessionId}, commandName=${commandName}`);
|
|
332
|
+
}
|
|
333
|
+
// Principal Interception: Handle Xenon-specific commands regardless of session state in memory
|
|
334
|
+
if (commandName === 'execute') {
|
|
335
|
+
const script = ((_a = request.body) === null || _a === void 0 ? void 0 : _a.script) || (Array.isArray(request.body) ? request.body[0] : undefined);
|
|
336
|
+
if (script && commands_1.dashboardCommands.isDashboardCommand(script)) {
|
|
337
|
+
logger_1.default.info(`[EventManager] Intercepting Xenon command: ${script} for session ${sessionId}`);
|
|
338
|
+
yield commands_1.dashboardCommands.process(sessionId, request, response);
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
else if (script && script.includes(':')) {
|
|
342
|
+
logger_1.default.debug(`[EventManager] Custom command ${script} not handled by Xenon. Passing to driver.`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const session = SessionManager_1.SESSION_MANAGER.getSession(sessionId);
|
|
346
|
+
if (!session) {
|
|
347
|
+
logger_1.default.debug(`[EventManager] No session object found in memory for ${sessionId}. Allowing command ${commandName} to proceed.`);
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
if (commandName === 'deleteSession') {
|
|
351
|
+
// Video recording is handled in onSessionStoped() called after deleteSession
|
|
352
|
+
// No need to handle it here to avoid race conditions
|
|
353
|
+
}
|
|
354
|
+
return true;
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
afterSessionCommand(sessionId, commandName, driver, request, response, responseBody, healingInfo) {
|
|
358
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
359
|
+
var _a;
|
|
360
|
+
const session = SessionManager_1.SESSION_MANAGER.getSession(sessionId);
|
|
361
|
+
if (session) {
|
|
362
|
+
try {
|
|
363
|
+
// Save device logs (only if driver is available)
|
|
364
|
+
if (driver) {
|
|
365
|
+
yield this.saveDeviceLogs(sessionId, driver);
|
|
366
|
+
}
|
|
367
|
+
// Save command log
|
|
368
|
+
const parsedResponse = (0, helpers_1.safeParseJson)(responseBody);
|
|
369
|
+
const isSuccessResponse = !((_a = parsedResponse === null || parsedResponse === void 0 ? void 0 : parsedResponse.value) === null || _a === void 0 ? void 0 : _a.error);
|
|
370
|
+
const tracingService = typedi_1.Container.get(TracingService_1.TracingService);
|
|
371
|
+
const spanId = tracingService.getSpanId(`${session.getId()}:${commandName}`);
|
|
372
|
+
const traceId = tracingService.getTraceId(session.getId());
|
|
373
|
+
const startTime = this.commandStartTime.get(`${sessionId}:${commandName}`);
|
|
374
|
+
const duration = startTime ? Date.now() - startTime : null;
|
|
375
|
+
const logEntry = {
|
|
376
|
+
session_id: session.getId(),
|
|
377
|
+
command_name: commandName || null,
|
|
378
|
+
body: JSON.stringify(request.body),
|
|
379
|
+
response: responseBody,
|
|
380
|
+
is_success: isSuccessResponse,
|
|
381
|
+
is_error: !isSuccessResponse,
|
|
382
|
+
method: request.method,
|
|
383
|
+
title: this.getTitleFromCommandName(commandName),
|
|
384
|
+
subtitle: '',
|
|
385
|
+
screenshot: null,
|
|
386
|
+
url: request.originalUrl,
|
|
387
|
+
is_healed: !!healingInfo,
|
|
388
|
+
original_selector: (healingInfo === null || healingInfo === void 0 ? void 0 : healingInfo.originalSelector) || null,
|
|
389
|
+
healed_selector: (healingInfo === null || healingInfo === void 0 ? void 0 : healingInfo.healedSelector) || null,
|
|
390
|
+
healing_confidence: (healingInfo === null || healingInfo === void 0 ? void 0 : healingInfo.confidence) || null,
|
|
391
|
+
span_id: spanId,
|
|
392
|
+
trace_id: traceId,
|
|
393
|
+
duration: duration,
|
|
394
|
+
};
|
|
395
|
+
// Increment Healing Metrics
|
|
396
|
+
if (healingInfo) {
|
|
397
|
+
typedi_1.Container.get(MetricsService_1.MetricsService).incrementHealingAttempt();
|
|
398
|
+
if (isSuccessResponse) {
|
|
399
|
+
typedi_1.Container.get(MetricsService_1.MetricsService).incrementHealingSuccess();
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (startTime) {
|
|
403
|
+
this.commandStartTime.delete(`${sessionId}:${commandName}`);
|
|
404
|
+
}
|
|
405
|
+
// Take screenshots for specific commands (like click, setValue, etc.)
|
|
406
|
+
// OR on failure if SCREENSHOT_ON_FAILURE capability is enabled
|
|
407
|
+
const shouldTakeScreenshotForCommand = commandName && config_1.config.takeScreenshotsFor.indexOf(commandName) >= 0;
|
|
408
|
+
const screenShotCapability = session.getXenonOption(XenonCapabilityManager_1.XENON_CAPABILITIES.SCREENSHOT_ON_FAILURE, false);
|
|
409
|
+
const screenshotEveryCommandCapability = session.getXenonOption(XenonCapabilityManager_1.XENON_CAPABILITIES.SCREENSHOT_ON_EVERY_COMMAND, false);
|
|
410
|
+
const shouldTakeScreenshotOnFailure = !lodash_1.default.isNil(screenShotCapability) &&
|
|
411
|
+
screenShotCapability.toString() === 'true' &&
|
|
412
|
+
!isSuccessResponse;
|
|
413
|
+
const shouldTakeScreenshotOnEveryCommand = !lodash_1.default.isNil(screenshotEveryCommandCapability) &&
|
|
414
|
+
screenshotEveryCommandCapability.toString() === 'true';
|
|
415
|
+
if (shouldTakeScreenshotForCommand ||
|
|
416
|
+
shouldTakeScreenshotOnFailure ||
|
|
417
|
+
shouldTakeScreenshotOnEveryCommand) {
|
|
418
|
+
let screenshotBase64 = null;
|
|
419
|
+
try {
|
|
420
|
+
// Principal Intelligence: Always prefer session.getScreenShot() because it contains
|
|
421
|
+
// platform-specific optimizations (like direct, high-speed ADB capture for Android).
|
|
422
|
+
screenshotBase64 = yield session.getScreenShot();
|
|
423
|
+
}
|
|
424
|
+
catch (err) {
|
|
425
|
+
logger_1.default.warn(`[Dashboard] Session-level screenshot failed for ${sessionId}: ${err.message}. Trying direct driver...`);
|
|
426
|
+
if (driver) {
|
|
427
|
+
try {
|
|
428
|
+
screenshotBase64 = yield (0, helpers_2.takeScreenshot)(driver);
|
|
429
|
+
}
|
|
430
|
+
catch (driverErr) {
|
|
431
|
+
logger_1.default.error(`[Dashboard] Driver screenshot also failed: ${driverErr.message}`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (screenshotBase64) {
|
|
436
|
+
logEntry['screenshot'] = (0, asset_manager_1.saveScreenShot)(session.getId(), screenshotBase64);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
yield prisma_1.prisma.sessionLog.create({
|
|
440
|
+
data: logEntry,
|
|
441
|
+
});
|
|
442
|
+
// Emit command log event to dashboard
|
|
443
|
+
typedi_1.Container.get(SocketServer_1.SocketServer).emitToDashboard(SocketEvents_1.SocketEvents.SESSION_COMMAND, Object.assign({ session_id: session.getId() }, logEntry));
|
|
444
|
+
}
|
|
445
|
+
catch (err) {
|
|
446
|
+
logger_1.default.error(`[Dashboard] Failed to process command telemetry for ${sessionId}: ${err.message}`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
onSessionLog(sessionId, logEntry) {
|
|
452
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
453
|
+
yield prisma_1.prisma.log.create({
|
|
454
|
+
data: {
|
|
455
|
+
session_id: sessionId,
|
|
456
|
+
log_type: logEntry.level.toUpperCase(),
|
|
457
|
+
message: logEntry.message,
|
|
458
|
+
timestamp: new Date(),
|
|
459
|
+
},
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
getTitleFromCommandName(commandName) {
|
|
464
|
+
if (commandName) {
|
|
465
|
+
return commandName.replace(/([A-Z])/g, ' $1').replace(/^./, function (str) {
|
|
466
|
+
return str.toUpperCase();
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
return undefined;
|
|
470
|
+
}
|
|
471
|
+
getDeviceLogs(driver, sessionId) {
|
|
472
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
473
|
+
try {
|
|
474
|
+
if (!driver || !driver.caps || !driver.caps.automationName) {
|
|
475
|
+
return [];
|
|
476
|
+
}
|
|
477
|
+
const automationName = driver.caps.automationName.toLowerCase();
|
|
478
|
+
// Get device info for this session
|
|
479
|
+
const device = this.sessionToDevice.get(sessionId);
|
|
480
|
+
// Use Appium driver's extractLogs method for proper log extraction
|
|
481
|
+
if (automationName === 'xcuitest' && typeof driver.extractLogs === 'function') {
|
|
482
|
+
try {
|
|
483
|
+
// Check if this is a real iOS device using device.realDevice property
|
|
484
|
+
const isRealDevice = (device === null || device === void 0 ? void 0 : device.realDevice) === true;
|
|
485
|
+
logger_1.default.debug(`Device info for session ${sessionId} - isRealDevice: ${isRealDevice}, deviceType: ${device === null || device === void 0 ? void 0 : device.deviceType}, UDID: ${device === null || device === void 0 ? void 0 : device.udid}`);
|
|
486
|
+
if (isRealDevice) {
|
|
487
|
+
// For real iOS devices, use appium-ios-device syslog service
|
|
488
|
+
const udid = driver.caps.udid;
|
|
489
|
+
// Track session to UDID mapping for cleanup
|
|
490
|
+
this.sessionToUdid.set(sessionId, udid);
|
|
491
|
+
// If we don't have a syslog service for this device yet, start one
|
|
492
|
+
if (!this.syslogServices.has(udid)) {
|
|
493
|
+
try {
|
|
494
|
+
const syslogService = yield appium_ios_device_1.services.startSyslogService(udid);
|
|
495
|
+
const logs = [];
|
|
496
|
+
// Start listening to logs and buffer them
|
|
497
|
+
syslogService.start((logLine) => {
|
|
498
|
+
logs.push(logLine);
|
|
499
|
+
});
|
|
500
|
+
// Store the service and logs
|
|
501
|
+
this.syslogServices.set(udid, { service: syslogService, logs });
|
|
502
|
+
logger_1.default.info(`Started syslog service for real iOS device ${udid}`);
|
|
503
|
+
}
|
|
504
|
+
catch (err) {
|
|
505
|
+
logger_1.default.debug(`Could not start syslog service for real device: ${err}`);
|
|
506
|
+
return [];
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
// Return the buffered logs
|
|
510
|
+
const deviceData = this.syslogServices.get(udid);
|
|
511
|
+
if (deviceData && deviceData.logs) {
|
|
512
|
+
const currentLogs = [...deviceData.logs];
|
|
513
|
+
// Clear the buffer after retrieving
|
|
514
|
+
deviceData.logs.length = 0;
|
|
515
|
+
return currentLogs.map((logLine) => ({ message: logLine, timestamp: Date.now() }));
|
|
516
|
+
}
|
|
517
|
+
return [];
|
|
518
|
+
}
|
|
519
|
+
// For iOS simulators, extract syslog
|
|
520
|
+
const logs = yield driver.extractLogs('syslog');
|
|
521
|
+
return Array.isArray(logs) ? logs : [];
|
|
522
|
+
}
|
|
523
|
+
catch (err) {
|
|
524
|
+
logger_1.default.debug(`Could not extract syslog: ${err}`);
|
|
525
|
+
return [];
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
else if (automationName === 'uiautomator2' || (device === null || device === void 0 ? void 0 : device.platform) === 'android') {
|
|
529
|
+
try {
|
|
530
|
+
if (typeof driver.extractLogs === 'function') {
|
|
531
|
+
const logs = yield driver.extractLogs('logcat');
|
|
532
|
+
if (Array.isArray(logs) && logs.length > 0) {
|
|
533
|
+
return logs;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
catch (err) {
|
|
538
|
+
logger_1.default.debug(`Could not extract logcat via driver: ${err}. Trying direct ADB...`);
|
|
539
|
+
}
|
|
540
|
+
// Principal Intelligence: Fallback to direct ADB logs if driver fails or returns nothing
|
|
541
|
+
if (device === null || device === void 0 ? void 0 : device.udid) {
|
|
542
|
+
try {
|
|
543
|
+
const deviceManager = typedi_1.Container.get(device_managers_1.XenonManager);
|
|
544
|
+
const androidManager = (yield deviceManager.deviceInstances()).find((m) => m instanceof AndroidDeviceManager_1.default);
|
|
545
|
+
if (androidManager) {
|
|
546
|
+
const rawLogs = yield androidManager.getLogs(device.udid);
|
|
547
|
+
if (rawLogs && typeof rawLogs === 'string') {
|
|
548
|
+
const logLines = rawLogs.split('\n').filter((l) => l.trim().length > 0);
|
|
549
|
+
// Convert string lines to expected format (last 100 lines to avoid DB bloat)
|
|
550
|
+
return logLines.slice(-100).map((line) => ({
|
|
551
|
+
message: line,
|
|
552
|
+
timestamp: Date.now(),
|
|
553
|
+
level: 'INFO',
|
|
554
|
+
}));
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
catch (adbErr) {
|
|
559
|
+
logger_1.default.debug(`Direct ADB log fetch failed for ${device.udid}: ${adbErr.message}`);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return [];
|
|
564
|
+
}
|
|
565
|
+
catch (error) {
|
|
566
|
+
logger_1.default.error(`Error getting device logs: ${error}`);
|
|
567
|
+
return [];
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
saveDeviceLogs(sessionId, driver) {
|
|
572
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
573
|
+
try {
|
|
574
|
+
const logs = yield this.getDeviceLogs(driver, sessionId);
|
|
575
|
+
if (!logs || logs.length === 0) {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const lastLine = this.lastLogLine.get(sessionId) || 0;
|
|
579
|
+
const newLogs = logs.slice(lastLine);
|
|
580
|
+
if (newLogs.length === 0) {
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
this.lastLogLine.set(sessionId, logs.length);
|
|
584
|
+
// Save device logs to database
|
|
585
|
+
const logEntries = newLogs.map((logItem) => ({
|
|
586
|
+
session_id: sessionId,
|
|
587
|
+
log_type: 'DEVICE',
|
|
588
|
+
message: typeof logItem === 'string' ? logItem : logItem.message || JSON.stringify(logItem),
|
|
589
|
+
timestamp: logItem.timestamp ? new Date(logItem.timestamp) : new Date(),
|
|
590
|
+
}));
|
|
591
|
+
if (logEntries.length > 0) {
|
|
592
|
+
for (const logEntry of logEntries) {
|
|
593
|
+
yield prisma_1.prisma.log.create({
|
|
594
|
+
data: logEntry,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
catch (error) {
|
|
600
|
+
logger_1.default.error(`Error saving device logs: ${error.message}`);
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
startAppProfiling(sessionId, device, sessionCapabilities) {
|
|
605
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
606
|
+
// Only support Android profiling
|
|
607
|
+
if (device.platform.toLowerCase() !== 'android') {
|
|
608
|
+
return { is_profiling_available: false };
|
|
609
|
+
}
|
|
610
|
+
// Check if we have an app package
|
|
611
|
+
const appPackage = (sessionCapabilities === null || sessionCapabilities === void 0 ? void 0 : sessionCapabilities.appPackage) || (sessionCapabilities === null || sessionCapabilities === void 0 ? void 0 : sessionCapabilities['appium:appPackage']);
|
|
612
|
+
if (!appPackage) {
|
|
613
|
+
logger_1.default.info(`[Profiling] No app package found for session ${sessionId}, skipping profiling`);
|
|
614
|
+
return { is_profiling_available: false };
|
|
615
|
+
}
|
|
616
|
+
try {
|
|
617
|
+
// Principal Intelligence: Use the Properly configured ADB from the manager
|
|
618
|
+
// instead of a naked instance, to avoid 'defaultArgs not iterable' crashes.
|
|
619
|
+
const deviceManager = typedi_1.Container.get(device_managers_1.XenonManager);
|
|
620
|
+
const androidManager = (yield deviceManager.deviceInstances()).find((m) => m instanceof AndroidDeviceManager_1.default);
|
|
621
|
+
let adb;
|
|
622
|
+
if (androidManager) {
|
|
623
|
+
adb = yield androidManager.getAdbForDevice(device.udid);
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
adb = yield appium_adb_1.ADB.createADB({});
|
|
627
|
+
}
|
|
628
|
+
// Create app profiler
|
|
629
|
+
const profiler = new AndroidAppProfiler_1.AndroidAppProfiler({
|
|
630
|
+
adb,
|
|
631
|
+
deviceUDID: device.udid,
|
|
632
|
+
appPackage,
|
|
633
|
+
});
|
|
634
|
+
// Get device info and start capture
|
|
635
|
+
const device_info = yield profiler.getDeviceInfo();
|
|
636
|
+
yield profiler.startCapture();
|
|
637
|
+
// Store profiler for this session
|
|
638
|
+
this.appProfilers.set(sessionId, profiler);
|
|
639
|
+
logger_1.default.info(`[Profiling] Started app profiling for session ${sessionId}`);
|
|
640
|
+
return {
|
|
641
|
+
is_profiling_available: true,
|
|
642
|
+
device_info,
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
catch (err) {
|
|
646
|
+
logger_1.default.error(`[Profiling] Error initializing app profiler for session ${sessionId}: ${err.message}`);
|
|
647
|
+
return { is_profiling_available: false };
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
saveAppProfilingData(sessionId) {
|
|
652
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
653
|
+
const profiler = this.appProfilers.get(sessionId);
|
|
654
|
+
if (!profiler) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
try {
|
|
658
|
+
// Stop capture
|
|
659
|
+
yield profiler.stopCapture();
|
|
660
|
+
// Get logs
|
|
661
|
+
const logs = profiler.getLogs();
|
|
662
|
+
if (logs.length === 0) {
|
|
663
|
+
logger_1.default.info(`[Profiling] No profiling data to save for session ${sessionId}`);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
// Save to database
|
|
667
|
+
const profilingEntries = logs.map((log) => ({
|
|
668
|
+
session_id: sessionId,
|
|
669
|
+
cpu: log.cpu,
|
|
670
|
+
memory: log.memory,
|
|
671
|
+
total_cpu_used: log.total_cpu_used.toString(),
|
|
672
|
+
total_memory_used: log.total_memory_used.toString(),
|
|
673
|
+
raw_cpu_log: log.raw_cpu_log,
|
|
674
|
+
raw_memory_log: log.raw_memory_log,
|
|
675
|
+
timestamp: new Date(log.timestamp),
|
|
676
|
+
}));
|
|
677
|
+
// Batch insert profiling data
|
|
678
|
+
for (const entry of profilingEntries) {
|
|
679
|
+
yield prisma_1.prisma.profiling.create({
|
|
680
|
+
data: entry,
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
logger_1.default.info(`[Profiling] Saved ${profilingEntries.length} profiling entries for session ${sessionId}`);
|
|
684
|
+
}
|
|
685
|
+
catch (err) {
|
|
686
|
+
logger_1.default.error(`[Profiling] Error saving profiling data for session ${sessionId}: ${err.message}`);
|
|
687
|
+
}
|
|
688
|
+
finally {
|
|
689
|
+
// Clean up profiler
|
|
690
|
+
this.appProfilers.delete(sessionId);
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
exports.DashboardEventManager = DashboardEventManager;
|
|
696
|
+
exports.DashboardEventManager = DashboardEventManager = __decorate([
|
|
697
|
+
(0, typedi_2.Service)()
|
|
698
|
+
], DashboardEventManager);
|
|
699
|
+
exports.DASHBORD_EVENT_MANAGER = typedi_1.Container.get(DashboardEventManager);
|