appium-xcuitest-driver
Version:
Appium driver for iOS using XCUITest for backend
187 lines • 7.52 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.IOSCrashLog = void 0;
const support_1 = require("appium/support");
const asyncbox_1 = require("asyncbox");
const node_path_1 = __importDefault(require("node:path"));
const utils_1 = require("../../utils");
const crash_reports_client_1 = require("../crash-reports-client");
const ios_log_1 = require("./ios-log");
const helpers_1 = require("./helpers");
// 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;
/**
* Collects iOS/tvOS crash logs for BiDi `log.entryAdded` / classic log APIs.
*
* - **Simulator:** reads `~/Library/Logs/DiagnosticReports` and filters by simulator UDID.
* - **Real device:** uses RemoteXPC (`appium-ios-remotexpc`) when `useRemoteXPC` is true; if the client
* cannot be created, collection is skipped and errors are logged (no session failure).
*/
class IOSCrashLog extends ios_log_1.IOSLog {
_udid;
_useRemoteXPC;
_realDeviceClient;
_logDir;
_sim;
_recentCrashFiles;
_started;
/**
* @param opts - Provide `udid` for a real device or `sim` for a Simulator (mutually exclusive by usage).
*/
constructor(opts) {
super({
log: opts.log,
maxBufferSize: MAX_RECENT_ITEMS,
});
this._udid = opts.udid;
this._sim = opts.sim;
this._useRemoteXPC = opts.useRemoteXPC ?? false;
this._realDeviceClient = null;
this._logDir = this._isRealDevice()
? null
: node_path_1.default.resolve(process.env.HOME || '/', 'Library', 'Logs', 'DiagnosticReports');
this._recentCrashFiles = [];
this._started = false;
}
get isCapturing() {
return this._started;
}
/** Records the current crash file snapshot so only new reports appear in {@link IOSCrashLog.getLogs}. */
async startCapture() {
this._recentCrashFiles = await this._listCrashFiles();
this._started = true;
}
/** Stops polling and closes any real-device {@link CrashReportsClient}. */
async stopCapture() {
this._started = false;
// Clean up the client connection
if (this._realDeviceClient) {
await this._realDeviceClient.close();
this._realDeviceClient = null;
}
}
/**
* @returns New crash log entries since the last successful poll (bounded by {@link MAX_RECENT_ITEMS}).
*/
async getLogs() {
const crashFiles = (await this._listCrashFiles()).slice(-MAX_RECENT_ITEMS);
const recentCrashFiles = new Set(this._recentCrashFiles);
const diffFiles = crashFiles.filter((file) => !recentCrashFiles.has(file));
if ((0, utils_1.isEmpty)(diffFiles)) {
return [];
}
this.log.debug(`Found ${support_1.util.pluralize('fresh crash report', diffFiles.length, true)}`);
await this._serializeCrashes(diffFiles);
this._recentCrashFiles = crashFiles;
return super.getLogs();
}
_serializeEntry(value) {
return value;
}
_deserializeEntry(value) {
const [message, timestamp] = value;
return (0, helpers_1.toLogEntry)(message, timestamp);
}
/** Reads crash file contents and {@link IOSLog.broadcast}s them as `[text, mtime]` tuples. */
async _serializeCrashes(paths) {
const tmpRoot = await support_1.tempDir.openDir();
try {
for (const filePath of paths) {
let fullPath = filePath;
if (this._isRealDevice()) {
const fileName = filePath;
try {
await this._realDeviceClient.exportCrash(fileName, tmpRoot);
}
catch (e) {
this.log.warn(`Cannot export the crash report '${fileName}'. Skipping it. ` +
`Original error: ${e.message}`);
return;
}
fullPath = node_path_1.default.join(tmpRoot, fileName);
}
const { ctime } = await support_1.fs.stat(fullPath);
this.broadcast([await support_1.fs.readFile(fullPath, 'utf8'), ctime.getTime()]);
}
}
finally {
await support_1.fs.rimraf(tmpRoot);
}
}
/**
* Lazily creates a {@link CrashReportsClient} and lists `.ips` basenames on the device.
*
* @returns Empty array if RemoteXPC setup or listing fails (logged). The client is reset after
* listing errors so a later poll can recreate it. Never throws to callers.
*/
async _gatherFromRealDevice() {
if (!this._realDeviceClient) {
try {
this._realDeviceClient = await crash_reports_client_1.CrashReportsClient.create(this._udid, this._useRemoteXPC);
}
catch (err) {
this.log.error(`Failed to create crash reports client: ${err.message}. ` +
`Skipping crash logs collection for real devices.`);
return [];
}
}
try {
return await this._realDeviceClient.listCrashes();
}
catch (err) {
this.log.error(`Failed to list crash reports on device: ${err.message}. ` +
`Skipping this poll; the next poll will attempt to reconnect.`);
const client = this._realDeviceClient;
this._realDeviceClient = null;
if (client) {
try {
await client.close();
}
catch {
// ignore secondary teardown errors
}
}
return [];
}
}
/** Glob diagnostic reports and keep files whose content references the simulator UDID. */
async _gatherFromSimulator() {
if (!this._logDir || !this._sim || !(await support_1.fs.exists(this._logDir))) {
this.log.debug(`Crash reports root '${this._logDir}' does not exist. Got nothing to gather.`);
return [];
}
const foundFiles = await support_1.fs.glob(CRASH_REPORTS_GLOB_PATTERN, {
cwd: this._logDir,
absolute: true,
});
const simUdid = this._sim.udid;
// For Simulator only include files, that contain current UDID
return await (0, asyncbox_1.asyncfilter)(foundFiles, async (filePath) => {
try {
return await (0, helpers_1.grepFile)(filePath, simUdid, { caseInsensitive: true });
}
catch (err) {
this.log.warn(err);
return false;
}
});
}
/** Dispatches to real-device RemoteXPC listing or simulator filesystem globbing. */
async _listCrashFiles() {
return this._isRealDevice()
? await this._gatherFromRealDevice()
: await this._gatherFromSimulator();
}
_isRealDevice() {
return Boolean(this._udid);
}
}
exports.IOSCrashLog = IOSCrashLog;
exports.default = IOSCrashLog;
//# sourceMappingURL=ios-crash-log.js.map