@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,866 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
9
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
10
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
11
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
12
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
13
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
14
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
18
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
|
+
};
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.WDAClient = void 0;
|
|
22
|
+
const axios_1 = __importDefault(require("axios"));
|
|
23
|
+
const semver_1 = __importDefault(require("semver"));
|
|
24
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
25
|
+
const typedi_1 = require("typedi");
|
|
26
|
+
const IOSStreamService_1 = __importDefault(require("./IOSStreamService"));
|
|
27
|
+
const device_store_1 = require("../../data-service/device-store");
|
|
28
|
+
const child_process_1 = require("child_process");
|
|
29
|
+
const util_1 = require("util");
|
|
30
|
+
const readline_1 = __importDefault(require("readline"));
|
|
31
|
+
const path_1 = __importDefault(require("path"));
|
|
32
|
+
const os_1 = __importDefault(require("os"));
|
|
33
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
34
|
+
const execFilePromise = (0, util_1.promisify)(child_process_1.execFile);
|
|
35
|
+
const execPromise = (0, util_1.promisify)(child_process_1.exec);
|
|
36
|
+
let WDAClient = class WDAClient {
|
|
37
|
+
constructor() {
|
|
38
|
+
this.log = logger_1.default.scope('WDAClient');
|
|
39
|
+
this.wdaConnectionCache = new Map();
|
|
40
|
+
this.commandQueues = new Map();
|
|
41
|
+
this.ideviceinstallerVersion = null;
|
|
42
|
+
this.typeBuffers = new Map();
|
|
43
|
+
}
|
|
44
|
+
executeSerializedCommand(udid, action) {
|
|
45
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
46
|
+
const currentQueue = this.commandQueues.get(udid) || Promise.resolve();
|
|
47
|
+
const nextInQueue = currentQueue
|
|
48
|
+
.catch(() => { })
|
|
49
|
+
.then(() => action())
|
|
50
|
+
.finally(() => {
|
|
51
|
+
if (this.commandQueues.get(udid) === nextInQueue)
|
|
52
|
+
this.commandQueues.delete(udid);
|
|
53
|
+
});
|
|
54
|
+
this.commandQueues.set(udid, nextInQueue);
|
|
55
|
+
return nextInQueue;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
sendWDACommand(udid, method, endpoint, data, options) {
|
|
59
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
60
|
+
if (['/status', '/health'].includes(endpoint) ||
|
|
61
|
+
(method === 'get' && endpoint === '/sessions')) {
|
|
62
|
+
return this.performWDACommand(udid, method, endpoint, data, 0, options);
|
|
63
|
+
}
|
|
64
|
+
return this.executeSerializedCommand(udid, () => this.performWDACommand(udid, method, endpoint, data, 0, options));
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
performWDACommand(udid_1, method_1, endpoint_1, data_1) {
|
|
68
|
+
return __awaiter(this, arguments, void 0, function* (udid, method, endpoint, data, retryCount = 0, options) {
|
|
69
|
+
var _a, _b, _c;
|
|
70
|
+
const device = yield device_store_1.DeviceStoreFactory.getStore().findDevice({ udid });
|
|
71
|
+
if (!device)
|
|
72
|
+
throw new Error(`Device ${udid} not found`);
|
|
73
|
+
const streamStatus = typedi_1.Container.get(IOSStreamService_1.default).getStreamStatus(udid);
|
|
74
|
+
const port = (streamStatus === null || streamStatus === void 0 ? void 0 : streamStatus.status) === 'running' || (streamStatus === null || streamStatus === void 0 ? void 0 : streamStatus.status) === 'starting'
|
|
75
|
+
? streamStatus.wdaPort
|
|
76
|
+
: device.wdaLocalPort;
|
|
77
|
+
if (!port)
|
|
78
|
+
throw new Error(`WDA port not available for ${udid}`);
|
|
79
|
+
const cacheKey = `${udid}:${port}`;
|
|
80
|
+
let cached = this.wdaConnectionCache.get(cacheKey);
|
|
81
|
+
if (!(cached === null || cached === void 0 ? void 0 : cached.sessionId)) {
|
|
82
|
+
const sid = typedi_1.Container.get(IOSStreamService_1.default).getWDASessionId(udid);
|
|
83
|
+
if (sid) {
|
|
84
|
+
cached = { host: '127.0.0.1', pathPrefix: `/session/${sid}`, sessionId: sid };
|
|
85
|
+
this.wdaConnectionCache.set(cacheKey, cached);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const prefixes = (cached === null || cached === void 0 ? void 0 : cached.sessionId) ? [`/session/${cached.sessionId}`] : ['', '/session/any'];
|
|
89
|
+
let lastError;
|
|
90
|
+
// Principal Intelligence: Prioritize the host that worked last time.
|
|
91
|
+
// Usually localhost (tunnel) is fastest, but if it's lagging, network IP is a solid fallback.
|
|
92
|
+
const hosts = [];
|
|
93
|
+
if (cached === null || cached === void 0 ? void 0 : cached.host) {
|
|
94
|
+
hosts.push(cached.host);
|
|
95
|
+
if (cached.host === '127.0.0.1' && device.ip)
|
|
96
|
+
hosts.push(device.ip);
|
|
97
|
+
if (cached.host !== '127.0.0.1')
|
|
98
|
+
hosts.push('127.0.0.1');
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
hosts.push('127.0.0.1');
|
|
102
|
+
if (device.ip)
|
|
103
|
+
hosts.push(device.ip);
|
|
104
|
+
}
|
|
105
|
+
for (const targetHost of hosts) {
|
|
106
|
+
const isNetworkHost = targetHost !== '127.0.0.1';
|
|
107
|
+
const targetPort = isNetworkHost ? 8100 : port;
|
|
108
|
+
for (const prefix of prefixes) {
|
|
109
|
+
try {
|
|
110
|
+
const isSessionless = !(options === null || options === void 0 ? void 0 : options.useSessionPath) &&
|
|
111
|
+
(['/status', '/health'].includes(endpoint) || endpoint.startsWith('/wda/'));
|
|
112
|
+
const url = `http://${targetHost}:${targetPort}${isSessionless ? '' : prefix}${endpoint}`;
|
|
113
|
+
// Principal Stability: Screenshots, activation, and script execution need longer timeouts.
|
|
114
|
+
// Heartbeats and Status checks should be snappy to fail-over quickly.
|
|
115
|
+
const isHeavyCommand = endpoint.includes('screenshot') ||
|
|
116
|
+
endpoint.includes('source') ||
|
|
117
|
+
endpoint.includes('swipe') ||
|
|
118
|
+
endpoint.includes('touchAndHold') ||
|
|
119
|
+
endpoint.includes('activate') ||
|
|
120
|
+
endpoint.includes('execute') ||
|
|
121
|
+
endpoint.includes('actions');
|
|
122
|
+
const timeout = isHeavyCommand ? 30000 : 5000; // Snappy 5s timeout for standard commands
|
|
123
|
+
const res = method === 'post'
|
|
124
|
+
? yield axios_1.default.post(url, data || {}, { timeout })
|
|
125
|
+
: yield axios_1.default.get(url, { timeout });
|
|
126
|
+
// If we reached a host but it returned a logical error (e.g. 404 for session),
|
|
127
|
+
// we should update our cache and potentially stop retrying this host/prefix combo.
|
|
128
|
+
// However, for simplicity, we treat any successful HTTP response as "Host is alive".
|
|
129
|
+
if (isNetworkHost && targetHost !== (cached === null || cached === void 0 ? void 0 : cached.host)) {
|
|
130
|
+
this.log.debug(`[WDA] Successful fallback to network IP ${targetHost} for ${udid}`);
|
|
131
|
+
}
|
|
132
|
+
const sid = ((_a = res.data) === null || _a === void 0 ? void 0 : _a.sessionId) || ((_c = (_b = res.data) === null || _b === void 0 ? void 0 : _b.value) === null || _c === void 0 ? void 0 : _c.sessionId);
|
|
133
|
+
if (sid && sid !== (cached === null || cached === void 0 ? void 0 : cached.sessionId)) {
|
|
134
|
+
this.log.debug(`[WDA] Detected new session ID: ${sid} via ${targetHost}`);
|
|
135
|
+
this.wdaConnectionCache.set(cacheKey, {
|
|
136
|
+
host: targetHost,
|
|
137
|
+
pathPrefix: `/session/${sid}`,
|
|
138
|
+
sessionId: sid,
|
|
139
|
+
});
|
|
140
|
+
typedi_1.Container.get(IOSStreamService_1.default).setWDASessionId(udid, sid);
|
|
141
|
+
}
|
|
142
|
+
return res;
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
lastError = err;
|
|
146
|
+
if (err.response) {
|
|
147
|
+
const status = err.response.status;
|
|
148
|
+
const errorMsg = JSON.stringify(err.response.data);
|
|
149
|
+
this.log.info(`[WDA] Command ${method.toUpperCase()} ${endpoint} failed (${status}): ${errorMsg}`);
|
|
150
|
+
// Principal Resiliency: Multi-Phase Recovery
|
|
151
|
+
if (retryCount < 2) {
|
|
152
|
+
// Phase 1: Dead Session Recovery (404)
|
|
153
|
+
if (status === 404 && prefix.includes('/session/')) {
|
|
154
|
+
// Special Intelligence: Some WDA versions don't support /execute, /wda/homescreen, etc.
|
|
155
|
+
// We should NOT clear the session if these specific commands fail with 404,
|
|
156
|
+
// as they might just be unsupported features.
|
|
157
|
+
const NON_FATAL_ENDPOINTS = ['execute', 'homescreen', 'pressButton'];
|
|
158
|
+
if (NON_FATAL_ENDPOINTS.some((e) => endpoint.includes(e))) {
|
|
159
|
+
this.log.debug(`[WDA] Command ${endpoint} failed with 404 (Unsupported or Logical Error). Skipping session reset.`);
|
|
160
|
+
throw err;
|
|
161
|
+
}
|
|
162
|
+
this.log.warn(`[WDA] Session ${cached === null || cached === void 0 ? void 0 : cached.sessionId} appears dead. Resetting and retrying...`);
|
|
163
|
+
this.wdaConnectionCache.delete(cacheKey);
|
|
164
|
+
typedi_1.Container.get(IOSStreamService_1.default).setWDASessionId(udid, '');
|
|
165
|
+
return this.performWDACommand(udid, method, endpoint, data, retryCount + 1, options);
|
|
166
|
+
}
|
|
167
|
+
// Phase 2: Transient State Recovery (500)
|
|
168
|
+
// This handles "Focused" errors, "Internal" errors, or transient locks during app switches.
|
|
169
|
+
if (status === 500) {
|
|
170
|
+
this.log.warn(`[WDA] Transient error (500) for ${udid}, retrying in 1s... (Reason: ${errorMsg})`);
|
|
171
|
+
yield new Promise((r) => setTimeout(r, 1000));
|
|
172
|
+
return this.performWDACommand(udid, method, endpoint, data, retryCount + 1, options);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
throw err;
|
|
176
|
+
}
|
|
177
|
+
// If network host fails, don't retry other prefixes if it's just a status check
|
|
178
|
+
if (isNetworkHost && ['/status', '/health'].includes(endpoint))
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
throw lastError;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
ensureWDASession(udid, port) {
|
|
187
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
188
|
+
var _a, _b, _c;
|
|
189
|
+
try {
|
|
190
|
+
const res = yield axios_1.default.post(`http://127.0.0.1:${port}/session`, { capabilities: { alwaysMatch: { bundleId: 'com.apple.springboard' } } }, { timeout: 10000 });
|
|
191
|
+
const sid = ((_a = res.data) === null || _a === void 0 ? void 0 : _a.sessionId) || ((_c = (_b = res.data) === null || _b === void 0 ? void 0 : _b.value) === null || _c === void 0 ? void 0 : _c.sessionId);
|
|
192
|
+
if (sid) {
|
|
193
|
+
this.wdaConnectionCache.set(`${udid}:${port}`, {
|
|
194
|
+
host: '127.0.0.1',
|
|
195
|
+
pathPrefix: `/session/${sid}`,
|
|
196
|
+
sessionId: sid,
|
|
197
|
+
});
|
|
198
|
+
typedi_1.Container.get(IOSStreamService_1.default).setWDASessionId(udid, sid);
|
|
199
|
+
return sid;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch (e) {
|
|
203
|
+
this.log.debug(`Failed to ensure WDA session for ${udid}: ${e.message}\n${e.stack}`);
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
recoverWDASession(udid, port) {
|
|
209
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
210
|
+
var _a, _b, _c;
|
|
211
|
+
try {
|
|
212
|
+
const res = yield axios_1.default.get(`http://127.0.0.1:${port}/sessions`, { timeout: 5000 });
|
|
213
|
+
const sid = (_c = (_b = (_a = res.data) === null || _a === void 0 ? void 0 : _a.value) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.id;
|
|
214
|
+
if (sid) {
|
|
215
|
+
this.wdaConnectionCache.set(`${udid}:${port}`, {
|
|
216
|
+
host: '127.0.0.1',
|
|
217
|
+
pathPrefix: `/session/${sid}`,
|
|
218
|
+
sessionId: sid,
|
|
219
|
+
});
|
|
220
|
+
typedi_1.Container.get(IOSStreamService_1.default).setWDASessionId(udid, sid);
|
|
221
|
+
return sid;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch (e) {
|
|
225
|
+
this.log.debug(`Failed to recover WDA session for ${udid}: ${e.message}\n${e.stack}`);
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
tap(udid, x, y) {
|
|
231
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
232
|
+
try {
|
|
233
|
+
yield this.sendWDACommand(udid, 'post', '/actions', {
|
|
234
|
+
actions: [
|
|
235
|
+
{
|
|
236
|
+
type: 'pointer',
|
|
237
|
+
id: 'finger1',
|
|
238
|
+
parameters: { pointerType: 'touch' },
|
|
239
|
+
actions: [
|
|
240
|
+
{ type: 'pointerMove', duration: 0, x, y },
|
|
241
|
+
{ type: 'pointerDown', button: 0 },
|
|
242
|
+
{ type: 'pause', duration: 50 },
|
|
243
|
+
{ type: 'pointerUp', button: 0 },
|
|
244
|
+
],
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
catch (e) {
|
|
250
|
+
this.log.error(`Failed to tap at (${x}, ${y}): ${e.message}`);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
swipe(udid, x, y, ex, ey, duration) {
|
|
255
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
256
|
+
try {
|
|
257
|
+
// Principal Strategy: Use W3C Actions with touch pointer.
|
|
258
|
+
// This is the most modern, precise, and reliable method for iOS.
|
|
259
|
+
yield this.sendWDACommand(udid, 'post', '/actions', {
|
|
260
|
+
actions: [
|
|
261
|
+
{
|
|
262
|
+
type: 'pointer',
|
|
263
|
+
id: 'finger1',
|
|
264
|
+
parameters: { pointerType: 'touch' },
|
|
265
|
+
actions: [
|
|
266
|
+
{ type: 'pointerMove', duration: 0, x: Math.round(x), y: Math.round(y) },
|
|
267
|
+
{ type: 'pointerDown', button: 0 },
|
|
268
|
+
{ type: 'pause', duration: 100 },
|
|
269
|
+
{ type: 'pointerMove', duration: 500, x: Math.round(ex), y: Math.round(ey) },
|
|
270
|
+
{ type: 'pointerUp', button: 0 },
|
|
271
|
+
],
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
catch (e) {
|
|
277
|
+
this.log.debug('[WDA] W3C actions failed, falling back to legacy swipe...');
|
|
278
|
+
try {
|
|
279
|
+
// Fallback 1: Coordinate-based (older WDA)
|
|
280
|
+
yield this.sendWDACommand(udid, 'post', '/wda/swipe', {
|
|
281
|
+
startX: Math.round(x),
|
|
282
|
+
startY: Math.round(y),
|
|
283
|
+
endX: Math.round(ex),
|
|
284
|
+
endY: Math.round(ey),
|
|
285
|
+
delay: duration / 1000,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
catch (e2) {
|
|
289
|
+
// Fallback 2: Direction-based (strict WDA)
|
|
290
|
+
let direction = 'up';
|
|
291
|
+
if (Math.abs(ex - x) > Math.abs(ey - y)) {
|
|
292
|
+
direction = ex > x ? 'right' : 'left';
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
direction = ey > y ? 'down' : 'up';
|
|
296
|
+
}
|
|
297
|
+
yield this.sendWDACommand(udid, 'post', '/wda/swipe', { direction });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
typeText(udid, text) {
|
|
303
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
304
|
+
const b = this.typeBuffers.get(udid) || { text: '', timer: null, pending: false };
|
|
305
|
+
this.typeBuffers.set(udid, b);
|
|
306
|
+
b.text += text;
|
|
307
|
+
if (b.timer)
|
|
308
|
+
clearTimeout(b.timer);
|
|
309
|
+
if (!b.pending)
|
|
310
|
+
b.timer = setTimeout(() => this.flushTypeBuffer(udid), 150);
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
flushTypeBuffer(udid) {
|
|
314
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
315
|
+
const b = this.typeBuffers.get(udid);
|
|
316
|
+
if (!b || !b.text)
|
|
317
|
+
return;
|
|
318
|
+
const t = b.text;
|
|
319
|
+
b.text = '';
|
|
320
|
+
b.timer = null;
|
|
321
|
+
b.pending = true;
|
|
322
|
+
try {
|
|
323
|
+
yield this.sendWDACommand(udid, 'post', '/wda/type', { text: t });
|
|
324
|
+
}
|
|
325
|
+
finally {
|
|
326
|
+
b.pending = false;
|
|
327
|
+
if (b.text)
|
|
328
|
+
setTimeout(() => this.flushTypeBuffer(udid), 50);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
pressKey(udid, key) {
|
|
333
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
334
|
+
const n = key.toString().toLowerCase();
|
|
335
|
+
// Principal Modernization: Use /wda/pressButton with {name: 'home'} as primary.
|
|
336
|
+
// Legacy /wda/homescreen is often removed in modern WDA versions.
|
|
337
|
+
if (['home', '3'].includes(n)) {
|
|
338
|
+
try {
|
|
339
|
+
yield this.sendWDACommand(udid, 'post', '/wda/pressButton', { name: 'home' });
|
|
340
|
+
}
|
|
341
|
+
catch (e) {
|
|
342
|
+
// Fallback for extremely old WDA
|
|
343
|
+
yield this.sendWDACommand(udid, 'post', '/wda/homescreen', {}).catch(() => { });
|
|
344
|
+
}
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
yield this.sendWDACommand(udid, 'post', '/wda/pressButton', {
|
|
348
|
+
name: n === 'backspace' ? 'delete' : n,
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
getScreenshot(udid) {
|
|
353
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
354
|
+
var _a, _b, _c;
|
|
355
|
+
const s = typedi_1.Container.get(IOSStreamService_1.default);
|
|
356
|
+
if (yield s.isGoIOSAvailable()) {
|
|
357
|
+
try {
|
|
358
|
+
const p = path_1.default.join(os_1.default.tmpdir(), `screenshot-${udid}.png`);
|
|
359
|
+
yield execFilePromise(s.goIOSPath, ['screenshot', '--udid', udid, '--output', p], {
|
|
360
|
+
env: Object.assign(Object.assign({}, process.env), { ENABLE_GO_IOS_AGENT: 'yes' }),
|
|
361
|
+
});
|
|
362
|
+
const b = yield fs_extra_1.default.readFile(p);
|
|
363
|
+
yield fs_extra_1.default.remove(p);
|
|
364
|
+
return b.toString('base64');
|
|
365
|
+
}
|
|
366
|
+
catch (e) {
|
|
367
|
+
this.log.debug(`go-ios screenshot failed for ${udid}: ${e.message}\n${e.stack}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
const res = yield this.sendWDACommand(udid, 'get', '/screenshot');
|
|
371
|
+
return ((_b = (_a = res.data) === null || _a === void 0 ? void 0 : _a.value) === null || _b === void 0 ? void 0 : _b.screenshot) || ((_c = res.data) === null || _c === void 0 ? void 0 : _c.value) || '';
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
getClipboard(udid) {
|
|
375
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
376
|
+
var _a, _b, _c, _d, _e;
|
|
377
|
+
try {
|
|
378
|
+
this.log.debug(`[Clipboard] Requesting pasteboard for ${udid}...`);
|
|
379
|
+
const device = yield device_store_1.DeviceStoreFactory.getStore().findDevice({ udid });
|
|
380
|
+
if (!device)
|
|
381
|
+
throw new Error(`Device ${udid} not found`);
|
|
382
|
+
const isRealDevice = !!device.realDevice;
|
|
383
|
+
const fetchParams = { contentType: 'plaintext' };
|
|
384
|
+
let res;
|
|
385
|
+
let value = '';
|
|
386
|
+
// On iOS 13+ real devices, WDA must be in foreground to read clipboard (Apple limitation).
|
|
387
|
+
// Skip fast-path for real devices and activate WDA first; use fast-path only for simulators.
|
|
388
|
+
if (!isRealDevice) {
|
|
389
|
+
res = yield this.sendWDACommand(udid, 'post', '/wda/getPasteboard', fetchParams);
|
|
390
|
+
const raw = (_a = res.data) === null || _a === void 0 ? void 0 : _a.value;
|
|
391
|
+
if (raw && typeof raw === 'string' && raw.length > 0) {
|
|
392
|
+
this.log.debug(`[Clipboard] Fast-path successful for ${udid}`);
|
|
393
|
+
return this.parsePasteboardResponse(raw);
|
|
394
|
+
}
|
|
395
|
+
this.log.debug('[Clipboard] Fast-path empty. Proceeding with surgical foregrounding...');
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
this.log.debug('[Clipboard] Real device: activating WDA first (required for clipboard access).');
|
|
399
|
+
}
|
|
400
|
+
// Surgical Foregrounding: Capture -> Activate WDA -> Fetch -> Restore
|
|
401
|
+
let previousApp = null;
|
|
402
|
+
try {
|
|
403
|
+
const activeAppRes = yield this.sendWDACommand(udid, 'get', '/wda/activeAppInfo');
|
|
404
|
+
previousApp = ((_c = (_b = activeAppRes.data) === null || _b === void 0 ? void 0 : _b.value) === null || _c === void 0 ? void 0 : _c.bundleId) || ((_d = activeAppRes.data) === null || _d === void 0 ? void 0 : _d.bundleId);
|
|
405
|
+
}
|
|
406
|
+
catch (e) {
|
|
407
|
+
this.log.debug(`[Clipboard] Failed to capture active app: ${e.message}`);
|
|
408
|
+
}
|
|
409
|
+
const s = typedi_1.Container.get(IOSStreamService_1.default);
|
|
410
|
+
const wdaBundleId = (yield s.detectWDABundleId(udid)) || 'com.qasecret.WebDriverAgentRunner.xctrunner';
|
|
411
|
+
// Phase 1: Activate WDA so it can access UIPasteboard (required on real devices)
|
|
412
|
+
const CLIPBOARD_SETTLE_MS = 1500; // WDA needs time in foreground to read pasteboard
|
|
413
|
+
try {
|
|
414
|
+
this.log.debug(`[Clipboard] Activating WDA (${wdaBundleId}) for ${udid}...`);
|
|
415
|
+
yield this.sendWDACommand(udid, 'post', '/wda/apps/activate', { bundleId: wdaBundleId });
|
|
416
|
+
yield new Promise((r) => setTimeout(r, CLIPBOARD_SETTLE_MS));
|
|
417
|
+
}
|
|
418
|
+
catch (e) {
|
|
419
|
+
this.log.debug(`[Clipboard] Cross-session activation info: ${e.message}`);
|
|
420
|
+
}
|
|
421
|
+
// Tier 1: Session-less getPasteboard (primary for standalone WDA)
|
|
422
|
+
res = yield this.sendWDACommand(udid, 'post', '/wda/getPasteboard', fetchParams).catch(() => this.sendWDACommand(udid, 'get', '/wda/getPasteboard'));
|
|
423
|
+
this.log.debug(`[Clipboard] Tier 1 (Runner-CS) raw response: ${JSON.stringify(res === null || res === void 0 ? void 0 : res.data)}`);
|
|
424
|
+
value = this.parsePasteboardResponse(res === null || res === void 0 ? void 0 : res.data);
|
|
425
|
+
// Tier 1b: If empty and we have a session, try session-scoped getPasteboard (some WDA builds use /session/.../wda/getPasteboard)
|
|
426
|
+
if (!value && s.getWDASessionId(udid)) {
|
|
427
|
+
try {
|
|
428
|
+
res = yield this.sendWDACommand(udid, 'post', '/wda/getPasteboard', fetchParams, {
|
|
429
|
+
useSessionPath: true,
|
|
430
|
+
});
|
|
431
|
+
value = this.parsePasteboardResponse(res === null || res === void 0 ? void 0 : res.data);
|
|
432
|
+
if (value)
|
|
433
|
+
this.log.debug(`[Clipboard] Session-scoped getPasteboard succeeded for ${udid}`);
|
|
434
|
+
}
|
|
435
|
+
catch (e) {
|
|
436
|
+
this.log.debug(`[Clipboard] Session-scoped getPasteboard failed: ${e.message}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
// Tier 2: Home -> Runner -> Focus Fallback
|
|
440
|
+
if (!value) {
|
|
441
|
+
this.log.debug('[Clipboard] Primary fetch empty. Trying Home-Surgical restoration...');
|
|
442
|
+
try {
|
|
443
|
+
yield this.sendWDACommand(udid, 'post', '/wda/homescreen', {});
|
|
444
|
+
yield new Promise((r) => setTimeout(r, 1000));
|
|
445
|
+
yield this.sendWDACommand(udid, 'post', '/wda/apps/activate', { bundleId: wdaBundleId });
|
|
446
|
+
yield new Promise((r) => setTimeout(r, CLIPBOARD_SETTLE_MS));
|
|
447
|
+
yield this.tap(udid, 200, 400).catch(() => { });
|
|
448
|
+
yield new Promise((r) => setTimeout(r, 800));
|
|
449
|
+
res = yield this.sendWDACommand(udid, 'post', '/wda/getPasteboard', fetchParams);
|
|
450
|
+
this.log.debug(`[Clipboard] Tier 2 (Home-Surgical) raw response: ${JSON.stringify(res === null || res === void 0 ? void 0 : res.data)}`);
|
|
451
|
+
value = this.parsePasteboardResponse(res === null || res === void 0 ? void 0 : res.data);
|
|
452
|
+
}
|
|
453
|
+
catch (e) { }
|
|
454
|
+
}
|
|
455
|
+
// Tier 3: Safari System Fallback
|
|
456
|
+
if (!value) {
|
|
457
|
+
this.log.debug('[Clipboard] Trying Safari System Tier...');
|
|
458
|
+
try {
|
|
459
|
+
yield this.sendWDACommand(udid, 'post', '/wda/apps/activate', {
|
|
460
|
+
bundleId: 'com.apple.mobilesafari',
|
|
461
|
+
});
|
|
462
|
+
yield new Promise((r) => setTimeout(r, CLIPBOARD_SETTLE_MS));
|
|
463
|
+
res = yield this.sendWDACommand(udid, 'post', '/wda/getPasteboard', fetchParams);
|
|
464
|
+
this.log.debug(`[Clipboard] Tier 3 (Safari-CS) raw response: ${JSON.stringify(res === null || res === void 0 ? void 0 : res.data)}`);
|
|
465
|
+
value = this.parsePasteboardResponse(res === null || res === void 0 ? void 0 : res.data);
|
|
466
|
+
}
|
|
467
|
+
catch (e) { }
|
|
468
|
+
}
|
|
469
|
+
// Restore Previous App
|
|
470
|
+
if (previousApp && previousApp !== wdaBundleId && !previousApp.startsWith('com.apple.')) {
|
|
471
|
+
this.log.debug(`[Clipboard] Background restoring ${previousApp}`);
|
|
472
|
+
this.sendWDACommand(udid, 'post', '/wda/apps/activate', { bundleId: previousApp }).catch(() => { });
|
|
473
|
+
}
|
|
474
|
+
// FINAL FALLBACK: Appium script (session-scoped)
|
|
475
|
+
if (!value) {
|
|
476
|
+
try {
|
|
477
|
+
this.log.debug('[Clipboard] Tiers failed. Executing Mobile Script fallback...');
|
|
478
|
+
res = yield this.sendWDACommand(udid, 'post', '/execute', {
|
|
479
|
+
script: 'mobile: getClipboard',
|
|
480
|
+
args: [fetchParams],
|
|
481
|
+
}).catch(() => this.sendWDACommand(udid, 'post', '/execute/sync', {
|
|
482
|
+
script: 'mobile: getClipboard',
|
|
483
|
+
args: [fetchParams],
|
|
484
|
+
}));
|
|
485
|
+
value = this.parsePasteboardResponse(res === null || res === void 0 ? void 0 : res.data);
|
|
486
|
+
}
|
|
487
|
+
catch (e) { }
|
|
488
|
+
}
|
|
489
|
+
if (!value) {
|
|
490
|
+
this.log.info(`[Clipboard] All recovery tiers yielded empty results for ${udid}. Last HTTP status: ${(_e = res === null || res === void 0 ? void 0 : res.status) !== null && _e !== void 0 ? _e : 'N/A'}`);
|
|
491
|
+
}
|
|
492
|
+
return value || '';
|
|
493
|
+
}
|
|
494
|
+
catch (e) {
|
|
495
|
+
this.log.error(`Failed to get clipboard for ${udid}: ${e.message}`);
|
|
496
|
+
}
|
|
497
|
+
return '';
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Universal parser for varied WDA pasteboard response formats.
|
|
502
|
+
* Handles strings, nested objects, content/result/data fields, and base64 auto-decoding.
|
|
503
|
+
*/
|
|
504
|
+
parsePasteboardResponse(data) {
|
|
505
|
+
if (data === undefined || data === null)
|
|
506
|
+
return '';
|
|
507
|
+
if (typeof data === 'string')
|
|
508
|
+
return data;
|
|
509
|
+
// Top-level value (standard WDA/Appium response)
|
|
510
|
+
let value = data === null || data === void 0 ? void 0 : data.value;
|
|
511
|
+
// Some proxies or WDA builds use content/result/data at top level
|
|
512
|
+
if ((!value || (typeof value === 'object' && Object.keys(value).length === 0)) &&
|
|
513
|
+
typeof (data === null || data === void 0 ? void 0 : data.content) === 'string')
|
|
514
|
+
value = data.content;
|
|
515
|
+
if ((!value || (typeof value === 'object' && Object.keys(value).length === 0)) &&
|
|
516
|
+
typeof (data === null || data === void 0 ? void 0 : data.result) === 'string')
|
|
517
|
+
value = data.result;
|
|
518
|
+
if ((!value || (typeof value === 'object' && Object.keys(value).length === 0)) &&
|
|
519
|
+
typeof (data === null || data === void 0 ? void 0 : data.data) === 'string')
|
|
520
|
+
value = data.data;
|
|
521
|
+
// Unwrap nested objects: { value: { string/content/value/plaintext/text: "..." } }
|
|
522
|
+
if (value && typeof value === 'object') {
|
|
523
|
+
value = value.string || value.content || value.value || value.plaintext || value.text || '';
|
|
524
|
+
}
|
|
525
|
+
if (!value || typeof value !== 'string')
|
|
526
|
+
return '';
|
|
527
|
+
// Trim so we don't treat whitespace-only as empty when it's valid content
|
|
528
|
+
value = value.trim();
|
|
529
|
+
if (value.length === 0)
|
|
530
|
+
return '';
|
|
531
|
+
// Base64: WDA often returns plaintext as base64. Only decode when it looks like base64.
|
|
532
|
+
if (value.length > 8 && /^[A-Za-z0-9+/]+=*$/.test(value) && value.length % 4 === 0) {
|
|
533
|
+
try {
|
|
534
|
+
const decoded = Buffer.from(value, 'base64').toString('utf8');
|
|
535
|
+
if (!/[\x00-\x08\x0E-\x1F]/.test(decoded))
|
|
536
|
+
return decoded;
|
|
537
|
+
}
|
|
538
|
+
catch (e) {
|
|
539
|
+
// not valid base64, use as-is
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return value;
|
|
543
|
+
}
|
|
544
|
+
setClipboard(udid, content) {
|
|
545
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
546
|
+
try {
|
|
547
|
+
yield this.sendWDACommand(udid, 'post', '/wda/setPasteboard', {
|
|
548
|
+
content: Buffer.from(content).toString('base64'),
|
|
549
|
+
contentType: 'plaintext',
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
catch (e) {
|
|
553
|
+
this.log.debug(`Failed to set clipboard for ${udid}: ${e.message}\n${e.stack}`);
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
lock(udid) {
|
|
558
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
559
|
+
try {
|
|
560
|
+
yield this.sendWDACommand(udid, 'post', '/wda/lock', {});
|
|
561
|
+
}
|
|
562
|
+
catch (e) {
|
|
563
|
+
this.log.debug(`Failed to lock device ${udid}: ${e.message}\n${e.stack}`);
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
unlock(udid) {
|
|
568
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
569
|
+
try {
|
|
570
|
+
yield this.sendWDACommand(udid, 'post', '/wda/unlock', {});
|
|
571
|
+
}
|
|
572
|
+
catch (e) {
|
|
573
|
+
this.log.debug(`Failed to unlock device ${udid}: ${e.message}\n${e.stack}`);
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Performs an early check of ideviceinstaller requirements
|
|
579
|
+
*/
|
|
580
|
+
checkRequirements() {
|
|
581
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
582
|
+
const version = yield this.getIdeviceinstallerVersion();
|
|
583
|
+
this.log.info(`iOS Installation Requirement Check: ideviceinstaller ${version}`);
|
|
584
|
+
if (semver_1.default.lt(version, '1.1.0')) {
|
|
585
|
+
this.log.warn(`⚠️ ideviceinstaller version ${version} is legacy. We recommend upgrading to 1.1.2 or 1.2.0+ for better compatibility (brew upgrade ideviceinstaller).`);
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
getIdeviceinstallerVersion() {
|
|
590
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
591
|
+
if (this.ideviceinstallerVersion)
|
|
592
|
+
return this.ideviceinstallerVersion;
|
|
593
|
+
try {
|
|
594
|
+
const { stdout } = yield execFilePromise('ideviceinstaller', ['--version']);
|
|
595
|
+
const match = stdout.match(/ideviceinstaller\s+([\d.]+)/);
|
|
596
|
+
if (match) {
|
|
597
|
+
const coerced = semver_1.default.coerce(match[1]);
|
|
598
|
+
if (coerced) {
|
|
599
|
+
this.ideviceinstallerVersion = coerced.version;
|
|
600
|
+
this.log.info(`Detected ideviceinstaller version: Raw="${stdout.trim()}", Normalized="${this.ideviceinstallerVersion}"`);
|
|
601
|
+
return this.ideviceinstallerVersion;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
catch (e) {
|
|
606
|
+
try {
|
|
607
|
+
const { stdout } = yield execFilePromise('ideviceinstaller', ['-v']);
|
|
608
|
+
const match = stdout.match(/ideviceinstaller\s+([\d.]+)/);
|
|
609
|
+
if (match) {
|
|
610
|
+
const coerced = semver_1.default.coerce(match[1]);
|
|
611
|
+
if (coerced) {
|
|
612
|
+
this.ideviceinstallerVersion = coerced.version;
|
|
613
|
+
this.log.info(`Detected ideviceinstaller version (alt): Raw="${stdout.trim()}", Normalized="${this.ideviceinstallerVersion}"`);
|
|
614
|
+
return this.ideviceinstallerVersion;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
catch (e2) {
|
|
619
|
+
this.log.warn(`Failed to detect ideviceinstaller version: ${e2.message}`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
this.ideviceinstallerVersion = '1.0.0'; // Fallback to legacy
|
|
623
|
+
return this.ideviceinstallerVersion;
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
installApp(udid, p) {
|
|
627
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
628
|
+
const version = yield this.getIdeviceinstallerVersion();
|
|
629
|
+
// Threshold adjusted: 1.1.0 and above use 'install' subcommand. Version 1.1.1 dropped legacy flags.
|
|
630
|
+
try {
|
|
631
|
+
if (semver_1.default.gte(version, '1.1.0')) {
|
|
632
|
+
yield execFilePromise('ideviceinstaller', ['-u', udid, 'install', p]);
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
yield execFilePromise('ideviceinstaller', ['-u', udid, '-i', p]);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
catch (e) {
|
|
639
|
+
this.log.warn(`ideviceinstaller failed for ${udid}: ${e.message}. Trying go-ios fallback.`);
|
|
640
|
+
const s = typedi_1.Container.get(IOSStreamService_1.default);
|
|
641
|
+
try {
|
|
642
|
+
yield execFilePromise(s.goIOSPath, ['install', `--path=${p}`, '--udid', udid], {
|
|
643
|
+
env: Object.assign(Object.assign({}, process.env), { ENABLE_GO_IOS_AGENT: 'yes' }),
|
|
644
|
+
});
|
|
645
|
+
this.log.info(`Successfully installed app using go-ios on ${udid}`);
|
|
646
|
+
}
|
|
647
|
+
catch (e2) {
|
|
648
|
+
this.log.error(`Installation failed for ${udid} with both tools. Last error: ${e2.message}`);
|
|
649
|
+
throw e2;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
uninstallApp(udid, b) {
|
|
655
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
656
|
+
const version = yield this.getIdeviceinstallerVersion();
|
|
657
|
+
try {
|
|
658
|
+
if (semver_1.default.gte(version, '1.1.0')) {
|
|
659
|
+
yield execFilePromise('ideviceinstaller', ['-u', udid, 'uninstall', b]);
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
yield execFilePromise('ideviceinstaller', ['-u', udid, '-U', b]);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
catch (e) {
|
|
666
|
+
this.log.warn(`ideviceinstaller uninstall failed for ${udid}: ${e.message}. Trying go-ios fallback.`);
|
|
667
|
+
const s = typedi_1.Container.get(IOSStreamService_1.default);
|
|
668
|
+
try {
|
|
669
|
+
yield execFilePromise(s.goIOSPath, ['uninstall', `--bundleid=${b}`, '--udid', udid], {
|
|
670
|
+
env: Object.assign(Object.assign({}, process.env), { ENABLE_GO_IOS_AGENT: 'yes' }),
|
|
671
|
+
});
|
|
672
|
+
this.log.info(`Successfully uninstalled app ${b} using go-ios on ${udid}`);
|
|
673
|
+
}
|
|
674
|
+
catch (e2) {
|
|
675
|
+
this.log.error(`Uninstallation failed for ${udid} with both tools. Last error: ${e2.message}`);
|
|
676
|
+
throw e2;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
listApps(udid) {
|
|
682
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
683
|
+
const version = yield this.getIdeviceinstallerVersion();
|
|
684
|
+
const args = semver_1.default.gte(version, '1.1.0') ? ['-u', udid, 'list'] : ['-u', udid, '-l'];
|
|
685
|
+
try {
|
|
686
|
+
const { stdout } = yield execFilePromise('ideviceinstaller', args);
|
|
687
|
+
const apps = stdout
|
|
688
|
+
.split('\n')
|
|
689
|
+
.filter((l) => l.includes(' - '))
|
|
690
|
+
.map((l) => l.split(' - ')[0]);
|
|
691
|
+
if (apps.length > 0)
|
|
692
|
+
return apps;
|
|
693
|
+
this.log.info(`ideviceinstaller returned empty app list for ${udid}. Trying go-ios fallback.`);
|
|
694
|
+
}
|
|
695
|
+
catch (e) {
|
|
696
|
+
this.log.warn(`ideviceinstaller listApps failed for ${udid}: ${e.message}. Trying go-ios fallback.`);
|
|
697
|
+
}
|
|
698
|
+
// Fallback to go-ios
|
|
699
|
+
try {
|
|
700
|
+
const s = typedi_1.Container.get(IOSStreamService_1.default);
|
|
701
|
+
const { stdout } = yield execFilePromise(s.goIOSPath, ['apps', '--list', '--udid', udid], {
|
|
702
|
+
env: Object.assign(Object.assign({}, process.env), { ENABLE_GO_IOS_AGENT: 'yes' }),
|
|
703
|
+
});
|
|
704
|
+
return stdout
|
|
705
|
+
.split('\n')
|
|
706
|
+
.filter((line) => line.trim() !== '')
|
|
707
|
+
.map((line) => line.split(/\s+/)[0]);
|
|
708
|
+
}
|
|
709
|
+
catch (e2) {
|
|
710
|
+
this.log.error(`Failed to list apps for ${udid} using go-ios: ${e2.message}`);
|
|
711
|
+
return [];
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
getLogs(udid) {
|
|
716
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
717
|
+
const s = typedi_1.Container.get(IOSStreamService_1.default);
|
|
718
|
+
// Find device to check if it's a simulator
|
|
719
|
+
const device = yield device_store_1.DeviceStoreFactory.getStore().findDevice({ udid });
|
|
720
|
+
const isSimulator = device && !device.realDevice;
|
|
721
|
+
let command = 'idevicesyslog';
|
|
722
|
+
let args = ['-u', udid];
|
|
723
|
+
if (isSimulator) {
|
|
724
|
+
command = 'xcrun';
|
|
725
|
+
args = ['simctl', 'spawn', udid, 'log', 'show', '--last', '10s', '--style', 'compact'];
|
|
726
|
+
}
|
|
727
|
+
else if (yield s.isGoIOSAvailable()) {
|
|
728
|
+
command = s.goIOSPath;
|
|
729
|
+
args = ['syslog', '--udid', udid];
|
|
730
|
+
}
|
|
731
|
+
this.log.debug(`Fetching logs for ${udid} (Simulator: ${isSimulator}) using ${command} ${args.join(' ')}`);
|
|
732
|
+
return new Promise((resolve) => {
|
|
733
|
+
var _a;
|
|
734
|
+
const proc = (0, child_process_1.spawn)(command, args, {
|
|
735
|
+
env: Object.assign(Object.assign({}, process.env), { ENABLE_GO_IOS_AGENT: 'yes' }),
|
|
736
|
+
});
|
|
737
|
+
let output = '';
|
|
738
|
+
let resolved = false;
|
|
739
|
+
if (!proc.stdout) {
|
|
740
|
+
resolved = true;
|
|
741
|
+
this.log.error(`Failed to capture logs for ${udid}: stdout stream is missing`);
|
|
742
|
+
proc.kill('SIGKILL');
|
|
743
|
+
resolve(output);
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
const rl = readline_1.default.createInterface({
|
|
747
|
+
input: proc.stdout,
|
|
748
|
+
terminal: false,
|
|
749
|
+
});
|
|
750
|
+
let timer;
|
|
751
|
+
const resetInactivityTimeout = () => {
|
|
752
|
+
if (timer)
|
|
753
|
+
clearTimeout(timer);
|
|
754
|
+
timer = setTimeout(() => {
|
|
755
|
+
if (!resolved) {
|
|
756
|
+
resolved = true;
|
|
757
|
+
rl.close();
|
|
758
|
+
proc.kill('SIGKILL');
|
|
759
|
+
this.log.debug(`getLogs snapshot completed (inactivity timeout) for ${udid}`);
|
|
760
|
+
resolve(output);
|
|
761
|
+
}
|
|
762
|
+
}, 2000);
|
|
763
|
+
};
|
|
764
|
+
// Initial timer start
|
|
765
|
+
resetInactivityTimeout();
|
|
766
|
+
rl.on('line', (line) => {
|
|
767
|
+
resetInactivityTimeout();
|
|
768
|
+
if (!line.trim())
|
|
769
|
+
return;
|
|
770
|
+
if (command === s.goIOSPath) {
|
|
771
|
+
try {
|
|
772
|
+
const parsed = JSON.parse(line);
|
|
773
|
+
output += (parsed.msg || line) + '\n';
|
|
774
|
+
}
|
|
775
|
+
catch (e) {
|
|
776
|
+
output += line + '\n';
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
else {
|
|
780
|
+
output += line + '\n';
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
(_a = proc.stderr) === null || _a === void 0 ? void 0 : _a.on('data', (data) => {
|
|
784
|
+
resetInactivityTimeout();
|
|
785
|
+
this.log.debug(`[getLogs][stderr] ${data.toString()}`);
|
|
786
|
+
});
|
|
787
|
+
proc.on('error', (err) => {
|
|
788
|
+
if (!resolved) {
|
|
789
|
+
resolved = true;
|
|
790
|
+
rl.close();
|
|
791
|
+
this.log.debug(`getLogs failed for ${udid}: ${err.message}`);
|
|
792
|
+
clearTimeout(timer);
|
|
793
|
+
resolve(output);
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
proc.on('exit', (code) => {
|
|
797
|
+
if (!resolved) {
|
|
798
|
+
resolved = true;
|
|
799
|
+
rl.close();
|
|
800
|
+
clearTimeout(timer);
|
|
801
|
+
this.log.debug(`getLogs process exited with code ${code} for ${udid}`);
|
|
802
|
+
resolve(output);
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
});
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
verifyWDAStatus(udid) {
|
|
809
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
810
|
+
var _a, _b, _c;
|
|
811
|
+
// Use retry logic for transient errors (ECONNRESET, timeouts)
|
|
812
|
+
const maxRetries = 2;
|
|
813
|
+
let lastError;
|
|
814
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
815
|
+
try {
|
|
816
|
+
const res = yield this.sendWDACommand(udid, 'get', '/status');
|
|
817
|
+
return ((_b = (_a = res.data) === null || _a === void 0 ? void 0 : _a.value) === null || _b === void 0 ? void 0 : _b.ready) === true;
|
|
818
|
+
}
|
|
819
|
+
catch (e) {
|
|
820
|
+
lastError = e;
|
|
821
|
+
const isTransientError = e.code === 'ECONNRESET' ||
|
|
822
|
+
e.code === 'ETIMEDOUT' ||
|
|
823
|
+
e.code === 'ECONNABORTED' ||
|
|
824
|
+
(e.response && e.response.status >= 500);
|
|
825
|
+
if (isTransientError && attempt < maxRetries) {
|
|
826
|
+
const backoffMs = Math.min(500 * Math.pow(2, attempt), 2000);
|
|
827
|
+
this.log.debug(`[WDA] Transient error verifying status for ${udid} (${e.code || ((_c = e.response) === null || _c === void 0 ? void 0 : _c.status)}), retrying in ${backoffMs}ms (attempt ${attempt + 1}/${maxRetries + 1})`);
|
|
828
|
+
yield new Promise((resolve) => setTimeout(resolve, backoffMs));
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
// Permanent error or max retries reached
|
|
832
|
+
this.log.debug(`Failed to verify WDA status for ${udid}: ${e.message}`);
|
|
833
|
+
return false;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return false;
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
executeShell(udid, command) {
|
|
840
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
841
|
+
const ALLOWED_COMMANDS = ['ls', 'ps', 'top', 'whoami', 'date', 'uptime', 'netstat', 'id'];
|
|
842
|
+
// Basic sanitation
|
|
843
|
+
const safeCommand = command.trim();
|
|
844
|
+
// Check if the command starts with any allowed prefix
|
|
845
|
+
const isAllowed = ALLOWED_COMMANDS.some((prefix) => safeCommand.startsWith(prefix));
|
|
846
|
+
if (!isAllowed) {
|
|
847
|
+
this.log.warn(`Blocked potentially unsafe shell command on ${udid}: ${safeCommand}`);
|
|
848
|
+
throw new Error(`Command '${safeCommand}' is not allowed for security reasons.`);
|
|
849
|
+
}
|
|
850
|
+
// Split command into args for execFilePromise
|
|
851
|
+
const args = safeCommand.split(/\s+/);
|
|
852
|
+
const { stdout } = yield execFilePromise('xcrun', ['simctl', ...args, udid]).catch((e) => __awaiter(this, void 0, void 0, function* () {
|
|
853
|
+
this.log.debug(`xcrun simctl failed for ${udid}: ${e.message}. Trying go-ios fallback.`);
|
|
854
|
+
const s = typedi_1.Container.get(IOSStreamService_1.default);
|
|
855
|
+
return yield execFilePromise(s.goIOSPath, [...args, '--udid', udid], {
|
|
856
|
+
env: Object.assign(Object.assign({}, process.env), { ENABLE_GO_IOS_AGENT: 'yes' }),
|
|
857
|
+
});
|
|
858
|
+
}));
|
|
859
|
+
return stdout;
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
exports.WDAClient = WDAClient;
|
|
864
|
+
exports.WDAClient = WDAClient = __decorate([
|
|
865
|
+
(0, typedi_1.Service)()
|
|
866
|
+
], WDAClient);
|