Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Emit logs over BiDi socket #955

Merged
merged 2 commits into from
Sep 8, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
81 changes: 56 additions & 25 deletions lib/commands/log.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import {DEFAULT_WS_PATHNAME_PREFIX, BaseDriver} from 'appium/driver';
import {util} from 'appium/support';
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 +31,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 +43,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 +154,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: util.uuidV4(),
mykola-mokhnach marked this conversation as resolved.
Show resolved Hide resolved
},
text: finalEntry.message,
timestamp: finalEntry.timestamp,
},
});
};
logEmitter.on(srcEventName, listener);
return [logEmitter, listener];
}

/**
* @this {import('../driver').AndroidDriver}
* @param {string} logType
Expand All @@ -179,23 +214,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
*/
Loading