Skip to content

Commit

Permalink
feat: Emit logs over BiDi socket (#955)
Browse files Browse the repository at this point in the history
  • Loading branch information
mykola-mokhnach authored Sep 8, 2024
1 parent 2cb7adc commit 4f7045b
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 25 deletions.
18 changes: 18 additions & 0 deletions lib/commands/device/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import {
} from './utils';
import {adjustTimeZone} from '../time';
import { retryInterval } from 'asyncbox';
import {
GET_SERVER_LOGS_FEATURE,
nativeLogEntryToSeleniumEntry
} from '../../utils';

/**
* @this {AndroidDriver}
Expand Down Expand Up @@ -247,6 +251,11 @@ export async function initDevice() {
filterSpecs: logcatFilterSpecs,
});
this.eventEmitter.emit('syslogStarted', this.adb.logcat);
if (this.adb.logcat) {
this.assignBiDiLogListener(this.adb.logcat, {
type: 'syslog',
});
}
};
setupPromises.push(logcatStartupPromise());
}
Expand All @@ -272,6 +281,15 @@ export async function initDevice() {
if (timeZone) {
setupPromises.push(adjustTimeZone.bind(this)(timeZone));
}
if (this.isFeatureEnabled(GET_SERVER_LOGS_FEATURE)) {
[, this._bidiServerLogListener] = this.assignBiDiLogListener(
this.log.unwrap(), {
type: 'server',
srcEventName: 'log',
entryTransformer: nativeLogEntryToSeleniumEntry,
}
);
}

await B.all(setupPromises);
}
Expand Down
80 changes: 55 additions & 25 deletions lib/commands/log.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@ import {DEFAULT_WS_PATHNAME_PREFIX, BaseDriver} from 'appium/driver';
import _ from 'lodash';
import os from 'node:os';
import WebSocket from 'ws';

const GET_SERVER_LOGS_FEATURE = 'get_server_logs';
import {
BIDI_EVENT_NAME,
GET_SERVER_LOGS_FEATURE,
toLogRecord,
nativeLogEntryToSeleniumEntry,
} from '../utils';
import { NATIVE_WIN } from './context/helpers';

export const supportedLogTypes = {
logcat: {
Expand All @@ -25,7 +30,7 @@ export const supportedLogTypes = {
getter: async (self) => {
const output = await /** @type {ADB} */ (self.adb).bugreport();
const timestamp = Date.now();
return output.split(os.EOL).map((x) => toLogRecord(timestamp, 'ALL', x));
return output.split(os.EOL).map((x) => toLogRecord(timestamp, x));
},
},
server: {
Expand All @@ -37,14 +42,7 @@ export const supportedLogTypes = {
*/
getter: (self) => {
self.ensureFeatureEnabled(GET_SERVER_LOGS_FEATURE);
return self.log
.unwrap()
.record.map((x) => toLogRecord(
/** @type {any} */ (x).timestamp ?? Date.now(),
'ALL',
_.isEmpty(x.prefix) ? x.message : `[${x.prefix}] ${x.message}`,
),
);
return self.log.unwrap().record.map(nativeLogEntryToSeleniumEntry);
},
},
};
Expand Down Expand Up @@ -155,6 +153,42 @@ export async function getLogTypes() {
return nativeLogTypes;
}

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

/**
* @this {import('../driver').AndroidDriver}
* @param {string} logType
Expand All @@ -179,23 +213,19 @@ export async function getLog(logType) {
const WEBSOCKET_ENDPOINT = (sessionId) =>
`${DEFAULT_WS_PATHNAME_PREFIX}/session/${sessionId}/appium/device/logcat`;

/**
*
* @see {@link https://github.com/SeleniumHQ/selenium/blob/0d425676b3c9df261dd641917f867d4d5ce7774d/java/client/src/org/openqa/selenium/logging/LogEntry.java}
* @param {number} timestamp
* @param {string} level
* @param {string} message
*/
function toLogRecord(timestamp, level, message) {
return {
timestamp,
level,
message,
};
}

// #endregion

/**
* @typedef {import('appium-adb').ADB} ADB
*/

/**
* @typedef {Object} BiDiListenerProperties
* @property {string} type
* @property {string} [srcEventName='output']
* @property {string} [context=NATIVE_WIN]
* @property {(x: Object) => import('../utils').LogEntry} [entryTransformer]
*/

/** @typedef {(logEntry: import('../utils').LogEntry) => any} LogListener */
10 changes: 10 additions & 0 deletions lib/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ import {
mobileStopLogsBroadcast,
getLogTypes,
getLog,
assignBiDiLogListener,
} from './commands/log';
import {
mobileIsMediaProjectionRecordingRunning,
Expand Down Expand Up @@ -252,6 +253,8 @@ class AndroidDriver

_logcatWebsocketListener?: LogcatListener;

_bidiServerLogListener?: (...args: any[]) => void;

opts: AndroidDriverOpts;

constructor(opts: InitialOpts = {} as InitialOpts, shouldValidateCaps = true) {
Expand All @@ -271,6 +274,7 @@ class AndroidDriver
this.curContext = this.defaultContextName();
this.opts = opts as AndroidDriverOpts;
this._cachedActivityArgs = {};
this.doesSupportBidi = true;
}

get settingsApp(): SettingsApp {
Expand Down Expand Up @@ -330,11 +334,16 @@ class AndroidDriver
}

try {
this.adb?.logcat?.removeAllListeners();
await this.adb?.stopLogcat();
} catch (e) {
this.log.warn(`Cannot stop the logcat process. Original error: ${e.message}`);
}

if (this._bidiServerLogListener) {
this.log.unwrap().off('log', this._bidiServerLogListener);
}

await super.deleteSession(sessionId);
}

Expand Down Expand Up @@ -488,6 +497,7 @@ class AndroidDriver
mobileStopLogsBroadcast = mobileStopLogsBroadcast;
getLogTypes = getLogTypes;
getLog = getLog;
assignBiDiLogListener = assignBiDiLogListener;

mobileIsMediaProjectionRecordingRunning = mobileIsMediaProjectionRecordingRunning;
mobileStartMediaProjectionRecording = mobileStartMediaProjectionRecording;
Expand Down
34 changes: 34 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import _ from 'lodash';
import {errors} from 'appium/driver';

export const ADB_SHELL_FEATURE = 'adb_shell';
export const BIDI_EVENT_NAME = 'bidiEvent';
export const GET_SERVER_LOGS_FEATURE = 'get_server_logs';

/**
* Assert the presence of particular keys in the given object
Expand Down Expand Up @@ -55,3 +57,35 @@ export async function removeAllSessionWebSocketHandlers(server, sessionId) {
await server.removeWebSocketHandler(pathname);
}
}

/**
*
* @param {Object} x
* @returns {LogEntry}
*/
export function nativeLogEntryToSeleniumEntry (x) {
return toLogRecord(
_.isEmpty(x.prefix) ? x.message : `[${x.prefix}] ${x.message}`,
/** @type {any} */ (x).timestamp ?? Date.now()
);
}

/**
*
* @see {@link https://github.com/SeleniumHQ/selenium/blob/0d425676b3c9df261dd641917f867d4d5ce7774d/java/client/src/org/openqa/selenium/logging/LogEntry.java}
* @param {number} timestamp
* @param {string} message
* @param {string} [level='ALL']
* @returns {LogEntry}
*/
export function toLogRecord(timestamp, message, level = 'ALL') {
return {
timestamp,
level,
message,
};
}

/**
* @typedef {import('appium-adb').LogcatRecord} LogEntry
*/

0 comments on commit 4f7045b

Please sign in to comment.