Skip to content

Commit

Permalink
feat: Publish different log types over BiDi
Browse files Browse the repository at this point in the history
  • Loading branch information
mykola-mokhnach committed Aug 27, 2024
1 parent 673ebe9 commit 65150d2
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 47 deletions.
18 changes: 12 additions & 6 deletions lib/commands/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import {errors, isErrorType} from 'appium/driver';
import {util, timing} from 'appium/support';
import IOSPerformanceLog from '../device-log/ios-performance-log';
import _ from 'lodash';
import { NATIVE_WIN } from '../utils';

const NATIVE_WIN = 'NATIVE_APP';
const WEBVIEW_WIN = 'WEBVIEW';
const WEBVIEW_BASE = `${WEBVIEW_WIN}_`;
const DEFAULT_REMOTE_DEBUGGER_CONNECT_TIMEOUT_MS = 5000;
Expand Down Expand Up @@ -556,11 +556,17 @@ const commands = {

// attempt to start performance logging, if requested
if (this.opts.enablePerformanceLogging && this.remote) {
this.log.debug(`Starting performance log on '${this.curContext}'`);
this.logs.performance = new IOSPerformanceLog({
remoteDebugger: this.remote,
log: this.log,
});
const context = this.curContext;
this.log.debug(`Starting performance log on '${context}'`);
[this.logs.performance,] = this.assignBiDiLogListener(
new IOSPerformanceLog({
remoteDebugger: this.remote,
log: this.log,
}), {
type: 'performance',
context,
}
);
await this.logs.performance.startCapture();
}

Expand Down
143 changes: 105 additions & 38 deletions lib/commands/log.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import {DEFAULT_WS_PATHNAME_PREFIX} from 'appium/driver';
import {IOSCrashLog} from '../device-log/ios-crash-log';
import {IOSSimulatorLog} from '../device-log/ios-simulator-log';
import {IOSDeviceLog} from '../device-log/ios-device-log';
import log from '../logger';
import WebSocket from 'ws';
import SafariConsoleLog from '../device-log/safari-console-log';
import SafariNetworkLog from '../device-log/safari-network-log';
import { toLogEntry } from '../device-log/helpers';
import { NATIVE_WIN, BIDI_EVENT_NAME } from '../utils';
import { util } from 'appium/support';

/**
* Determines the websocket endpoint based on the `sessionId`
Expand Down Expand Up @@ -52,7 +53,7 @@ const SUPPORTED_LOG_TYPES = {
*/
getter: (self) => {
self.assertFeatureEnabled(GET_SERVER_LOGS_FEATURE);
return log.unwrap().record.map((x) => toLogEntry(
return self.log.unwrap().record.map((x) => toLogEntry(
_.isEmpty(x.prefix) ? x.message : `[${x.prefix}] ${x.message}`,
/** @type {any} */ (x).timestamp ?? Date.now()
));
Expand Down Expand Up @@ -97,44 +98,103 @@ export default {
);
},

/**
* https://w3c.github.io/webdriver-bidi/#event-log-entryAdded
*
* @template {import('node:events').EventEmitter} EE
* @this {XCUITestDriver}
* @param {EE} logEmitter
* @param {BiDiListenerProperties} properties
* @returns {[EE, import('./types').LogListener]}
*/
assignBiDiListener (logEmitter, properties) {
const {
type,
context = NATIVE_WIN,
srcEventName = 'output',
} = properties;
const listener = (/** @type {import('./types').LogEntry} */ logEntry) =>
this.eventEmitter.emit(BIDI_EVENT_NAME, {
context,
method: 'log.entryAdded',
params: {
type,
level: logEntry.level,
source: {
realm: util.uuidV4(),
},
text: logEntry.message,
timestamp: logEntry.timestamp,
},
});
logEmitter.on(srcEventName, listener);
return [logEmitter, listener];
},

/**
* @this {XCUITestDriver}
*/
async startLogCapture() {
this.logs = this.logs || {};
if (!_.isUndefined(this.logs.syslog) && this.logs.syslog.isCapturing) {
log.warn('Trying to start iOS log capture but it has already started!');
this.log.warn('Trying to start iOS log capture but it has already started!');
return true;
}

if (_.isUndefined(this.logs.syslog)) {
this.logs.crashlog = new IOSCrashLog({
sim: /** @type {import('appium-ios-simulator').Simulator} */ (this.device),
udid: this.isRealDevice() ? this.opts.udid : undefined,
log: this.log,
});
this.logs.syslog = this.isRealDevice()
? new IOSDeviceLog({
udid: this.opts.udid,
showLogs: this.opts.showIOSLog,
log: this.log,
})
: new IOSSimulatorLog({
[this.logs.crashlog,] = this.assignBiDiLogListener(
new IOSCrashLog({
sim: /** @type {import('appium-ios-simulator').Simulator} */ (this.device),
showLogs: this.opts.showIOSLog,
iosSimulatorLogsPredicate: this.opts.iosSimulatorLogsPredicate,
udid: this.isRealDevice() ? this.opts.udid : undefined,
log: this.log,
});
}), {
type: 'crashlog',
}
);
[this.logs.syslog,] = this.assignBiDiLogListener(
this.isRealDevice()
? new IOSDeviceLog({
udid: this.opts.udid,
showLogs: this.opts.showIOSLog,
log: this.log,
})
: new IOSSimulatorLog({
sim: /** @type {import('appium-ios-simulator').Simulator} */ (this.device),
showLogs: this.opts.showIOSLog,
iosSimulatorLogsPredicate: this.opts.iosSimulatorLogsPredicate,
log: this.log,
}),
{
type: 'syslog',
}
);
if (_.isBoolean(this.opts.showSafariConsoleLog)) {
this.logs.safariConsole = new SafariConsoleLog({
showLogs: this.opts.showSafariConsoleLog,
log: this.log,
});
[this.logs.safariConsole,] = this.assignBiDiLogListener(
new SafariConsoleLog({
showLogs: this.opts.showSafariConsoleLog,
log: this.log,
}), {
type: 'safariConsole',
}
);
}
if (_.isBoolean(this.opts.showSafariNetworkLog)) {
this.logs.safariNetwork = new SafariNetworkLog({
showLogs: this.opts.showSafariNetworkLog,
log: this.log,
});
[this.logs.safariNetwork,] = this.assignBiDiLogListener(
new SafariNetworkLog({
showLogs: this.opts.showSafariNetworkLog,
log: this.log,
}), {
type: 'safariNetwork',
}
);
}
if (this.isFeatureEnabled(GET_SERVER_LOGS_FEATURE)) {
[, this._bidiServerLogListener] = this.assignBiDiLogListener(
this.log.unwrap(), {
type: 'server',
srcEventName: 'log',
}
);
}
}

Expand All @@ -143,15 +203,15 @@ export default {
const promises = [
(async () => {
try {
await this.logs.syslog.startCapture();
await this.logs.syslog?.startCapture();
didStartSyslog = true;
this.eventEmitter.emit('syslogStarted', this.logs.syslog);
} catch (err) {
log.debug(err.stack);
log.warn(`Continuing without capturing device logs: ${err.message}`);
this.log.debug(err.stack);
this.log.warn(`Continuing without capturing device logs: ${err.message}`);
}
})(),
this.logs.crashlog.startCapture(),
this.logs.crashlog?.startCapture() ?? B.resolve(),
];
await B.all(promises);

Expand Down Expand Up @@ -179,13 +239,13 @@ export default {
).getWebSocketHandlers(pathname),
)
) {
log.debug(
this.log.debug(
`The system logs broadcasting web socket server is already listening at ${pathname}`,
);
return;
}

log.info(`Assigning system logs broadcasting web socket server to ${pathname}`);
this.log.info(`Assigning system logs broadcasting web socket server to ${pathname}`);
// https://github.com/websockets/ws/blob/master/doc/ws.md
const wss = new WebSocket.Server({
noServer: true,
Expand All @@ -195,9 +255,9 @@ export default {
const remoteIp = _.isEmpty(req.headers['x-forwarded-for'])
? req.connection?.remoteAddress
: req.headers['x-forwarded-for'];
log.debug(`Established a new system logs listener web socket connection from ${remoteIp}`);
this.log.debug(`Established a new system logs listener web socket connection from ${remoteIp}`);
} else {
log.debug('Established a new system logs listener web socket connection');
this.log.debug('Established a new system logs listener web socket connection');
}

if (_.isEmpty(this._syslogWebsocketListener)) {
Expand All @@ -207,11 +267,11 @@ export default {
}
};
}
this.logs.syslog.on('output', this._syslogWebsocketListener);
this.logs.syslog?.on('output', this._syslogWebsocketListener);

ws.on('close', (code, reason) => {
if (!_.isEmpty(this._syslogWebsocketListener)) {
this.logs.syslog.removeListener('output', this._syslogWebsocketListener);
this.logs.syslog?.removeListener('output', this._syslogWebsocketListener);
this._syslogWebsocketListener = null;
}

Expand All @@ -222,7 +282,7 @@ export default {
if (!_.isEmpty(reason)) {
closeMsg += ` Reason: ${reason.toString()}.`;
}
log.debug(closeMsg);
this.log.debug(closeMsg);
});
});
await /** @type {AppiumServer} */ (this.server).addWebSocketHandler(
Expand All @@ -243,7 +303,7 @@ export default {
return;
}

log.debug('Stopping the system logs broadcasting web socket server');
this.log.debug('Stopping the system logs broadcasting web socket server');
await /** @type {AppiumServer} */ (this.server).removeWebSocketHandler(pathname);
},
};
Expand All @@ -259,3 +319,10 @@ export default {
/**
* @typedef {import('@appium/types').AppiumServer} AppiumServer
*/

/**
* @typedef {Object} BiDiListenerProperties
* @property {string} type
* @property {string} [srcEventName='output']
* @property {string} [context=NATIVE_WIN]
*/
2 changes: 2 additions & 0 deletions lib/commands/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -572,3 +572,5 @@ export interface LogEntry {
level: string,
message: string;
}

export type LogListener = (logEntry: LogEntry) => any;
25 changes: 22 additions & 3 deletions lib/driver.js
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,12 @@ export class XCUITestDriver extends BaseDriver {
/** @type {import('appium-remote-debugger').RemoteDebugger|null} */
remote;

/** @type {DriverLogs} */
logs;

/** @type {import('./commands/types').LogListener|undefined} */
_bidiServerLogListener;

/**
*
* @param {XCUITestDriverOpts} opts
Expand Down Expand Up @@ -315,6 +321,7 @@ export class XCUITestDriver extends BaseDriver {
this._audioRecorder = null;
this.appInfosCache = new AppInfosCache(this.log);
this.remote = null;
this.doesSupportBidi = true;
}

async onSettingsUpdate(key, value) {
Expand Down Expand Up @@ -977,10 +984,12 @@ export class XCUITestDriver extends BaseDriver {
}
}

if (!_.isEmpty(this.logs)) {
await this.logs.syslog.stopCapture();
this.logs = {};
await this.logs.syslog?.stopCapture();
_.values(this.logs).forEach((x) => x.removeAllListeners());
if (this._bidiServerLogListener) {
this.log.unwrap().off('log', this._bidiServerLogListener);
}
this.logs = {};

if (this.mjpegStream) {
this.log.info('Closing MJPEG stream');
Expand Down Expand Up @@ -2007,6 +2016,7 @@ export class XCUITestDriver extends BaseDriver {
extractLogs = commands.logExtensions.extractLogs;
supportedLogTypes = commands.logExtensions.supportedLogTypes;
startLogCapture = commands.logExtensions.startLogCapture;
assignBiDiLogListener = commands.logExtensions.assignBiDiListener;
mobileStartLogsBroadcast = commands.logExtensions.mobileStartLogsBroadcast;
mobileStopLogsBroadcast = commands.logExtensions.mobileStopLogsBroadcast;

Expand Down Expand Up @@ -2180,3 +2190,12 @@ export default XCUITestDriver;
* @typedef {import('appium-xcode').XcodeVersion} XcodeVersion
* @typedef {import('appium-ios-simulator').Simulator} Simulator
*/

/**
* @typedef {Object} DriverLogs
* @property {import('./device-log/ios-device-log').IOSDeviceLog|import('./device-log/ios-simulator-log').IOSSimulatorLog} [syslog]
* @property {import('./device-log/ios-crash-log').IOSCrashLog} [crashlog]
* @property {import('./device-log/safari-console-log').SafariConsoleLog} [safariConsole]
* @property {import('./device-log/safari-network-log').SafariNetworkLog} [safariNetwork]
* @property {import('./device-log/ios-performance-log').IOSPerformanceLog} [performance]
*/
2 changes: 2 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const XCTEST_LOG_FILES_PATTERNS = [
/^StandardOutputAndStandardError\.txt$/i,
];
const XCTEST_LOGS_CACHE_FOLDER_PREFIX = 'com.apple.dt.XCTest';
export const NATIVE_WIN = 'NATIVE_APP';
export const BIDI_EVENT_NAME = 'bidiEvent';

/**
* @privateRemarks Is the minimum version really Xcode 7.3?
Expand Down

0 comments on commit 65150d2

Please sign in to comment.