UNPKG

appium-android-driver

Version:

Android UiAutomator and Chrome support for Appium

216 lines (198 loc) 7.08 kB
import {DEFAULT_WS_PATHNAME_PREFIX, BaseDriver} from 'appium/driver'; import _ from 'lodash'; import os from 'node:os'; import WebSocket from 'ws'; import type {AppiumServer, WSServer} from '@appium/types'; import type {EventEmitter} from 'node:events'; import type {ADB} from 'appium-adb'; import type {Chromedriver} from 'appium-chromedriver'; import { GET_SERVER_LOGS_FEATURE, toLogRecord, nativeLogEntryToSeleniumEntry, type LogEntry, } from '../utils'; import { NATIVE_WIN } from './context/helpers'; import { BIDI_EVENT_NAME } from './bidi/constants'; import { makeLogEntryAddedEvent } from './bidi/models'; import type {AndroidDriver} from '../driver'; export const supportedLogTypes = { logcat: { description: 'Logs for Android applications on real device and emulators via ADB', getter: (self: AndroidDriver) => (self.adb as ADB).getLogcatLogs(), }, bugreport: { description: `'adb bugreport' output for advanced issues diagnostic`, getter: async (self: AndroidDriver) => { const output = await (self.adb as ADB).bugreport(); const timestamp = Date.now(); return output.split(os.EOL).map((x) => toLogRecord(timestamp, x)); }, }, server: { description: 'Appium server logs', getter: (self: AndroidDriver) => { self.assertFeatureEnabled(GET_SERVER_LOGS_FEATURE); return self.log.unwrap().record.map(nativeLogEntryToSeleniumEntry); }, }, } as const; /** * Starts Android logcat broadcast websocket on the same host and port * where Appium server is running at `/ws/session/:sessionId:/appium/logcat` endpoint. The method * will return immediately if the web socket is already listening. * * Each connected websocket listener will receive logcat log lines * as soon as they are visible to Appium. * * @returns Promise that resolves when the logcat broadcasting websocket is started. */ export async function mobileStartLogsBroadcast( this: AndroidDriver, ): Promise<void> { const server = this.server as AppiumServer; const pathname = WEBSOCKET_ENDPOINT(this.sessionId as string); if (!_.isEmpty(await server.getWebSocketHandlers(pathname))) { this.log.debug(`The logcat broadcasting web socket server is already listening at ${pathname}`); return; } this.log.info( `Starting logcat broadcasting on web socket server ` + `${JSON.stringify(server.address())} to ${pathname}`, ); // https://github.com/websockets/ws/blob/master/doc/ws.md const wss = new WebSocket.Server({ noServer: true, }); wss.on('connection', (ws, req) => { if (req) { const remoteIp = _.isEmpty(req.headers['x-forwarded-for']) ? req.socket.remoteAddress : req.headers['x-forwarded-for']; this.log.debug(`Established a new logcat listener web socket connection from ${remoteIp}`); } else { this.log.debug('Established a new logcat listener web socket connection'); } if (_.isEmpty(this._logcatWebsocketListener)) { this._logcatWebsocketListener = (logRecord: LogEntry) => { if (ws?.readyState === WebSocket.OPEN) { ws.send(logRecord.message); } }; } this.adb.setLogcatListener(this._logcatWebsocketListener); ws.on('close', (code, reason) => { if (!_.isEmpty(this._logcatWebsocketListener)) { try { this.adb.removeLogcatListener(this._logcatWebsocketListener); } catch {} this._logcatWebsocketListener = undefined; } let closeMsg = 'Logcat listener web socket is closed.'; if (!_.isEmpty(code)) { closeMsg += ` Code: ${code}.`; } if (!_.isEmpty(reason)) { closeMsg += ` Reason: ${reason.toString()}.`; } this.log.debug(closeMsg); }); }); await server.addWebSocketHandler(pathname, wss as WSServer); } /** * Stops the previously started logcat broadcasting wesocket server. * This method will return immediately if no server is running. * * @returns Promise that resolves when the logcat broadcasting websocket is stopped. */ export async function mobileStopLogsBroadcast( this: AndroidDriver, ): Promise<void> { const pathname = WEBSOCKET_ENDPOINT(this.sessionId as string); const server = this.server as AppiumServer; if (_.isEmpty(await server.getWebSocketHandlers(pathname))) { return; } this.log.debug( `Stopping logcat broadcasting on web socket server ` + `${JSON.stringify(server.address())} to ${pathname}`, ); await server.removeWebSocketHandler(pathname); } /** * Gets the list of available log types. * * @returns Promise that resolves to an array of log type names. */ export async function getLogTypes( this: AndroidDriver, ): Promise<string[]> { // XXX why doesn't `super` work here? const nativeLogTypes = await BaseDriver.prototype.getLogTypes.call(this); if (this.isWebContext()) { const webLogTypes = await (this.chromedriver as Chromedriver).jwproxy.command('/log/types', 'GET') as string[]; return [...nativeLogTypes, ...webLogTypes]; } return nativeLogTypes; } /** * Assigns a BiDi log listener to an event emitter. * * https://w3c.github.io/webdriver-bidi/#event-log-entryAdded * * @template EE The event emitter type. * @param logEmitter The event emitter to attach the listener to. * @param properties The BiDi listener properties. * @returns A tuple containing the event emitter and the listener function. */ export function assignBiDiLogListener<EE extends EventEmitter>( this: AndroidDriver, logEmitter: EE, properties: BiDiListenerProperties, ): [EE, LogListener] { const { type, context = NATIVE_WIN, srcEventName = 'output', entryTransformer, } = properties; const listener: LogListener = (logEntry: LogEntry) => { const finalEntry = entryTransformer ? entryTransformer(logEntry) : logEntry; this.eventEmitter.emit(BIDI_EVENT_NAME, makeLogEntryAddedEvent(finalEntry, context, type)); }; logEmitter.on(srcEventName, listener); return [logEmitter, listener]; } /** * Gets logs of a specific type. * * @param logType The type of logs to retrieve. * @returns Promise that resolves to the logs for the specified type. */ export async function getLog( this: AndroidDriver, logType: string, ): Promise<any> { if (this.isWebContext() && !_.keys(this.supportedLogTypes).includes(logType)) { return await (this.chromedriver as Chromedriver).jwproxy.command('/log', 'POST', {type: logType}); } return await BaseDriver.prototype.getLog.call(this, logType); } // #region Internal helpers /** * Generates the websocket endpoint path for logcat broadcasting. * * @param sessionId The session ID. * @returns The websocket endpoint path. */ const WEBSOCKET_ENDPOINT = (sessionId: string): string => `${DEFAULT_WS_PATHNAME_PREFIX}/session/${sessionId}/appium/device/logcat`; // #endregion export interface BiDiListenerProperties { type: string; srcEventName?: string; context?: string; entryTransformer?: (x: LogEntry) => LogEntry; } export type LogListener = (logEntry: LogEntry) => any;