appium-xcuitest-driver
Version:
Appium driver for iOS using XCUITest for backend
184 lines • 7.08 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.IOSSimulatorLog = void 0;
const lodash_1 = __importDefault(require("lodash"));
const teen_process_1 = require("teen_process");
const support_1 = require("appium/support");
const line_consuming_log_1 = require("./line-consuming-log");
const winston_1 = require("winston");
const promises_1 = __importDefault(require("node:fs/promises"));
const EXECVP_ERROR_PATTERN = /execvp\(\)/;
const LOG_STREAMING_PROCESS_NAME_PATTERN = /^com\.apple\.xpc\.launchd\.oneshot\.0x[0-f]+\.log$/;
const START_TIMEOUT = 10000;
class IOSSimulatorLog extends line_consuming_log_1.LineConsumingLog {
sim;
showLogs;
predicate;
logLevel;
proc;
iosSyslogFile;
syslogLogger;
constructor(opts) {
super({ log: opts.log });
this.sim = opts.sim;
this.showLogs = !!opts.showLogs;
this.predicate = opts.iosSimulatorLogsPredicate;
this.logLevel = opts.simulatorLogLevel;
this.proc = null;
this.iosSyslogFile = opts.iosSyslogFile;
this.syslogLogger = null;
}
async startCapture() {
if (lodash_1.default.isUndefined(this.sim.udid)) {
throw new Error(`Log capture requires a sim udid`);
}
if (!(await this.sim.isRunning())) {
throw new Error(`iOS Simulator with udid '${this.sim.udid}' is not running`);
}
if (this.iosSyslogFile && this.showLogs) {
await this.clearExistingSyslog();
try {
this.syslogLogger = (0, winston_1.createLogger)({
level: 'info',
format: winston_1.format.combine(winston_1.format.timestamp(), winston_1.format.simple()),
transports: [new winston_1.transports.File({ filename: this.iosSyslogFile })],
exitOnError: false
});
this.log.debug(`iOS syslog will be written to: '${this.iosSyslogFile}'`);
}
catch (e) {
this.log.warn(`Could not set up iOS syslog logger for '${this.iosSyslogFile}': ${e.message}`);
this.syslogLogger = null;
}
}
const spawnArgs = ['log', 'stream', '--style', 'compact'];
if (this.predicate) {
spawnArgs.push('--predicate', this.predicate);
}
if (this.logLevel) {
spawnArgs.push('--level', this.logLevel);
}
this.log.debug(`Starting log capture for iOS Simulator with udid '${this.sim.udid}' ` +
`via simctl using the following arguments '${support_1.util.quote(spawnArgs)}'`);
await this.cleanupObsoleteLogStreams();
try {
this.proc = await this.sim.simctl.spawnSubProcess(spawnArgs);
await this.finishStartingLogCapture();
}
catch (e) {
this.shutdownSyslogger();
throw new Error(`Simulator log capture failed. Original error: ${e.message}`);
}
}
async stopCapture() {
if (!this.proc) {
return;
}
await this.killLogSubProcess();
this.proc = null;
this.shutdownSyslogger();
}
get isCapturing() {
return Boolean(this.proc?.isRunning);
}
async clearExistingSyslog() {
if (this.iosSyslogFile === undefined) {
this.log.debug('iOS Syslog file path is not defined, skipping deletion.');
return;
}
try {
await promises_1.default.unlink(this.iosSyslogFile);
this.log.debug(`Existing iOS Syslog file: '${this.iosSyslogFile}' deleted.`);
}
catch (unlinkErr) {
if (unlinkErr.code !== 'ENOENT') {
this.log.warn(`Could not delete existing syslog file '${this.iosSyslogFile}': ${unlinkErr.message}`);
}
}
}
shutdownSyslogger() {
if (this.syslogLogger) {
this.log.debug(`Closing iOS syslog file: '${this.iosSyslogFile}'`);
const fileTransport = this.syslogLogger.transports.find((t) => (t instanceof winston_1.transports.File) && (t.filename === this.iosSyslogFile));
if (fileTransport) {
fileTransport.end?.();
this.syslogLogger.remove(fileTransport);
}
this.syslogLogger = null;
}
}
onOutput(logRow, prefix = '') {
this.broadcast(logRow);
const space = prefix.length > 0 ? ' ' : '';
if (this.showLogs && !this.iosSyslogFile) {
this.log.info(`[IOS_SYSLOG_ROW${space}${prefix}] ${logRow}`);
}
else if (this.iosSyslogFile && this.showLogs) {
this.writeToSyslogFile(`[IOS_SYSLOG_ROW${space}${prefix}] ${logRow}`);
}
}
/**
* Writes the given log row to the dedicated iOS syslog file if the logger is active.
* @param {string} logRow - The log line to write.
* @private
*/
writeToSyslogFile(logRow) {
if (this.syslogLogger && this.showLogs) {
this.syslogLogger.info(logRow);
}
}
async killLogSubProcess() {
if (!this.proc?.isRunning) {
return;
}
this.log.debug('Stopping iOS log capture');
try {
await this.proc.stop('SIGTERM', 1000);
}
catch {
if (!this.proc.isRunning) {
return;
}
this.log.warn('Cannot stop log capture process. Sending SIGKILL');
await this.proc.stop('SIGKILL');
}
}
async finishStartingLogCapture() {
if (!this.proc) {
throw this.log.errorWithException('Could not capture simulator log');
}
for (const streamName of ['stdout', 'stderr']) {
this.proc.on(`line-${streamName}`, (line) => {
this.onOutput(line, ...(streamName === 'stderr' ? ['STDERR'] : []));
});
}
const startDetector = (stdout, stderr) => {
if (EXECVP_ERROR_PATTERN.test(stderr)) {
throw new Error('iOS log capture process failed to start');
}
return Boolean(stdout || stderr);
};
await this.proc.start(startDetector, START_TIMEOUT);
}
async cleanupObsoleteLogStreams() {
const processes = await this.sim.ps();
const pids = processes
.filter(({ name }) => LOG_STREAMING_PROCESS_NAME_PATTERN.test(name))
.map(({ pid }) => pid);
if (lodash_1.default.isEmpty(pids)) {
return;
}
try {
await (0, teen_process_1.exec)('kill', pids.map(String));
}
catch (e) {
this.log.warn(`Could not terminate one or more obsolete log streams: ${e.stderr || e.message}`);
}
}
}
exports.IOSSimulatorLog = IOSSimulatorLog;
exports.default = IOSSimulatorLog;
//# sourceMappingURL=ios-simulator-log.js.map