@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,370 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
12
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
13
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
14
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
15
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
16
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
17
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
21
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
22
|
+
};
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
const child_process_1 = require("child_process");
|
|
25
|
+
const http_1 = __importDefault(require("http"));
|
|
26
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
27
|
+
const helpers_1 = require("../../helpers");
|
|
28
|
+
const device_store_1 = require("../../data-service/device-store");
|
|
29
|
+
const device_service_1 = require("../../data-service/device-service");
|
|
30
|
+
const DeviceLockManager_1 = require("./DeviceLockManager");
|
|
31
|
+
const typedi_1 = require("typedi");
|
|
32
|
+
const ResourceIsolationService_1 = require("../../services/ResourceIsolationService");
|
|
33
|
+
// FFmpeg is optional - only used for raw-to-JPEG conversion
|
|
34
|
+
let FFMPEG_PATH = null;
|
|
35
|
+
try {
|
|
36
|
+
// Dynamic require for optional dependency
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
38
|
+
FFMPEG_PATH = require('@ffmpeg-installer/ffmpeg').path;
|
|
39
|
+
}
|
|
40
|
+
catch (_a) {
|
|
41
|
+
// ffmpeg not installed - streaming will be unavailable
|
|
42
|
+
}
|
|
43
|
+
let AndroidStreamService = class AndroidStreamService {
|
|
44
|
+
constructor() {
|
|
45
|
+
this.sessions = new Map();
|
|
46
|
+
this.startPromises = new Map();
|
|
47
|
+
this.startWatchdog();
|
|
48
|
+
}
|
|
49
|
+
startWatchdog() {
|
|
50
|
+
setInterval(() => __awaiter(this, void 0, void 0, function* () {
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
for (const [udid, session] of this.sessions.entries()) {
|
|
53
|
+
if (session.status === 'running') {
|
|
54
|
+
if (now - session.lastViewerAt > 600000 && session.viewerCount === 0) {
|
|
55
|
+
logger_1.default.info(`[${udid}] Stopping idle stream (No active viewers for 600s)`);
|
|
56
|
+
this.stopStream(udid);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}), 3600000);
|
|
61
|
+
}
|
|
62
|
+
captureLoop(udid, session) {
|
|
63
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
64
|
+
logger_1.default.info(`[${udid}] Background capture loop started.`);
|
|
65
|
+
while (session.status === 'running' || session.status === 'starting') {
|
|
66
|
+
if (session.viewerCount === 0 && session.status === 'running') {
|
|
67
|
+
yield new Promise((r) => setTimeout(r, 1000));
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
// Respect device interaction lock
|
|
71
|
+
if (DeviceLockManager_1.deviceLock.isLocked(udid)) {
|
|
72
|
+
yield new Promise((r) => setTimeout(r, 150));
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const startTime = Date.now();
|
|
77
|
+
// High-Speed Binary Snapshot
|
|
78
|
+
const screenshot = yield DeviceLockManager_1.deviceLock.acquire(udid, () => __awaiter(this, void 0, void 0, function* () {
|
|
79
|
+
return yield new Promise((resolve, reject) => {
|
|
80
|
+
const isolationService = typedi_1.Container.get(ResourceIsolationService_1.ResourceIsolationService);
|
|
81
|
+
const { command, args } = isolationService.wrapSpawn('adb', ['-s', udid, 'exec-out', 'screencap'], 'Performance');
|
|
82
|
+
const proc = (0, child_process_1.spawn)(command, args);
|
|
83
|
+
const chunks = [];
|
|
84
|
+
proc.stdout.on('data', (c) => chunks.push(c));
|
|
85
|
+
proc.on('close', (code) => {
|
|
86
|
+
if (code === 0)
|
|
87
|
+
resolve(Buffer.concat(chunks));
|
|
88
|
+
else
|
|
89
|
+
reject(new Error(`ADB Exit ${code}`));
|
|
90
|
+
});
|
|
91
|
+
proc.on('error', reject);
|
|
92
|
+
setTimeout(() => {
|
|
93
|
+
proc.kill();
|
|
94
|
+
reject(new Error('ADB Timeout (15s)'));
|
|
95
|
+
}, 15000);
|
|
96
|
+
});
|
|
97
|
+
}));
|
|
98
|
+
if (screenshot.length > 16) {
|
|
99
|
+
const w = screenshot.readInt32LE(0);
|
|
100
|
+
const h = screenshot.readInt32LE(4);
|
|
101
|
+
const fmt = screenshot.readInt32LE(8);
|
|
102
|
+
// Auto-detect header size (16 for modern Android, 12 for legacy)
|
|
103
|
+
const headerSize = w * h * 4 + 16 === screenshot.length ? 16 : 12;
|
|
104
|
+
const pixels = screenshot.slice(headerSize);
|
|
105
|
+
if (pixels.length >= w * h * 4) {
|
|
106
|
+
// Sanity check: Extreme small images or weird ratios are often corrupted buffers
|
|
107
|
+
if (w < 100 || h < 100 || w > 8000 || h > 8000) {
|
|
108
|
+
logger_1.default.debug(`[${udid}] Suspicious frame detected (${w}x${h}). Skipping.`);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const pixFmt = fmt === 1 ? 'rgba' : 'rgb565';
|
|
112
|
+
// Optimization: Downscale to 720p maximum width for efficiency
|
|
113
|
+
let targetW = w;
|
|
114
|
+
let targetH = h;
|
|
115
|
+
if (w > 720) {
|
|
116
|
+
targetW = 720;
|
|
117
|
+
targetH = Math.round((h / w) * targetW);
|
|
118
|
+
}
|
|
119
|
+
// FFmpeg requires even dimensions for some encoders/filters
|
|
120
|
+
if (targetW % 2 !== 0)
|
|
121
|
+
targetW--;
|
|
122
|
+
if (targetH % 2 !== 0)
|
|
123
|
+
targetH--;
|
|
124
|
+
session.latestFrame = yield this.convertRawToJpeg(pixels, w, h, targetW, targetH, pixFmt);
|
|
125
|
+
if (!session.latestFrameTimestamp) {
|
|
126
|
+
logger_1.default.info(`[${udid}] First Android frame successfully captured and converted.`);
|
|
127
|
+
}
|
|
128
|
+
session.latestFrameTimestamp = Date.now();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
logger_1.default.warn(`[${udid}] Received empty or tiny screenshot (${screenshot.length} bytes). Is the screen 'flag_secure'?`);
|
|
133
|
+
}
|
|
134
|
+
const elapsed = Date.now() - startTime;
|
|
135
|
+
const delay = Math.max(5, 70 - elapsed); // Target ~14 FPS
|
|
136
|
+
yield new Promise((r) => setTimeout(r, delay));
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
logger_1.default.debug(`[${udid}] Capture failure: ${e.message}`);
|
|
140
|
+
yield new Promise((r) => setTimeout(r, 1000));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
logger_1.default.info(`[${udid}] Background capture loop stopped.`);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
startStream(udid) {
|
|
147
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
148
|
+
if (this.startPromises.has(udid))
|
|
149
|
+
return this.startPromises.get(udid);
|
|
150
|
+
const performStartup = () => __awaiter(this, void 0, void 0, function* () {
|
|
151
|
+
try {
|
|
152
|
+
const existing = this.sessions.get(udid);
|
|
153
|
+
if (existing && (existing.status === 'running' || existing.status === 'starting')) {
|
|
154
|
+
return { mjpegPort: existing.mjpegPort };
|
|
155
|
+
}
|
|
156
|
+
const device = yield device_store_1.DeviceStoreFactory.getStore().findDevice({ udid });
|
|
157
|
+
if (!device)
|
|
158
|
+
throw new Error(`Device ${udid} not found in DB`);
|
|
159
|
+
const mjpegPort = yield (0, helpers_1.getFreePort)();
|
|
160
|
+
// Aggressive cleanup: Kill any process already using this port
|
|
161
|
+
try {
|
|
162
|
+
const { exec } = require('child_process');
|
|
163
|
+
const { promisify } = require('util');
|
|
164
|
+
const execPromise = promisify(exec);
|
|
165
|
+
const { stdout } = yield execPromise(`lsof -ti :${mjpegPort}`);
|
|
166
|
+
const pids = stdout.trim().split('\n');
|
|
167
|
+
for (const pid of pids) {
|
|
168
|
+
if (pid) {
|
|
169
|
+
logger_1.default.debug(`[${udid}] Killing stale process ${pid} on port ${mjpegPort}`);
|
|
170
|
+
yield execPromise(`kill -9 ${pid}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch (e) {
|
|
175
|
+
/* ignore */
|
|
176
|
+
}
|
|
177
|
+
const session = {
|
|
178
|
+
udid,
|
|
179
|
+
mjpegPort,
|
|
180
|
+
server: null,
|
|
181
|
+
status: 'starting',
|
|
182
|
+
lastViewerAt: Date.now(),
|
|
183
|
+
viewerCount: 0,
|
|
184
|
+
};
|
|
185
|
+
this.sessions.set(udid, session);
|
|
186
|
+
session.server = http_1.default.createServer((req, res) => {
|
|
187
|
+
session.viewerCount++;
|
|
188
|
+
session.lastViewerAt = Date.now();
|
|
189
|
+
res.writeHead(200, {
|
|
190
|
+
'Content-Type': 'multipart/x-mixed-replace; boundary="BoundaryString"',
|
|
191
|
+
'Cache-Control': 'no-cache',
|
|
192
|
+
Connection: 'close',
|
|
193
|
+
Pragma: 'no-cache',
|
|
194
|
+
});
|
|
195
|
+
const lastFrameTime = 0;
|
|
196
|
+
const writeFrame = () => __awaiter(this, void 0, void 0, function* () {
|
|
197
|
+
// Principle: Handle both starting and running states to avoid race conditions
|
|
198
|
+
if (session.status === 'stopped' ||
|
|
199
|
+
session.status === 'error' ||
|
|
200
|
+
res.writableEnded ||
|
|
201
|
+
!res.writable)
|
|
202
|
+
return;
|
|
203
|
+
if (session.latestFrame) {
|
|
204
|
+
try {
|
|
205
|
+
res.write('--BoundaryString\r\n');
|
|
206
|
+
res.write('Content-Type: image/jpeg\r\n');
|
|
207
|
+
res.write(`Content-Length: ${session.latestFrame.length}\r\n\r\n`);
|
|
208
|
+
res.write(session.latestFrame);
|
|
209
|
+
res.write('\r\n');
|
|
210
|
+
}
|
|
211
|
+
catch (e) {
|
|
212
|
+
return; // Stop if socket broke
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
setTimeout(writeFrame, 60);
|
|
216
|
+
});
|
|
217
|
+
writeFrame();
|
|
218
|
+
req.on('close', () => {
|
|
219
|
+
session.viewerCount = Math.max(0, session.viewerCount - 1);
|
|
220
|
+
session.lastViewerAt = Date.now();
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
session.server.listen(mjpegPort, '127.0.0.1');
|
|
224
|
+
// Principal Resilience: Wait for port to be actually listening
|
|
225
|
+
// This prevents FFMPEG race conditions where it tries to connect before the socket is open
|
|
226
|
+
const tcpPortUsed = require('tcp-port-used');
|
|
227
|
+
try {
|
|
228
|
+
yield tcpPortUsed.waitUntilUsed(mjpegPort, 200, 2000);
|
|
229
|
+
logger_1.default.info(`[${udid}] MJPEG stream confirmed active on port ${mjpegPort}`);
|
|
230
|
+
}
|
|
231
|
+
catch (e) {
|
|
232
|
+
logger_1.default.warn(`[${udid}] MJPEG port check timed out, proceeding anyway: ${e}`);
|
|
233
|
+
}
|
|
234
|
+
session.status = 'running';
|
|
235
|
+
this.captureLoop(udid, session);
|
|
236
|
+
// Principal Resilience: Wait for the FIRST FRAME to be captured
|
|
237
|
+
// FFMPEG often fails to "open" the input if there is no data immediately available
|
|
238
|
+
const firstFrameStartTime = Date.now();
|
|
239
|
+
const firstFrameTimeout = 5000;
|
|
240
|
+
let frameCount = 0;
|
|
241
|
+
while (Date.now() - firstFrameStartTime < firstFrameTimeout) {
|
|
242
|
+
if (session.latestFrame) {
|
|
243
|
+
frameCount++;
|
|
244
|
+
if (frameCount >= 1)
|
|
245
|
+
break; // We have at least one frame ready to serve
|
|
246
|
+
}
|
|
247
|
+
yield new Promise((r) => setTimeout(r, 200));
|
|
248
|
+
}
|
|
249
|
+
if (!session.latestFrame) {
|
|
250
|
+
logger_1.default.warn(`[${udid}] Stream started but no frames captured after ${firstFrameTimeout}ms. Downstream video might be unstable.`);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
logger_1.default.info(`[${udid}] MJPEG stream primed with first frame.`);
|
|
254
|
+
}
|
|
255
|
+
// Update device info in store to ensure other services (like VideoPipeline) can find the port
|
|
256
|
+
const updatedDevice = yield device_store_1.DeviceStoreFactory.getStore().findDevice({ udid });
|
|
257
|
+
if (updatedDevice) {
|
|
258
|
+
yield device_store_1.DeviceStoreFactory.getStore().updateDevice(udid, updatedDevice.host, {
|
|
259
|
+
mjpegServerPort: mjpegPort,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return { mjpegPort };
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
logger_1.default.error(`[${udid}] Stream failed to start: ${error.message}`);
|
|
266
|
+
this.stopStream(udid);
|
|
267
|
+
throw error;
|
|
268
|
+
}
|
|
269
|
+
finally {
|
|
270
|
+
this.startPromises.delete(udid);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
const promise = performStartup();
|
|
274
|
+
this.startPromises.set(udid, promise);
|
|
275
|
+
return promise;
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
stopStream(udid) {
|
|
279
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
280
|
+
var _a;
|
|
281
|
+
const session = this.sessions.get(udid);
|
|
282
|
+
if (session) {
|
|
283
|
+
session.status = 'stopped';
|
|
284
|
+
if (session.server) {
|
|
285
|
+
session.server.close();
|
|
286
|
+
session.server = null;
|
|
287
|
+
}
|
|
288
|
+
// Principal Fix: Only release the device lock if THIS STREAM SERVICE owns it.
|
|
289
|
+
// If an Appium automation session owns the lock, don't touch it.
|
|
290
|
+
try {
|
|
291
|
+
const device = yield device_store_1.DeviceStoreFactory.getStore().findDevice({ udid });
|
|
292
|
+
if (device && ((_a = device.session_id) === null || _a === void 0 ? void 0 : _a.startsWith('manual_'))) {
|
|
293
|
+
logger_1.default.info(`[${udid}] Stream Stop: Releasing manual control lock.`);
|
|
294
|
+
yield (0, device_service_1.unblockDevice)(udid, device.host);
|
|
295
|
+
}
|
|
296
|
+
else if (device && device.busy) {
|
|
297
|
+
logger_1.default.info(`[${udid}] Stream Stop: Device busy with session ${device.session_id}. NOT releasing lock.`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
catch (e) {
|
|
301
|
+
/* Ignore unblocking failure on stop */
|
|
302
|
+
}
|
|
303
|
+
this.sessions.delete(udid);
|
|
304
|
+
logger_1.default.info(`[${udid}] Stream terminated.`);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
getStreamStatus(udid) {
|
|
309
|
+
return this.sessions.get(udid);
|
|
310
|
+
}
|
|
311
|
+
updateViewerCount(udid, delta) {
|
|
312
|
+
const session = this.sessions.get(udid);
|
|
313
|
+
if (session) {
|
|
314
|
+
session.viewerCount = Math.max(0, session.viewerCount + delta);
|
|
315
|
+
session.lastViewerAt = Date.now();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
cleanup() {
|
|
319
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
320
|
+
for (const udid of this.sessions.keys()) {
|
|
321
|
+
yield this.stopStream(udid);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
convertRawToJpeg(pixels, srcW, srcH, dstW, dstH, pixFmt) {
|
|
326
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
327
|
+
return new Promise((resolve, reject) => {
|
|
328
|
+
if (!FFMPEG_PATH)
|
|
329
|
+
return reject(new Error('No FFmpeg on path'));
|
|
330
|
+
const isolationService = typedi_1.Container.get(ResourceIsolationService_1.ResourceIsolationService);
|
|
331
|
+
const { command, args } = isolationService.wrapSpawn(FFMPEG_PATH, [
|
|
332
|
+
'-f',
|
|
333
|
+
'rawvideo',
|
|
334
|
+
'-pixel_format',
|
|
335
|
+
pixFmt,
|
|
336
|
+
'-video_size',
|
|
337
|
+
`${srcW}x${srcH}`,
|
|
338
|
+
'-i',
|
|
339
|
+
'pipe:0',
|
|
340
|
+
'-vf',
|
|
341
|
+
`scale=${dstW}:-1`,
|
|
342
|
+
'-f',
|
|
343
|
+
'mjpeg',
|
|
344
|
+
'-q:v',
|
|
345
|
+
'8', // Goldilocks quality for low lag
|
|
346
|
+
'-frames:v',
|
|
347
|
+
'1',
|
|
348
|
+
'pipe:1',
|
|
349
|
+
], 'Performance');
|
|
350
|
+
const ff = (0, child_process_1.spawn)(command, args);
|
|
351
|
+
const output = [];
|
|
352
|
+
ff.stdout.on('data', (d) => output.push(d));
|
|
353
|
+
ff.on('close', (code) => {
|
|
354
|
+
if (code === 0)
|
|
355
|
+
resolve(Buffer.concat(output));
|
|
356
|
+
else
|
|
357
|
+
reject(new Error(`FFmpeg exit ${code}`));
|
|
358
|
+
});
|
|
359
|
+
ff.on('error', reject);
|
|
360
|
+
ff.stdin.write(pixels);
|
|
361
|
+
ff.stdin.end();
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
AndroidStreamService = __decorate([
|
|
367
|
+
(0, typedi_1.Service)({ name: 'AndroidStreamService' }),
|
|
368
|
+
__metadata("design:paramtypes", [])
|
|
369
|
+
], AndroidStreamService);
|
|
370
|
+
exports.default = AndroidStreamService;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
12
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
13
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
14
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
15
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
16
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
17
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
21
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
22
|
+
};
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.deviceLock = exports.DeviceLockManager = void 0;
|
|
25
|
+
const async_lock_1 = __importDefault(require("async-lock"));
|
|
26
|
+
const typedi_1 = require("typedi");
|
|
27
|
+
let DeviceLockManager = class DeviceLockManager {
|
|
28
|
+
constructor() {
|
|
29
|
+
this.lock = new async_lock_1.default();
|
|
30
|
+
}
|
|
31
|
+
acquire(udid, fn) {
|
|
32
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
33
|
+
return yield this.lock.acquire(udid, fn);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
isLocked(udid) {
|
|
37
|
+
return this.lock.isBusy(udid);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
exports.DeviceLockManager = DeviceLockManager;
|
|
41
|
+
exports.DeviceLockManager = DeviceLockManager = __decorate([
|
|
42
|
+
(0, typedi_1.Service)(),
|
|
43
|
+
__metadata("design:paramtypes", [])
|
|
44
|
+
], DeviceLockManager);
|
|
45
|
+
exports.deviceLock = typedi_1.Container.get(DeviceLockManager);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const Cloud_1 = __importDefault(require("../../enums/Cloud"));
|
|
7
|
+
class CapabilityManager {
|
|
8
|
+
constructor(capabilities, freeDevice) {
|
|
9
|
+
this.capabilities = capabilities;
|
|
10
|
+
this.freeDevice = freeDevice;
|
|
11
|
+
}
|
|
12
|
+
getCapability() {
|
|
13
|
+
if (this.freeDevice.capability) {
|
|
14
|
+
const entries = Object.entries(this.freeDevice.capability);
|
|
15
|
+
entries.map(([key, val]) => {
|
|
16
|
+
this.capabilities.alwaysMatch[`appium:${key}`] = val;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
if (this.freeDevice.cloud.toLowerCase() === Cloud_1.default.PCLOUDY) {
|
|
20
|
+
this.capabilities.alwaysMatch['appium:pCloudy_ApiKey'] = process.env.CLOUD_KEY;
|
|
21
|
+
this.capabilities.alwaysMatch['appium:pCloudy_Username'] = process.env.CLOUD_USERNAME;
|
|
22
|
+
}
|
|
23
|
+
return this.capabilities;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
exports.default = CapabilityManager;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
/* eslint-disable no-prototype-builtins */
|
|
16
|
+
const Cloud_1 = __importDefault(require("../../enums/Cloud"));
|
|
17
|
+
const CloudSchema_1 = require("../../types/CloudSchema");
|
|
18
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
19
|
+
const jsonschema_1 = require("jsonschema");
|
|
20
|
+
const uuid_1 = require("uuid");
|
|
21
|
+
class Devices {
|
|
22
|
+
constructor(cloudArgs, deviceState, platform) {
|
|
23
|
+
this.devices = cloudArgs.devices;
|
|
24
|
+
this.deviceState = deviceState;
|
|
25
|
+
this.platform = platform;
|
|
26
|
+
this.cloud = cloudArgs;
|
|
27
|
+
this.validateSchema(CloudSchema_1.defaultSchema);
|
|
28
|
+
}
|
|
29
|
+
getDevices() {
|
|
30
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
31
|
+
const devicesByPlatform = this.devices.filter((value) => value.platform === this.platform);
|
|
32
|
+
let cloudDeviceProperties;
|
|
33
|
+
const result = devicesByPlatform.map((d) => {
|
|
34
|
+
if (this.isBrowserStack()) {
|
|
35
|
+
this.validateSchema(CloudSchema_1.browserStackSchema);
|
|
36
|
+
cloudDeviceProperties = {
|
|
37
|
+
name: d.deviceName,
|
|
38
|
+
sdk: d['os_version'],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (this.isSauceLabs() || this.isLambdaTest() || this.isHeadSpin()) {
|
|
42
|
+
this.validateSchema(CloudSchema_1.sauceOrLambdaSchema);
|
|
43
|
+
cloudDeviceProperties = {
|
|
44
|
+
name: d.deviceName,
|
|
45
|
+
sdk: d.platformVersion,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (this.isPCloudy()) {
|
|
49
|
+
this.validateSchema(CloudSchema_1.pCloudySchema);
|
|
50
|
+
cloudDeviceProperties = {
|
|
51
|
+
name: (d === null || d === void 0 ? void 0 : d.pCloudy_DeviceFullName) || (d === null || d === void 0 ? void 0 : d.pCloudy_DeviceManufacturer),
|
|
52
|
+
sdk: (d === null || d === void 0 ? void 0 : d.pCloudy_DeviceVersion) || (d === null || d === void 0 ? void 0 : d.platformVersion),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return Object.assign({}, ...devicesByPlatform, Object.assign({ host: this.cloud.url, busy: false, udid: (0, uuid_1.v4)(), userBlocked: false, deviceType: 'real', capability: d, cloud: this.cloud.cloudName }, cloudDeviceProperties));
|
|
56
|
+
});
|
|
57
|
+
this.deviceState.push(...result);
|
|
58
|
+
return this.deviceState;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
validateSchema(schema) {
|
|
62
|
+
const v = new jsonschema_1.Validator();
|
|
63
|
+
const validationResult = v.validate(this.cloud, schema);
|
|
64
|
+
if (validationResult.errors.length) {
|
|
65
|
+
throw new Error(`🔴 Invalid server config ${validationResult.errors} 🔴`);
|
|
66
|
+
}
|
|
67
|
+
else
|
|
68
|
+
logger_1.default.info('Loading devices from server config');
|
|
69
|
+
}
|
|
70
|
+
isBrowserStack() {
|
|
71
|
+
return this.cloud.cloudName.toLowerCase() === Cloud_1.default.BROWSERSTACK;
|
|
72
|
+
}
|
|
73
|
+
isPCloudy() {
|
|
74
|
+
return this.cloud.cloudName.toLowerCase() === Cloud_1.default.PCLOUDY;
|
|
75
|
+
}
|
|
76
|
+
isLambdaTest() {
|
|
77
|
+
return this.cloud.cloudName.toLowerCase() === Cloud_1.default.LAMBDATEST;
|
|
78
|
+
}
|
|
79
|
+
isSauceLabs() {
|
|
80
|
+
return this.cloud.cloudName.toLowerCase() === Cloud_1.default.SAUCELABS;
|
|
81
|
+
}
|
|
82
|
+
isHeadSpin() {
|
|
83
|
+
return this.cloud.cloudName.toLowerCase() === Cloud_1.default.HEADSPIN;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
exports.default = Devices;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
12
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
13
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
14
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
15
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
16
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
17
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
21
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
22
|
+
};
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.IosTracker = void 0;
|
|
25
|
+
const usbmux_1 = __importDefault(require("usbmux"));
|
|
26
|
+
const typedi_1 = require("typedi");
|
|
27
|
+
let IosTracker = class IosTracker {
|
|
28
|
+
constructor() {
|
|
29
|
+
this.listener = new usbmux_1.default.createListener();
|
|
30
|
+
}
|
|
31
|
+
getListener() {
|
|
32
|
+
return this.listener;
|
|
33
|
+
}
|
|
34
|
+
stop() {
|
|
35
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
36
|
+
this.listener.end();
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
exports.IosTracker = IosTracker;
|
|
41
|
+
exports.IosTracker = IosTracker = __decorate([
|
|
42
|
+
(0, typedi_1.Service)(),
|
|
43
|
+
__metadata("design:paramtypes", [])
|
|
44
|
+
], IosTracker);
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
12
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
13
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
14
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
15
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
16
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
17
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
21
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
22
|
+
};
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.XenonManager = void 0;
|
|
25
|
+
const AndroidDeviceManager_1 = __importDefault(require("./AndroidDeviceManager"));
|
|
26
|
+
const IOSDeviceManager_1 = __importDefault(require("./IOSDeviceManager"));
|
|
27
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
28
|
+
const typedi_1 = require("typedi");
|
|
29
|
+
const PluginContext_1 = require("../PluginContext");
|
|
30
|
+
let XenonManager = class XenonManager {
|
|
31
|
+
constructor(context) {
|
|
32
|
+
this.context = context;
|
|
33
|
+
this.log = logger_1.default.scope('XenonManager');
|
|
34
|
+
this.deviceManagers = [];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Initializes the managers based on the platform requested.
|
|
38
|
+
* This allows for lazy initialization after the PluginContext is ready.
|
|
39
|
+
*/
|
|
40
|
+
init() {
|
|
41
|
+
this.deviceManagers = [];
|
|
42
|
+
const platform = this.context.pluginArgs.platform.toLowerCase();
|
|
43
|
+
if (platform === 'both') {
|
|
44
|
+
this.deviceManagers.push(typedi_1.Container.get(AndroidDeviceManager_1.default));
|
|
45
|
+
this.deviceManagers.push(typedi_1.Container.get(IOSDeviceManager_1.default));
|
|
46
|
+
}
|
|
47
|
+
else if (platform === 'android') {
|
|
48
|
+
this.deviceManagers.push(typedi_1.Container.get(AndroidDeviceManager_1.default));
|
|
49
|
+
}
|
|
50
|
+
else if (platform === 'ios') {
|
|
51
|
+
this.deviceManagers.push(typedi_1.Container.get(IOSDeviceManager_1.default));
|
|
52
|
+
}
|
|
53
|
+
this.log.info(`Initialized with ${this.deviceManagers.length} device managers`);
|
|
54
|
+
}
|
|
55
|
+
getDevices(existingDeviceDetails) {
|
|
56
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
57
|
+
const devices = [];
|
|
58
|
+
// Auto-init if not already done
|
|
59
|
+
if (this.deviceManagers.length === 0) {
|
|
60
|
+
this.init();
|
|
61
|
+
}
|
|
62
|
+
for (const deviceManager of this.deviceManagers) {
|
|
63
|
+
devices.push(...(yield deviceManager.getDevices({
|
|
64
|
+
androidDeviceType: this.context.pluginArgs.androidDeviceType,
|
|
65
|
+
iosDeviceType: this.context.pluginArgs.iosDeviceType,
|
|
66
|
+
}, existingDeviceDetails || [])).map((device) => {
|
|
67
|
+
return Object.assign(Object.assign({}, device), { nodeId: !device.cloud ? this.context.nodeId : undefined });
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
return devices;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
getMaxSessionCount() {
|
|
74
|
+
return this.context.pluginArgs.maxSessions;
|
|
75
|
+
}
|
|
76
|
+
deviceInstances() {
|
|
77
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
78
|
+
if (this.deviceManagers.length === 0) {
|
|
79
|
+
this.init();
|
|
80
|
+
}
|
|
81
|
+
return this.deviceManagers;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
exports.XenonManager = XenonManager;
|
|
86
|
+
exports.XenonManager = XenonManager = __decorate([
|
|
87
|
+
(0, typedi_1.Service)(),
|
|
88
|
+
__metadata("design:paramtypes", [PluginContext_1.PluginContext])
|
|
89
|
+
], XenonManager);
|