UNPKG

appium-xcuitest-driver

Version:

Appium driver for iOS using XCUITest for backend

125 lines (110 loc) 4.22 kB
import {getRemoteXPCServices, wrapRemoteXPCConnectionError} from './remotexpc-utils'; import {log} from '../logger'; import type {CrashReportsService as RemoteXPCCrashReportsService} from 'appium-ios-remotexpc'; const CRASH_REPORT_EXTENSIONS = ['.ips']; const MAX_FILES_IN_ERROR = 10; /** * Lists and exports device crash reports (`.ips`) on real hardware over RemoteXPC. * * Requires **iOS/tvOS 18+** and the optional **`appium-ios-remotexpc`** package. * Used by {@link IOSCrashLog} for BiDi / `crashlog` collection on real devices. */ export class CrashReportsClient { private readonly crashReportsService: RemoteXPCCrashReportsService; private constructor(crashReportsService: RemoteXPCCrashReportsService) { this.crashReportsService = crashReportsService; } /** * Opens a RemoteXPC crash-reports service for the given UDID. * * @param udid - Real device UDID * @param useRemoteXPC - Must be `true`; callers derive this from `isIos18OrNewer` / session options * @throws {Error} If `useRemoteXPC` is false, or RemoteXPC setup fails */ static async create(udid: string, useRemoteXPC: boolean): Promise<CrashReportsClient> { if (!useRemoteXPC) { throw new Error( 'Real device crash report access requires iOS/tvOS 18 or newer with the appium-ios-remotexpc ' + 'package installed.', ); } try { const Services = await getRemoteXPCServices(); const crashReportsService = await Services.startCrashReportsService(udid); return new CrashReportsClient(crashReportsService); } catch (err: any) { throw wrapRemoteXPCConnectionError( err, 'Failed to create crash reports client via RemoteXPC', ); } } /** * @returns Basenames of crash report files on the device (e.g. `MyApp-2024-01-01-120000.ips`) */ async listCrashes(): Promise<string[]> { const allFiles = await this._listCrashReportPaths(); return allFiles.map((filePath) => { const parts = filePath.split('/'); return parts[parts.length - 1]; }); } /** * Pulls a single crash report off the device into a local folder. * * @param name - Crash file basename as returned by {@link CrashReportsClient.listCrashes} * @param dstFolder - Existing local directory to write into * @throws {Error} If the named report is not found on the device */ async exportCrash(name: string, dstFolder: string): Promise<void> { const allFiles = await this._listCrashReportPaths(); const fullPath = allFiles.find((p) => p.endsWith(`/${name}`) || p === `/${name}`); if (!fullPath) { const filesList = allFiles.slice(0, MAX_FILES_IN_ERROR).join(', '); const hasMore = allFiles.length > MAX_FILES_IN_ERROR; throw new Error( `Crash report '${name}' not found on device. ` + `Available files: ${filesList}${hasMore ? `, ... and ${allFiles.length - MAX_FILES_IN_ERROR} more` : ''}`, ); } await this.crashReportsService.pull(dstFolder, fullPath); } /** * Tears down the crash-reports service. */ async close(): Promise<void> { try { this.crashReportsService.close(); } catch (err) { log.warn(`Error closing crash reports service: ${(err as Error).message}`); } } /** * Walk the crash-reports tree and collect `.ips` paths without listing the full tree upfront. */ private async _listCrashReportPaths(): Promise<string[]> { const results: string[] = []; await this._collectCrashReportPaths('/', results); return results; } private async _collectCrashReportPaths(dirPath: string, results: string[]): Promise<void> { let children: string[]; try { children = await this.crashReportsService.ls(dirPath, 1); } catch { return; } for (const entryPath of children) { const basename = entryPath.split('/').pop() ?? entryPath; if (CRASH_REPORT_EXTENSIONS.some((ext) => basename.endsWith(ext))) { results.push(entryPath); continue; } try { await this._collectCrashReportPaths(entryPath, results); } catch { // Skip entries we can't access or that aren't directories } } } }