appium-xcuitest-driver
Version:
Appium driver for iOS using XCUITest for backend
167 lines (146 loc) • 5.32 kB
text/typescript
import {fs, tempDir, util} from 'appium/support';
import B from 'bluebird';
import path from 'path';
import _ from 'lodash';
import {Pyidevice} from '../real-device-clients/py-ios-device-client';
import {IOSLog} from './ios-log';
import { toLogEntry, grepFile } from './helpers';
import type { AppiumLogger } from '@appium/types';
import type { BaseDeviceClient } from '../real-device-clients/base-device-client';
import type { Simulator } from 'appium-ios-simulator';
import type { LogEntry } from '../commands/types';
// The file format has been changed from '.crash' to '.ips' since Monterey.
const CRASH_REPORTS_GLOB_PATTERN = '**/*.@(crash|ips)';
// The size of a single diagnostic report might be hundreds of kilobytes.
// Thus we do not want to store too many items in the memory at once.
const MAX_RECENT_ITEMS = 20;
type TSerializedEntry = [string, number];
export interface IOSCrashLogOptions {
/** UDID of a real device */
udid?: string;
/** Simulator instance */
sim?: Simulator;
log: AppiumLogger;
}
export class IOSCrashLog extends IOSLog<TSerializedEntry, TSerializedEntry> {
private readonly _udid: string | undefined;
private readonly _realDeviceClient: BaseDeviceClient | null;
private readonly _logDir: string | null;
private readonly _sim: Simulator | undefined;
private _recentCrashFiles: string[];
private _started: boolean;
constructor(opts: IOSCrashLogOptions) {
super({
log: opts.log,
maxBufferSize: MAX_RECENT_ITEMS,
});
this._udid = opts.udid;
this._sim = opts.sim;
this._realDeviceClient = this._isRealDevice()
? new Pyidevice({
udid: this._udid as string,
log: opts.log,
})
: null;
this._logDir = this._isRealDevice()
? null
: path.resolve(process.env.HOME || '/', 'Library', 'Logs', 'DiagnosticReports');
this._recentCrashFiles = [];
this._started = false;
}
override async startCapture(): Promise<void> {
this._recentCrashFiles = await this._listCrashFiles(false);
this._started = true;
}
override async stopCapture(): Promise<void> {
this._started = false;
}
override get isCapturing(): boolean {
return this._started;
}
override async getLogs(): Promise<LogEntry[]> {
const crashFiles = (await this._listCrashFiles(true)).slice(-MAX_RECENT_ITEMS);
const diffFiles = _.difference(crashFiles, this._recentCrashFiles);
if (_.isEmpty(diffFiles)) {
return [];
}
this.log.debug(`Found ${util.pluralize('fresh crash report', diffFiles.length, true)}`);
await this._serializeCrashes(diffFiles);
this._recentCrashFiles = crashFiles;
return super.getLogs();
}
protected override _serializeEntry(value: TSerializedEntry): TSerializedEntry {
return value;
}
protected override _deserializeEntry(value: TSerializedEntry): LogEntry {
const [message, timestamp] = value;
return toLogEntry(message, timestamp);
}
private async _serializeCrashes(paths: string[]): Promise<void> {
const tmpRoot = await tempDir.openDir();
try {
for (const filePath of paths) {
let fullPath = filePath;
if (this._isRealDevice()) {
const fileName = filePath;
try {
await (this._realDeviceClient as BaseDeviceClient).exportCrash(fileName, tmpRoot);
} catch (e) {
this.log.warn(
`Cannot export the crash report '${fileName}'. Skipping it. ` +
`Original error: ${e.message}`,
);
return;
}
fullPath = path.join(tmpRoot, fileName);
}
const {ctime} = await fs.stat(fullPath);
this.broadcast([await fs.readFile(fullPath, 'utf8'), ctime.getTime()]);
}
} finally {
await fs.rimraf(tmpRoot);
}
}
private async _gatherFromRealDevice(strict: boolean): Promise<string[]> {
if (!this._realDeviceClient) {
return [];
}
if (!await this._realDeviceClient.assertExists(strict)) {
this.log.info(
`The ${_.toLower(this._realDeviceClient.constructor.name)} tool is not present in PATH. ` +
`Skipping crash logs collection for real devices.`
);
return [];
}
return await this._realDeviceClient.listCrashes();
}
private async _gatherFromSimulator(): Promise<string[]> {
if (!this._logDir || !this._sim || !(await fs.exists(this._logDir))) {
this.log.debug(`Crash reports root '${this._logDir}' does not exist. Got nothing to gather.`);
return [];
}
const foundFiles = await fs.glob(CRASH_REPORTS_GLOB_PATTERN, {
cwd: this._logDir,
absolute: true,
});
const simUdid = (this._sim as Simulator).udid;
// For Simulator only include files, that contain current UDID
return await B.filter(foundFiles, async (filePath) => {
try {
return await grepFile(filePath, simUdid, {caseInsensitive: true});
} catch (err) {
this.log.warn(err);
return false;
}
});
}
private async _listCrashFiles(strict: boolean): Promise<string[]> {
return this._isRealDevice()
? await this._gatherFromRealDevice(strict)
: await this._gatherFromSimulator();
}
private _isRealDevice(): boolean {
return Boolean(this._udid);
}
}
export default IOSCrashLog;