@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,893 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* iOS Stream Service
|
|
4
|
+
*
|
|
5
|
+
* This service manages independent MJPEG streaming for iOS devices without requiring
|
|
6
|
+
* an active Appium session. It uses go-ios to start WDA and forwards the MJPEG stream.
|
|
7
|
+
*
|
|
8
|
+
* Based on GADS implementation approach.
|
|
9
|
+
*/
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
22
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
23
|
+
}) : function(o, v) {
|
|
24
|
+
o["default"] = v;
|
|
25
|
+
});
|
|
26
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
27
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
28
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
29
|
+
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;
|
|
30
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
31
|
+
};
|
|
32
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
33
|
+
var ownKeys = function(o) {
|
|
34
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
35
|
+
var ar = [];
|
|
36
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
37
|
+
return ar;
|
|
38
|
+
};
|
|
39
|
+
return ownKeys(o);
|
|
40
|
+
};
|
|
41
|
+
return function (mod) {
|
|
42
|
+
if (mod && mod.__esModule) return mod;
|
|
43
|
+
var result = {};
|
|
44
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
45
|
+
__setModuleDefault(result, mod);
|
|
46
|
+
return result;
|
|
47
|
+
};
|
|
48
|
+
})();
|
|
49
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
50
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
51
|
+
};
|
|
52
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
53
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
54
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
55
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
56
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
57
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
58
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
62
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
63
|
+
};
|
|
64
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
65
|
+
const typedi_1 = require("typedi");
|
|
66
|
+
const child_process_1 = require("child_process");
|
|
67
|
+
const util_1 = require("util");
|
|
68
|
+
const path_1 = __importDefault(require("path"));
|
|
69
|
+
const os_1 = __importDefault(require("os"));
|
|
70
|
+
const http_1 = __importDefault(require("http"));
|
|
71
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
72
|
+
const tcp_port_used_1 = __importDefault(require("tcp-port-used"));
|
|
73
|
+
const InternalHttpClient_1 = require("../../InternalHttpClient");
|
|
74
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
75
|
+
const helpers_1 = require("../../helpers");
|
|
76
|
+
const device_store_1 = require("../../data-service/device-store");
|
|
77
|
+
const device_service_1 = require("../../data-service/device-service");
|
|
78
|
+
const execPromise = (0, util_1.promisify)(child_process_1.exec);
|
|
79
|
+
const typedi_2 = require("typedi");
|
|
80
|
+
const ResourceIsolationService_1 = require("../../services/ResourceIsolationService");
|
|
81
|
+
let IOSStreamService = class IOSStreamService {
|
|
82
|
+
constructor() {
|
|
83
|
+
this.sessions = new Map();
|
|
84
|
+
this.startPromises = new Map();
|
|
85
|
+
this.recoveryCooldowns = new Map(); // Track last recovery attempt time
|
|
86
|
+
this.RECOVERY_COOLDOWN_MS = 30000; // 30s cooldown between recovery attempts
|
|
87
|
+
this.goIOSPath = this.getGoIOSPath();
|
|
88
|
+
this.startWatchdog();
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Watchdog: Periodically monitors running streams and cleans up idle ones
|
|
92
|
+
*/
|
|
93
|
+
startWatchdog() {
|
|
94
|
+
setInterval(() => __awaiter(this, void 0, void 0, function* () {
|
|
95
|
+
for (const [udid, session] of this.sessions.entries()) {
|
|
96
|
+
const now = Date.now();
|
|
97
|
+
if (session.status === 'running') {
|
|
98
|
+
// Check for inactivity
|
|
99
|
+
if (now - session.lastViewerAt > 600000 && session.viewerCount === 0) {
|
|
100
|
+
// Principal Protection: Never stop a stream if the device is busy with an active session.
|
|
101
|
+
// This prevents the watchdog from killing WDA while a test is running.
|
|
102
|
+
const device = yield device_store_1.DeviceStoreFactory.getStore().findDevice({ udid });
|
|
103
|
+
if (device && device.busy) {
|
|
104
|
+
logger_1.default.debug(`🛡️ [${udid}] [Watchdog] Stream is idle but device is BUSY. Keeping alive.`);
|
|
105
|
+
session.lastViewerAt = Date.now(); // Refresh timer to avoid constant DB checks
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
logger_1.default.info(`[${udid}] [Watchdog] Stopping idle iOS stream (No viewers for 30s)`);
|
|
109
|
+
this.stopStream(udid);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
// 2. Health check & Self-Healing
|
|
113
|
+
// We use elased autonomous methodology to ensure devices are always warm
|
|
114
|
+
const isAlive = yield this.isStreamResponsive(udid);
|
|
115
|
+
if (!isAlive) {
|
|
116
|
+
// Check cooldown before attempting recovery
|
|
117
|
+
if (!this.canAttemptRecovery(udid)) {
|
|
118
|
+
const lastAttempt = this.recoveryCooldowns.get(udid);
|
|
119
|
+
const waitMs = this.RECOVERY_COOLDOWN_MS - (Date.now() - (lastAttempt || 0));
|
|
120
|
+
logger_1.default.debug(`[Watchdog] ${udid} stream unhealthy but recovery cooldown active (${Math.ceil(waitMs / 1000)}s remaining). Skipping recovery attempt.`);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
logger_1.default.warn(`Stream watchdog detected failure for ${udid}. Attempting autonomous healing...`);
|
|
124
|
+
try {
|
|
125
|
+
// Get device state to check for Session Shield
|
|
126
|
+
const device = yield device_store_1.DeviceStoreFactory.getStore().findDevice({ udid });
|
|
127
|
+
if (device && device.busy) {
|
|
128
|
+
logger_1.default.info(`🛡️ [Watchdog] Skipping heal for ${udid} as it has an active session.`);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
this.markRecoveryAttempt(udid);
|
|
132
|
+
yield this.startStream(udid);
|
|
133
|
+
// Increment healed count for visual feedback
|
|
134
|
+
if (device) {
|
|
135
|
+
yield device_store_1.DeviceStoreFactory.getStore().updateDevice(udid, device.host, {
|
|
136
|
+
totalHealedCount: (device.totalHealedCount || 0) + 1,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
logger_1.default.error(`Watchdog healing failed for ${udid}: ${e}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}), 3600000); // 1hr interval for background stability
|
|
147
|
+
}
|
|
148
|
+
getGoIOSPath() {
|
|
149
|
+
const goIOSDir = (0, helpers_1.cachePath)('goIOS');
|
|
150
|
+
return path_1.default.join(goIOSDir, 'ios');
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Check if go-ios binary exists
|
|
154
|
+
*/
|
|
155
|
+
isGoIOSAvailable() {
|
|
156
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
157
|
+
try {
|
|
158
|
+
if (!fs_extra_1.default.existsSync(this.goIOSPath)) {
|
|
159
|
+
logger_1.default.warn(`go-ios binary not found at ${this.goIOSPath}`);
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Detect the WDA bundle ID installed on the device
|
|
171
|
+
*/
|
|
172
|
+
detectWDABundleId(udid) {
|
|
173
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
174
|
+
try {
|
|
175
|
+
const { stdout } = yield execPromise(`"${this.goIOSPath}" apps --udid ${udid}`, {
|
|
176
|
+
env: Object.assign(Object.assign({}, process.env), { ENABLE_GO_IOS_AGENT: 'yes' }),
|
|
177
|
+
});
|
|
178
|
+
const apps = JSON.parse(stdout);
|
|
179
|
+
const wda = apps.find((a) => (a.CFBundleIdentifier && a.CFBundleIdentifier.includes('WebDriverAgentRunner')) ||
|
|
180
|
+
(a.CFBundleName && a.CFBundleName.includes('WebDriverAgentRunner')));
|
|
181
|
+
return wda ? wda.CFBundleIdentifier : null;
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
logger_1.default.warn(`Failed to detect WDA bundle ID for ${udid}: ${error}`);
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Check if tunnel is needed (iOS 17+) and ensure it's running
|
|
191
|
+
*/
|
|
192
|
+
ensureTunnel(udid) {
|
|
193
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
194
|
+
var _a, _b;
|
|
195
|
+
try {
|
|
196
|
+
const { stdout } = yield execPromise(`"${this.goIOSPath}" info --udid ${udid}`, {
|
|
197
|
+
env: Object.assign(Object.assign({}, process.env), { ENABLE_GO_IOS_AGENT: 'yes' }),
|
|
198
|
+
});
|
|
199
|
+
const info = JSON.parse(stdout);
|
|
200
|
+
const versionStr = String(info.ProductVersion || info.HumanReadableProductVersionString || '0');
|
|
201
|
+
// Extract the first numeric sequence (e.g., "15.8.6" or "iOS 17.2")
|
|
202
|
+
const versionMatch = versionStr.match(/(\d+\.?\d*)/);
|
|
203
|
+
const version = versionMatch ? parseFloat(versionMatch[0]) : 0;
|
|
204
|
+
if (version >= 17) {
|
|
205
|
+
logger_1.default.info(`iOS ${version} detected for ${udid} (Raw: "${versionStr}"). Starting tunnel...`);
|
|
206
|
+
// Check if already running in our session tracker
|
|
207
|
+
const existing = [...this.sessions.values()].find((s) => s.udid === udid && s.tunnelProcess && s.tunnelProcess.exitCode === null);
|
|
208
|
+
if (existing && existing.tunnelProcess)
|
|
209
|
+
return existing.tunnelProcess;
|
|
210
|
+
// CRITICAL: Kill any orphan tunnel processes before starting new one
|
|
211
|
+
// This prevents 'address already in use' errors from stale processes
|
|
212
|
+
yield this.cleanupOrphanTunnels(udid);
|
|
213
|
+
const isolationService = typedi_2.Container.get(ResourceIsolationService_1.ResourceIsolationService);
|
|
214
|
+
const { command, args } = isolationService.wrapSpawn(this.goIOSPath, ['tunnel', 'start', '--udid', udid, '--userspace'], 'Performance');
|
|
215
|
+
const tunnelProcess = (0, child_process_1.spawn)(command, args, {
|
|
216
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
217
|
+
env: Object.assign(Object.assign({}, process.env), { ENABLE_GO_IOS_AGENT: 'yes' }),
|
|
218
|
+
});
|
|
219
|
+
(_a = tunnelProcess.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (data) => logger_1.default.debug(`Tunnel [${udid}]: ${data}`));
|
|
220
|
+
(_b = tunnelProcess.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (data) => logger_1.default.debug(`Tunnel Err [${udid}]: ${data}`));
|
|
221
|
+
// Wait for tunnel to establish by checking the go-ios agent port
|
|
222
|
+
logger_1.default.info('Waiting for tunnel agent on port 60105 to be ready...');
|
|
223
|
+
let tunnelReady = false;
|
|
224
|
+
const tunnelTimeout = 15000;
|
|
225
|
+
const subStartTime = Date.now();
|
|
226
|
+
while (Date.now() - subStartTime < tunnelTimeout) {
|
|
227
|
+
try {
|
|
228
|
+
tunnelReady = yield tcp_port_used_1.default.check(60105, '127.0.0.1');
|
|
229
|
+
if (tunnelReady)
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
catch (e) {
|
|
233
|
+
/* ignore */
|
|
234
|
+
}
|
|
235
|
+
yield new Promise((resolve) => setTimeout(resolve, 1000));
|
|
236
|
+
}
|
|
237
|
+
if (!tunnelReady) {
|
|
238
|
+
logger_1.default.warn(`Tunnel agent port 60105 not ready after ${tunnelTimeout / 1000}s, proceeding anyway...`);
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
logger_1.default.info('Tunnel agent is ready. Settling for 2s...');
|
|
242
|
+
yield new Promise((resolve) => setTimeout(resolve, 2000));
|
|
243
|
+
logger_1.default.info('Tunnel agent settled.');
|
|
244
|
+
}
|
|
245
|
+
return tunnelProcess;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
logger_1.default.warn(`Failed to check version or start tunnel for ${udid}: ${error}`);
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Kill any orphan tunnel processes that might be left from previous runs
|
|
256
|
+
* This is critical for preventing 'address already in use' errors
|
|
257
|
+
*/
|
|
258
|
+
cleanupOrphanTunnels(udid) {
|
|
259
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
260
|
+
logger_1.default.debug(`Cleaning up orphan tunnels for ${udid}...`);
|
|
261
|
+
const pkillCmds = [
|
|
262
|
+
// Kill any existing tunnel for this specific device
|
|
263
|
+
`pkill -9 -f "ios tunnel.*${udid}"`,
|
|
264
|
+
`pkill -9 -f "go-ios.*tunnel.*${udid}"`,
|
|
265
|
+
];
|
|
266
|
+
for (const cmd of pkillCmds) {
|
|
267
|
+
try {
|
|
268
|
+
yield execPromise(cmd);
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
/* ignore - process might not exist */
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
// Check common go-ios agent ports (60105, 60106) and kill if bound
|
|
275
|
+
const agentPorts = [60105, 60106];
|
|
276
|
+
for (const port of agentPorts) {
|
|
277
|
+
try {
|
|
278
|
+
const { stdout } = yield execPromise(`lsof -ti :${port}`);
|
|
279
|
+
const pids = stdout.trim().split('\n');
|
|
280
|
+
for (const pid of pids) {
|
|
281
|
+
if (pid) {
|
|
282
|
+
logger_1.default.debug(`Killing orphan process ${pid} on go-ios agent port ${port}`);
|
|
283
|
+
yield execPromise(`kill -9 ${pid}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
catch (err) {
|
|
288
|
+
/* ignore - lsof returns 1 if no port found */
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// Small delay to ensure OS releases sockets
|
|
292
|
+
yield new Promise((resolve) => setTimeout(resolve, 500));
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Check if WDA is already running and responding
|
|
297
|
+
* Principal Resilience: Retries transient connection errors (ECONNRESET) up to 2 times
|
|
298
|
+
* with exponential backoff, as these often indicate WDA is restarting or tunnel is reconnecting.
|
|
299
|
+
*/
|
|
300
|
+
isWDARunning(wdaPort_1) {
|
|
301
|
+
return __awaiter(this, arguments, void 0, function* (wdaPort, retries = 2) {
|
|
302
|
+
var _a, _b, _c, _d;
|
|
303
|
+
const axios = (yield Promise.resolve().then(() => __importStar(require('axios')))).default;
|
|
304
|
+
const host = '127.0.0.1'; // Force IPv4 for local tunnels
|
|
305
|
+
const maxRetries = retries;
|
|
306
|
+
let lastError;
|
|
307
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
308
|
+
try {
|
|
309
|
+
const response = yield axios.get(`http://${host}:${wdaPort}/status`, {
|
|
310
|
+
timeout: 2500, // 2.5s for consistency
|
|
311
|
+
httpAgent: new http_1.default.Agent({ keepAlive: false }),
|
|
312
|
+
validateStatus: (status) => status === 200,
|
|
313
|
+
});
|
|
314
|
+
const isReady = ((_b = (_a = response.data) === null || _a === void 0 ? void 0 : _a.value) === null || _b === void 0 ? void 0 : _b.ready) === true;
|
|
315
|
+
if (!isReady) {
|
|
316
|
+
logger_1.default.debug(`[WDA] Port ${wdaPort} active but not ready. Status: ${JSON.stringify((_c = response.data) === null || _c === void 0 ? void 0 : _c.value)}`);
|
|
317
|
+
}
|
|
318
|
+
return isReady;
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
lastError = error;
|
|
322
|
+
const isTransientError = error.code === 'ECONNRESET' ||
|
|
323
|
+
error.code === 'ETIMEDOUT' ||
|
|
324
|
+
error.code === 'ECONNABORTED' ||
|
|
325
|
+
(error.response && error.response.status >= 500);
|
|
326
|
+
if (isTransientError && attempt < maxRetries) {
|
|
327
|
+
const backoffMs = Math.min(500 * Math.pow(2, attempt), 2000); // 500ms, 1000ms, 2000ms max
|
|
328
|
+
logger_1.default.debug(`[WDA] Port ${wdaPort} transient error (${error.code || ((_d = error.response) === null || _d === void 0 ? void 0 : _d.status)}), retrying in ${backoffMs}ms (attempt ${attempt + 1}/${maxRetries + 1})`);
|
|
329
|
+
yield new Promise((resolve) => setTimeout(resolve, backoffMs));
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
// Permanent error or max retries reached
|
|
333
|
+
if (error.code === 'ECONNREFUSED') {
|
|
334
|
+
logger_1.default.debug(`[WDA] Port ${wdaPort} connection refused (Tunnel likely down)`);
|
|
335
|
+
}
|
|
336
|
+
else if (error.code === 'ECONNABORTED') {
|
|
337
|
+
logger_1.default.debug(`[WDA] Port ${wdaPort} health check timed out (WDA hanging)`);
|
|
338
|
+
}
|
|
339
|
+
else if (error.code === 'ECONNRESET') {
|
|
340
|
+
logger_1.default.debug(`[WDA] Port ${wdaPort} connection reset after ${attempt + 1} attempts (WDA may be restarting or tunnel unstable)`);
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
logger_1.default.debug(`[WDA] Port ${wdaPort} health check failed: ${error.message}`);
|
|
344
|
+
}
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return false;
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Comprehensive process-level and endpoint-level health check.
|
|
353
|
+
* Principal Intelligence: Differentiates between 'Process dead' and 'Network unreachable'.
|
|
354
|
+
* If only the tunnel is dead but WDA is alive via IP, it will trigger a tunnel restart.
|
|
355
|
+
*/
|
|
356
|
+
isStreamResponsive(udid) {
|
|
357
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
358
|
+
const session = this.sessions.get(udid);
|
|
359
|
+
if (!session || session.status !== 'running')
|
|
360
|
+
return false;
|
|
361
|
+
// 1. Process Check: verify child processes haven't exited
|
|
362
|
+
const isWdaAlive = session.wdaProcess && session.wdaProcess.exitCode === null;
|
|
363
|
+
const isWdaIproxyAlive = session.forwardWDAProcess && session.forwardWDAProcess.exitCode === null;
|
|
364
|
+
const isMjpegIproxyAlive = session.forwardMJPEGProcess && session.forwardMJPEGProcess.exitCode === null;
|
|
365
|
+
if (!isWdaAlive) {
|
|
366
|
+
logger_1.default.warn(`🛡️ [${udid}] [Watchdog] WDA process is dead. Full restart required.`);
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
if (!isWdaIproxyAlive || !isMjpegIproxyAlive) {
|
|
370
|
+
logger_1.default.warn(`🛡️ [${udid}] [Watchdog] Tunnel processes are dead. Attempting tunnel-only recovery...`);
|
|
371
|
+
const device = yield device_store_1.DeviceStoreFactory.getStore().findDevice({ udid });
|
|
372
|
+
if (device && device.ip) {
|
|
373
|
+
// Double check if WDA is alive via network IP
|
|
374
|
+
const isWdaAccessibleViaNetwork = yield this.isWDARunningOnHost(device.ip, 8100);
|
|
375
|
+
if (isWdaAccessibleViaNetwork) {
|
|
376
|
+
logger_1.default.info(`🛡️ [${udid}] [Watchdog] WDA is alive on network ${device.ip}. Restarting tunnels...`);
|
|
377
|
+
yield this.restartTunnelsOnly(session);
|
|
378
|
+
return true; // We healed it!
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return false; // Cannot heal without network access or if WDA is also dead
|
|
382
|
+
}
|
|
383
|
+
// 2. Network Check: verify endpoint is responding via tunnel
|
|
384
|
+
const isRespondingViaTunnel = yield this.isWDARunning(session.wdaPort);
|
|
385
|
+
if (!isRespondingViaTunnel) {
|
|
386
|
+
logger_1.default.warn(`🛡️ [${udid}] [Watchdog] WDA tunnel on port ${session.wdaPort} is unresponsive. checking network...`);
|
|
387
|
+
const device = yield device_store_1.DeviceStoreFactory.getStore().findDevice({ udid });
|
|
388
|
+
if (device && device.ip) {
|
|
389
|
+
const isWdaAccessibleViaNetwork = yield this.isWDARunningOnHost(device.ip, 8100);
|
|
390
|
+
if (isWdaAccessibleViaNetwork) {
|
|
391
|
+
logger_1.default.info(`🛡️ [${udid}] [Watchdog] WDA is alive on network but tunnel is hung. Restarting tunnels...`);
|
|
392
|
+
yield this.restartTunnelsOnly(session);
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
return true;
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Check if WDA is running on a specific host/port
|
|
403
|
+
*/
|
|
404
|
+
isWDARunningOnHost(host, port) {
|
|
405
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
406
|
+
var _a, _b;
|
|
407
|
+
const axios = (yield Promise.resolve().then(() => __importStar(require('axios')))).default;
|
|
408
|
+
try {
|
|
409
|
+
const response = yield axios.get(`http://${host}:${port}/status`, {
|
|
410
|
+
timeout: 3000,
|
|
411
|
+
validateStatus: (status) => status === 200,
|
|
412
|
+
});
|
|
413
|
+
return ((_b = (_a = response.data) === null || _a === void 0 ? void 0 : _a.value) === null || _b === void 0 ? void 0 : _b.ready) === true;
|
|
414
|
+
}
|
|
415
|
+
catch (e) {
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Restarts only the iproxy tunnels without stopping WDA
|
|
422
|
+
*/
|
|
423
|
+
restartTunnelsOnly(session) {
|
|
424
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
425
|
+
const udid = session.udid;
|
|
426
|
+
const isolationService = typedi_2.Container.get(ResourceIsolationService_1.ResourceIsolationService);
|
|
427
|
+
// 1. Kill old tunnels
|
|
428
|
+
if (session.forwardWDAProcess)
|
|
429
|
+
session.forwardWDAProcess.kill('SIGKILL');
|
|
430
|
+
if (session.forwardMJPEGProcess)
|
|
431
|
+
session.forwardMJPEGProcess.kill('SIGKILL');
|
|
432
|
+
// 2. Start new tunnels
|
|
433
|
+
const wdaIproxy = isolationService.wrapSpawn('iproxy', ['-u', udid, `${session.wdaPort}:8100`], 'Performance');
|
|
434
|
+
const mjpegIproxy = isolationService.wrapSpawn('iproxy', ['-u', udid, `${session.mjpegPort}:9100`], 'Performance');
|
|
435
|
+
session.forwardWDAProcess = (0, child_process_1.spawn)(wdaIproxy.command, wdaIproxy.args);
|
|
436
|
+
session.forwardMJPEGProcess = (0, child_process_1.spawn)(mjpegIproxy.command, mjpegIproxy.args);
|
|
437
|
+
logger_1.default.info(`🛡️ [${udid}] [Watchdog] Tunnels restarted successfully.`);
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Start streaming for a device
|
|
442
|
+
*/
|
|
443
|
+
/**
|
|
444
|
+
* Check if recovery is allowed (cooldown period has passed)
|
|
445
|
+
*/
|
|
446
|
+
canAttemptRecovery(udid) {
|
|
447
|
+
const lastAttempt = this.recoveryCooldowns.get(udid);
|
|
448
|
+
if (!lastAttempt)
|
|
449
|
+
return true;
|
|
450
|
+
const elapsed = Date.now() - lastAttempt;
|
|
451
|
+
return elapsed >= this.RECOVERY_COOLDOWN_MS;
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Mark recovery attempt timestamp
|
|
455
|
+
*/
|
|
456
|
+
markRecoveryAttempt(udid) {
|
|
457
|
+
this.recoveryCooldowns.set(udid, Date.now());
|
|
458
|
+
}
|
|
459
|
+
startStream(udid) {
|
|
460
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
461
|
+
// Return existing promise if already starting
|
|
462
|
+
if (this.startPromises.has(udid)) {
|
|
463
|
+
const existingPromise = this.startPromises.get(udid);
|
|
464
|
+
if (existingPromise)
|
|
465
|
+
return existingPromise;
|
|
466
|
+
}
|
|
467
|
+
// Check if stream is already running - avoid unnecessary restarts
|
|
468
|
+
const existingSession = this.sessions.get(udid);
|
|
469
|
+
if (existingSession && existingSession.status === 'running') {
|
|
470
|
+
const isHealthy = yield this.isWDARunning(existingSession.wdaPort);
|
|
471
|
+
if (isHealthy) {
|
|
472
|
+
logger_1.default.debug(`[${udid}] Stream already running and healthy, reusing existing session`);
|
|
473
|
+
return { wdaPort: existingSession.wdaPort, mjpegPort: existingSession.mjpegPort };
|
|
474
|
+
}
|
|
475
|
+
// Stream exists but unhealthy - check cooldown before recovery
|
|
476
|
+
if (!this.canAttemptRecovery(udid)) {
|
|
477
|
+
const lastAttempt = this.recoveryCooldowns.get(udid);
|
|
478
|
+
const waitMs = this.RECOVERY_COOLDOWN_MS - (Date.now() - (lastAttempt || 0));
|
|
479
|
+
logger_1.default.debug(`[${udid}] Recovery cooldown active (${Math.ceil(waitMs / 1000)}s remaining). Skipping recovery attempt.`);
|
|
480
|
+
throw new Error(`Stream recovery is in cooldown. Last attempt was ${Math.ceil(waitMs / 1000)}s ago.`);
|
|
481
|
+
}
|
|
482
|
+
// Stop unhealthy stream before restarting
|
|
483
|
+
logger_1.default.info(`[${udid}] Stream exists but unhealthy, stopping before restart...`);
|
|
484
|
+
yield this.stopStream(udid);
|
|
485
|
+
}
|
|
486
|
+
// Define the core logic as an internal async function
|
|
487
|
+
const performStartup = () => __awaiter(this, void 0, void 0, function* () {
|
|
488
|
+
var _a, _b, _c, _d;
|
|
489
|
+
try {
|
|
490
|
+
const existingSession = this.sessions.get(udid);
|
|
491
|
+
if (existingSession && existingSession.status === 'running') {
|
|
492
|
+
const isHealthy = yield this.isWDARunning(existingSession.wdaPort);
|
|
493
|
+
if (isHealthy) {
|
|
494
|
+
logger_1.default.debug(`[${udid}] Stream already running and healthy, reusing existing session`);
|
|
495
|
+
return { wdaPort: existingSession.wdaPort, mjpegPort: existingSession.mjpegPort };
|
|
496
|
+
}
|
|
497
|
+
// Stream exists but unhealthy - check cooldown before recovery
|
|
498
|
+
if (!this.canAttemptRecovery(udid)) {
|
|
499
|
+
const lastAttempt = this.recoveryCooldowns.get(udid);
|
|
500
|
+
const waitMs = this.RECOVERY_COOLDOWN_MS - (Date.now() - (lastAttempt || 0));
|
|
501
|
+
logger_1.default.debug(`[${udid}] Recovery cooldown active (${Math.ceil(waitMs / 1000)}s remaining). Skipping recovery attempt.`);
|
|
502
|
+
throw new Error(`Stream recovery is in cooldown. Last attempt was ${Math.ceil(waitMs / 1000)}s ago.`);
|
|
503
|
+
}
|
|
504
|
+
logger_1.default.info(`Existing session for ${udid} not responding, restarting...`);
|
|
505
|
+
this.markRecoveryAttempt(udid);
|
|
506
|
+
}
|
|
507
|
+
const device = yield device_store_1.DeviceStoreFactory.getStore().findDevice({ udid });
|
|
508
|
+
if (!device)
|
|
509
|
+
throw new Error(`Device ${udid} not found`);
|
|
510
|
+
const wdaPort = device.wdaLocalPort || (yield (0, helpers_1.getFreePort)());
|
|
511
|
+
const mjpegPort = device.mjpegServerPort || (yield (0, helpers_1.getFreePort)());
|
|
512
|
+
// Perform aggressive cleanup of any existing processes for THIS device/ports
|
|
513
|
+
yield this.stopStream(udid);
|
|
514
|
+
yield this.killStaleProcesses(udid, wdaPort, mjpegPort);
|
|
515
|
+
const session = {
|
|
516
|
+
udid,
|
|
517
|
+
wdaProcess: null,
|
|
518
|
+
forwardWDAProcess: null,
|
|
519
|
+
forwardMJPEGProcess: null,
|
|
520
|
+
tunnelProcess: null,
|
|
521
|
+
wdaPort,
|
|
522
|
+
mjpegPort,
|
|
523
|
+
status: 'starting',
|
|
524
|
+
startedAt: new Date(),
|
|
525
|
+
lastViewerAt: Date.now(),
|
|
526
|
+
viewerCount: 0,
|
|
527
|
+
};
|
|
528
|
+
this.sessions.set(udid, session);
|
|
529
|
+
const goIOSAvailable = yield this.isGoIOSAvailable();
|
|
530
|
+
if (!goIOSAvailable)
|
|
531
|
+
throw new Error('go-ios not available');
|
|
532
|
+
// 1. Technical Optimization: If real iOS device and WDA IPA exists in repository, use it as priority
|
|
533
|
+
const { APP_SERVICE } = yield Promise.resolve().then(() => __importStar(require('../../dashboard/services/app-service')));
|
|
534
|
+
const wdaApp = yield APP_SERVICE.getWDAApp();
|
|
535
|
+
if (wdaApp && fs_extra_1.default.existsSync(wdaApp.filepath)) {
|
|
536
|
+
logger_1.default.info(`📱 Artisan WDA: Found pre-signed artifact "${wdaApp.name}". Provisioning ${udid} for stream...`);
|
|
537
|
+
try {
|
|
538
|
+
const { Container } = yield Promise.resolve().then(() => __importStar(require('typedi')));
|
|
539
|
+
const { XenonManager } = yield Promise.resolve().then(() => __importStar(require('../index')));
|
|
540
|
+
const IOSDeviceManager = (yield Promise.resolve().then(() => __importStar(require('../IOSDeviceManager')))).default;
|
|
541
|
+
const deviceManager = Container.get(XenonManager);
|
|
542
|
+
const manager = (yield deviceManager.deviceInstances()).find((m) => m instanceof IOSDeviceManager);
|
|
543
|
+
if (manager) {
|
|
544
|
+
yield manager.installApp(udid, wdaApp.filepath);
|
|
545
|
+
logger_1.default.info(`📱 Artisan WDA: Artifact provisioned successfully to ${udid}`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
catch (e) {
|
|
549
|
+
logger_1.default.warn(`📱 Artisan WDA: Failed to provision WDA to ${udid}: ${e}`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
// 2. Ensure tunnel for iOS 17+
|
|
553
|
+
session.tunnelProcess = yield this.ensureTunnel(udid);
|
|
554
|
+
// 2. Start Port Forwarding using iproxy (more reliable on Mac)
|
|
555
|
+
logger_1.default.info(`Forwarding ${udid}: ${wdaPort}->8100, ${mjpegPort}->9100 using iproxy`);
|
|
556
|
+
const isolationService = typedi_2.Container.get(ResourceIsolationService_1.ResourceIsolationService);
|
|
557
|
+
const wdaIproxy = isolationService.wrapSpawn('iproxy', ['-u', udid, `${wdaPort}:8100`], 'Performance');
|
|
558
|
+
const mjpegIproxy = isolationService.wrapSpawn('iproxy', ['-u', udid, `${mjpegPort}:9100`], 'Performance');
|
|
559
|
+
session.forwardWDAProcess = (0, child_process_1.spawn)(wdaIproxy.command, wdaIproxy.args);
|
|
560
|
+
session.forwardMJPEGProcess = (0, child_process_1.spawn)(mjpegIproxy.command, mjpegIproxy.args);
|
|
561
|
+
const handleIproxyProcess = (p, name) => {
|
|
562
|
+
p.on('error', (err) => logger_1.default.error(`${name} [${udid}] error: ${err.message}`));
|
|
563
|
+
p.on('exit', (code) => {
|
|
564
|
+
if (code !== 0 && code !== null) {
|
|
565
|
+
logger_1.default.warn(`${name} [${udid}] exited with code ${code}`);
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
};
|
|
569
|
+
handleIproxyProcess(session.forwardWDAProcess, 'iproxy-wda');
|
|
570
|
+
handleIproxyProcess(session.forwardMJPEGProcess, 'iproxy-mjpeg');
|
|
571
|
+
// 3. Detect and Start WDA
|
|
572
|
+
const bundleId = (yield this.detectWDABundleId(udid)) || 'com.qasecret.WebDriverAgentRunner.xctrunner';
|
|
573
|
+
logger_1.default.info(`Starting WDA ${bundleId} on ${udid}`);
|
|
574
|
+
const wdaSpawn = isolationService.wrapSpawn(this.goIOSPath, [
|
|
575
|
+
'runwda',
|
|
576
|
+
'--bundleid',
|
|
577
|
+
bundleId,
|
|
578
|
+
'--testrunnerbundleid',
|
|
579
|
+
bundleId,
|
|
580
|
+
'--xctestconfig',
|
|
581
|
+
'WebDriverAgentRunner.xctest',
|
|
582
|
+
'--udid',
|
|
583
|
+
udid,
|
|
584
|
+
], 'Performance'); // WDA deserves Performance mode
|
|
585
|
+
session.wdaProcess = (0, child_process_1.spawn)(wdaSpawn.command, wdaSpawn.args, {
|
|
586
|
+
env: Object.assign(Object.assign({}, process.env), { ENABLE_GO_IOS_AGENT: 'yes' }),
|
|
587
|
+
});
|
|
588
|
+
const logDir = path_1.default.join(os_1.default.tmpdir(), 'xenon-logs');
|
|
589
|
+
if (!fs_extra_1.default.existsSync(logDir))
|
|
590
|
+
fs_extra_1.default.mkdirSync(logDir);
|
|
591
|
+
const wdaRunLog = path_1.default.join(logDir, `runwda-${udid}.log`);
|
|
592
|
+
(_a = session.wdaProcess.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (d) => fs_extra_1.default.appendFileSync(wdaRunLog, d));
|
|
593
|
+
(_b = session.wdaProcess.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (d) => fs_extra_1.default.appendFileSync(wdaRunLog, d));
|
|
594
|
+
// 4. Wait for WDA to be ready
|
|
595
|
+
const startTime = Date.now();
|
|
596
|
+
const timeout = 120000; // Senior Resiliency: 120s for WDA startup consistency
|
|
597
|
+
while (Date.now() - startTime < timeout) {
|
|
598
|
+
if (yield this.isWDARunning(wdaPort)) {
|
|
599
|
+
session.status = 'running';
|
|
600
|
+
session.startedAt = new Date(); // Reset settlement timer for strict readiness phase
|
|
601
|
+
// Ensure MJPEG server is started by updating WDA settings
|
|
602
|
+
yield this.updateWDASettings(wdaPort);
|
|
603
|
+
// Wait up to 5 seconds for MJPEG server to be ready on the local port
|
|
604
|
+
logger_1.default.info(`Waiting for MJPEG server to be ready on port ${mjpegPort}...`);
|
|
605
|
+
let mjpegReady = false;
|
|
606
|
+
for (let i = 0; i < 5; i++) {
|
|
607
|
+
// Try 5 times with 1-second delay = 5 seconds total
|
|
608
|
+
// Perform a real HTTP check to see if the MJPEG server is actually serving
|
|
609
|
+
try {
|
|
610
|
+
mjpegReady = yield tcp_port_used_1.default.check(mjpegPort, '127.0.0.1');
|
|
611
|
+
if (mjpegReady)
|
|
612
|
+
break;
|
|
613
|
+
}
|
|
614
|
+
catch (e) {
|
|
615
|
+
// Not ready yet
|
|
616
|
+
}
|
|
617
|
+
yield new Promise((resolve) => setTimeout(resolve, 1000));
|
|
618
|
+
}
|
|
619
|
+
if (mjpegReady) {
|
|
620
|
+
logger_1.default.info(`MJPEG server is ready on port ${mjpegPort} for ${udid}`);
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
logger_1.default.warn(`MJPEG server not detected on port ${mjpegPort} for ${udid} after 5s timeout. It might be starting slowly or WDA might be struggling.`);
|
|
624
|
+
}
|
|
625
|
+
// Fetch screen size from WDA
|
|
626
|
+
let screenWidth = 0, screenHeight = 0;
|
|
627
|
+
try {
|
|
628
|
+
const winSize = yield InternalHttpClient_1.InternalHttpClient.get(`http://127.0.0.1:${wdaPort}/window/size`);
|
|
629
|
+
screenWidth = winSize.value.width;
|
|
630
|
+
screenHeight = winSize.value.height;
|
|
631
|
+
session.screenWidth = screenWidth;
|
|
632
|
+
session.screenHeight = screenHeight;
|
|
633
|
+
logger_1.default.info(`Detected screen size for ${udid}: ${screenWidth}x${screenHeight}`);
|
|
634
|
+
}
|
|
635
|
+
catch (e) {
|
|
636
|
+
logger_1.default.warn(`Failed to get screen size for ${udid}: ${e}`);
|
|
637
|
+
}
|
|
638
|
+
// Update device info in store
|
|
639
|
+
const device = yield device_store_1.DeviceStoreFactory.getStore().findDevice({ udid });
|
|
640
|
+
if (device) {
|
|
641
|
+
const updateData = {
|
|
642
|
+
wdaLocalPort: wdaPort,
|
|
643
|
+
mjpegServerPort: mjpegPort,
|
|
644
|
+
};
|
|
645
|
+
if (screenWidth > 0) {
|
|
646
|
+
updateData.screenWidth = screenWidth.toString();
|
|
647
|
+
updateData.screenHeight = screenHeight.toString();
|
|
648
|
+
}
|
|
649
|
+
yield device_store_1.DeviceStoreFactory.getStore().updateDevice(udid, device.host, updateData);
|
|
650
|
+
}
|
|
651
|
+
logger_1.default.info(`WDA is ready for ${udid} at port ${wdaPort}`);
|
|
652
|
+
// Create a WDA session for keyboard/interaction operations
|
|
653
|
+
try {
|
|
654
|
+
const sessionId = yield this.createWDASession(wdaPort);
|
|
655
|
+
if (sessionId) {
|
|
656
|
+
session.sessionId = sessionId;
|
|
657
|
+
logger_1.default.info(`Created WDA session ${sessionId} for ${udid} during stream startup`);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
catch (e) {
|
|
661
|
+
logger_1.default.warn(`Failed to create WDA session during stream startup for ${udid}: ${e.message}`);
|
|
662
|
+
}
|
|
663
|
+
session.lastViewerAt = Date.now(); // Initialize activity
|
|
664
|
+
// Clear recovery cooldown on successful start
|
|
665
|
+
this.recoveryCooldowns.delete(udid);
|
|
666
|
+
return { wdaPort, mjpegPort };
|
|
667
|
+
}
|
|
668
|
+
if (((_c = session.wdaProcess) === null || _c === void 0 ? void 0 : _c.exitCode) !== null) {
|
|
669
|
+
const logContent = fs_extra_1.default.existsSync(wdaRunLog) ? fs_extra_1.default.readFileSync(wdaRunLog, 'utf8') : '';
|
|
670
|
+
throw new Error(`WDA process exited with code ${(_d = session.wdaProcess) === null || _d === void 0 ? void 0 : _d.exitCode}. Log: ${logContent.slice(-200)}`);
|
|
671
|
+
}
|
|
672
|
+
yield new Promise((resolve) => setTimeout(resolve, 1000));
|
|
673
|
+
}
|
|
674
|
+
throw new Error(`WDA failed to start within ${timeout / 1000}s. Check logs.`);
|
|
675
|
+
}
|
|
676
|
+
catch (error) {
|
|
677
|
+
const session = this.sessions.get(udid);
|
|
678
|
+
if (session) {
|
|
679
|
+
session.status = 'error';
|
|
680
|
+
session.lastError = error.message;
|
|
681
|
+
}
|
|
682
|
+
logger_1.default.error(`Stream start failed for ${udid}: ${error.message}`);
|
|
683
|
+
throw error;
|
|
684
|
+
}
|
|
685
|
+
finally {
|
|
686
|
+
this.startPromises.delete(udid);
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
// Set the promise in the map IMMEDIATELY before awaiting anything
|
|
690
|
+
const startPromise = performStartup();
|
|
691
|
+
this.startPromises.set(udid, startPromise);
|
|
692
|
+
return startPromise;
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
killStaleProcesses(udid, wdaPort, mjpegPort) {
|
|
696
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
697
|
+
logger_1.default.info(`Cleaning up stale processes for ${udid} (Ports: ${wdaPort}, ${mjpegPort})`);
|
|
698
|
+
// Senior Resiliency: Use centralized tunnel cleanup
|
|
699
|
+
yield this.cleanupOrphanTunnels(udid);
|
|
700
|
+
const pkillCmds = [`pkill -9 -f "iproxy.*${udid}"`, `pkill -9 -f "ios runwda.*${udid}"`];
|
|
701
|
+
for (const cmd of pkillCmds) {
|
|
702
|
+
try {
|
|
703
|
+
yield execPromise(cmd);
|
|
704
|
+
}
|
|
705
|
+
catch (err) {
|
|
706
|
+
/* ignore */
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
// Port-based cleanup (more surgical)
|
|
710
|
+
const ports = [wdaPort, mjpegPort];
|
|
711
|
+
for (const port of ports) {
|
|
712
|
+
try {
|
|
713
|
+
const { stdout } = yield execPromise(`lsof -ti :${port}`);
|
|
714
|
+
const pids = stdout.trim().split('\n');
|
|
715
|
+
for (const pid of pids) {
|
|
716
|
+
if (pid) {
|
|
717
|
+
logger_1.default.debug(`Killing stale process ${pid} on port ${port}`);
|
|
718
|
+
yield execPromise(`kill -9 ${pid}`);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
catch (err) {
|
|
723
|
+
/* ignore - lsof returns 1 if no port found */
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
// Small delay to ensure OS releases sockets
|
|
727
|
+
yield new Promise((resolve) => setTimeout(resolve, 500));
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
stopStream(udid) {
|
|
731
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
732
|
+
var _a;
|
|
733
|
+
const session = this.sessions.get(udid);
|
|
734
|
+
if (!session)
|
|
735
|
+
return;
|
|
736
|
+
// Kill all processes including tunnel (CRITICAL: tunnel was missing from cleanup!)
|
|
737
|
+
[
|
|
738
|
+
session.wdaProcess,
|
|
739
|
+
session.forwardWDAProcess,
|
|
740
|
+
session.forwardMJPEGProcess,
|
|
741
|
+
session.tunnelProcess,
|
|
742
|
+
].forEach((p) => {
|
|
743
|
+
if (p)
|
|
744
|
+
try {
|
|
745
|
+
p.kill('SIGKILL');
|
|
746
|
+
}
|
|
747
|
+
catch (e) {
|
|
748
|
+
// ignore
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
// Principal Fix: Only release the device lock if THIS STREAM SERVICE owns it.
|
|
752
|
+
// The lock could belong to an Appium automation session (session_id is a real UUID).
|
|
753
|
+
// We should only unblock if it's a manual control lock (session_id starts with 'manual_').
|
|
754
|
+
try {
|
|
755
|
+
const device = yield device_store_1.DeviceStoreFactory.getStore().findDevice({ udid });
|
|
756
|
+
if (device && ((_a = device.session_id) === null || _a === void 0 ? void 0 : _a.startsWith('manual_'))) {
|
|
757
|
+
logger_1.default.info(`Stream Stop: Releasing manual control lock for ${udid}`);
|
|
758
|
+
yield (0, device_service_1.unblockDevice)(udid, device.host);
|
|
759
|
+
}
|
|
760
|
+
else if (device && device.busy) {
|
|
761
|
+
logger_1.default.info(`Stream Stop: Device ${udid} is busy with session ${device.session_id}. NOT releasing lock.`);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
catch (e) {
|
|
765
|
+
logger_1.default.error(`Failed to check/release lock during stream stop for ${udid}: ${e}`);
|
|
766
|
+
}
|
|
767
|
+
// Also clean up any orphan processes (belt and suspenders)
|
|
768
|
+
yield this.cleanupOrphanTunnels(udid);
|
|
769
|
+
this.sessions.delete(udid);
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
updateWDASettings(wdaPort) {
|
|
773
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
774
|
+
const urls = [`http://127.0.0.1:${wdaPort}/appium/settings`];
|
|
775
|
+
const data = {
|
|
776
|
+
settings: {
|
|
777
|
+
mjpegServerPort: 9100,
|
|
778
|
+
mjpegServerFramerate: 15, // Optimal for tunnel stability
|
|
779
|
+
mjpegServerScreenshotQuality: 50, // Reduced quality to avoid ECONNRESETS during swipes
|
|
780
|
+
mjpegScalingFactor: 100,
|
|
781
|
+
},
|
|
782
|
+
};
|
|
783
|
+
for (const url of urls) {
|
|
784
|
+
try {
|
|
785
|
+
logger_1.default.info(`Broadcasting WDA settings to ${url} (15fps/50%)`);
|
|
786
|
+
yield InternalHttpClient_1.InternalHttpClient.post(url, data, { timeout: 10000 });
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
catch (error) {
|
|
790
|
+
logger_1.default.warn(`Failed to broadcast WDA settings via ${url}: ${error.code || error.message}`);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Create a WDA session for keyboard/interaction operations
|
|
797
|
+
* Uses minimal capabilities to avoid disrupting the current app
|
|
798
|
+
* IMPORTANT: First checks if a session already exists (e.g., from active Appium automation)
|
|
799
|
+
* and reuses it to avoid disrupting active automation runs.
|
|
800
|
+
*/
|
|
801
|
+
createWDASession(wdaPort) {
|
|
802
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
803
|
+
var _a, _b, _c, _d;
|
|
804
|
+
const axios = (yield Promise.resolve().then(() => __importStar(require('axios')))).default;
|
|
805
|
+
// SAFETY: First check if a session already exists (e.g., from active Appium session)
|
|
806
|
+
// If yes, reuse it instead of creating a new one that could displace the automation
|
|
807
|
+
try {
|
|
808
|
+
const existingResponse = yield axios.get(`http://127.0.0.1:${wdaPort}/sessions`, {
|
|
809
|
+
timeout: 5000,
|
|
810
|
+
});
|
|
811
|
+
const sessions = ((_a = existingResponse.data) === null || _a === void 0 ? void 0 : _a.value) || [];
|
|
812
|
+
if (sessions.length > 0) {
|
|
813
|
+
const existingSid = sessions[0].id || sessions[0].sessionId;
|
|
814
|
+
if (existingSid) {
|
|
815
|
+
logger_1.default.info(`Reusing existing WDA session ${existingSid} (likely from active automation)`);
|
|
816
|
+
return existingSid;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
catch (err) {
|
|
821
|
+
logger_1.default.debug(`No existing WDA sessions found: ${err.message}`);
|
|
822
|
+
}
|
|
823
|
+
// No existing session - create a new one with minimal capabilities
|
|
824
|
+
const sessionConfigs = [
|
|
825
|
+
// Minimal session - doesn't specify bundleId, uses current app
|
|
826
|
+
{ capabilities: { alwaysMatch: {} } },
|
|
827
|
+
// Springboard fallback
|
|
828
|
+
{ capabilities: { alwaysMatch: { bundleId: 'com.apple.springboard' } } },
|
|
829
|
+
];
|
|
830
|
+
for (const config of sessionConfigs) {
|
|
831
|
+
try {
|
|
832
|
+
const response = yield axios.post(`http://127.0.0.1:${wdaPort}/session`, config, {
|
|
833
|
+
timeout: 15000,
|
|
834
|
+
headers: { 'Content-Type': 'application/json' },
|
|
835
|
+
});
|
|
836
|
+
const sid = ((_b = response.data) === null || _b === void 0 ? void 0 : _b.sessionId) || ((_d = (_c = response.data) === null || _c === void 0 ? void 0 : _c.value) === null || _d === void 0 ? void 0 : _d.sessionId);
|
|
837
|
+
if (sid) {
|
|
838
|
+
return sid;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
catch (err) {
|
|
842
|
+
logger_1.default.debug(`WDA session creation attempt failed: ${err.message}`);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return null;
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
getStreamStatus(udid) {
|
|
849
|
+
return this.sessions.get(udid);
|
|
850
|
+
}
|
|
851
|
+
updateViewerCount(udid, delta) {
|
|
852
|
+
const session = this.sessions.get(udid);
|
|
853
|
+
if (session) {
|
|
854
|
+
session.viewerCount = Math.max(0, session.viewerCount + delta);
|
|
855
|
+
session.lastViewerAt = Date.now();
|
|
856
|
+
logger_1.default.debug(`[${udid}] iOS Stream viewer update: delta=${delta}, total=${session.viewerCount}`);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
cleanup() {
|
|
860
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
861
|
+
for (const udid of this.sessions.keys())
|
|
862
|
+
yield this.stopStream(udid);
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Get the cached WDA session ID for a device
|
|
867
|
+
*/
|
|
868
|
+
getWDASessionId(udid) {
|
|
869
|
+
var _a;
|
|
870
|
+
return (_a = this.sessions.get(udid)) === null || _a === void 0 ? void 0 : _a.sessionId;
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Set/update the WDA session ID for a device.
|
|
874
|
+
* Passing undefined will clear the cached session.
|
|
875
|
+
*/
|
|
876
|
+
setWDASessionId(udid, sessionId) {
|
|
877
|
+
const session = this.sessions.get(udid);
|
|
878
|
+
if (session) {
|
|
879
|
+
session.sessionId = sessionId;
|
|
880
|
+
if (sessionId) {
|
|
881
|
+
logger_1.default.info(`Cached WDA Session ID for ${udid}: ${sessionId}`);
|
|
882
|
+
}
|
|
883
|
+
else {
|
|
884
|
+
logger_1.default.debug(`Cleared cached WDA Session ID for ${udid}`);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
IOSStreamService = __decorate([
|
|
890
|
+
(0, typedi_1.Service)({ name: 'IOSStreamService' }),
|
|
891
|
+
__metadata("design:paramtypes", [])
|
|
892
|
+
], IOSStreamService);
|
|
893
|
+
exports.default = IOSStreamService;
|