From 97895755c41a3a729a8f4fd972c0f900a41f383a Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Mon, 1 Jul 2024 07:51:17 +0200 Subject: [PATCH 1/2] chore: Rewrite logging-related classes to typescript (#2420) --- lib/commands/context.js | 5 +- lib/commands/log.js | 5 +- lib/device-log/ios-device-log.js | 56 --------- lib/device-log/ios-device-log.ts | 52 ++++++++ lib/device-log/ios-log.js | 116 ------------------ lib/device-log/ios-log.ts | 87 +++++++++++++ lib/device-log/ios-performance-log.js | 68 ---------- lib/device-log/ios-performance-log.ts | 61 +++++++++ ...-simulator-log.js => ios-simulator-log.ts} | 73 ++++++----- lib/device-log/line-consuming-log.ts | 16 +++ 10 files changed, 259 insertions(+), 280 deletions(-) delete mode 100644 lib/device-log/ios-device-log.js create mode 100644 lib/device-log/ios-device-log.ts delete mode 100644 lib/device-log/ios-log.js create mode 100644 lib/device-log/ios-log.ts delete mode 100644 lib/device-log/ios-performance-log.js create mode 100644 lib/device-log/ios-performance-log.ts rename lib/device-log/{ios-simulator-log.js => ios-simulator-log.ts} (60%) create mode 100644 lib/device-log/line-consuming-log.ts diff --git a/lib/commands/context.js b/lib/commands/context.js index 8d64ebf7d..80a02f491 100644 --- a/lib/commands/context.js +++ b/lib/commands/context.js @@ -558,7 +558,10 @@ 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(this.remote); + this.logs.performance = new IOSPerformanceLog({ + remoteDebugger: this.remote, + log: this.log, + }); await this.logs.performance.startCapture(); } diff --git a/lib/commands/log.js b/lib/commands/log.js index d5532a715..c5d4e6947 100644 --- a/lib/commands/log.js +++ b/lib/commands/log.js @@ -103,13 +103,14 @@ export default { this.logs.syslog = new IOSDeviceLog({ udid: this.opts.udid, showLogs: this.opts.showIOSLog, + log: this.log, }); } else { this.logs.syslog = new IOSSimulatorLog({ - sim: this.device, + sim: /** @type {import('appium-ios-simulator').Simulator} */ (this.device), showLogs: this.opts.showIOSLog, - xcodeVersion: this.xcodeVersion, iosSimulatorLogsPredicate: this.opts.iosSimulatorLogsPredicate, + log: this.log, }); } this.logs.safariConsole = new SafariConsoleLog(!!this.opts.showSafariConsoleLog); diff --git a/lib/device-log/ios-device-log.js b/lib/device-log/ios-device-log.js deleted file mode 100644 index 50a29e91b..000000000 --- a/lib/device-log/ios-device-log.js +++ /dev/null @@ -1,56 +0,0 @@ -import {logger} from 'appium/support'; -import {IOSLog} from './ios-log'; -import {services} from 'appium-ios-device'; - -const log = logger.getLogger('IOSDeviceLog'); - -export class IOSDeviceLog extends IOSLog { - constructor(opts) { - super(); - this.udid = opts.udid; - this.showLogs = !!opts.showLogs; - this.service = null; - } - /** - * @override - */ - async startCapture() { - if (this.service) { - return; - } - this.service = await services.startSyslogService(this.udid); - this.service.start(this.onLog.bind(this)); - } - - /** - * @param {string} logLine - */ - onLog(logLine) { - this.broadcast(logLine); - if (this.showLogs) { - log.info(logLine); - } - } - - /** - * @override - */ - get isCapturing() { - return !!this.service; - } - - /** - * @override - */ - // XXX: superclass is async, but this is not - // eslint-disable-next-line require-await - async stopCapture() { - if (!this.service) { - return; - } - this.service.close(); - this.service = null; - } -} - -export default IOSDeviceLog; diff --git a/lib/device-log/ios-device-log.ts b/lib/device-log/ios-device-log.ts new file mode 100644 index 000000000..c60144d6d --- /dev/null +++ b/lib/device-log/ios-device-log.ts @@ -0,0 +1,52 @@ +import {services} from 'appium-ios-device'; +import { LineConsumingLog } from './line-consuming-log'; +import type { AppiumLogger } from '@appium/types'; + +export interface IOSDeviceLogOpts { + udid: string; + showLogs?: boolean; + log?: AppiumLogger; +} + +export class IOSDeviceLog extends LineConsumingLog { + private udid: string; + private showLogs: boolean; + private service: any | null; + + constructor(opts: IOSDeviceLogOpts) { + super({log: opts.log}); + this.udid = opts.udid; + this.showLogs = !!opts.showLogs; + this.service = null; + } + + override async startCapture(): Promise { + if (this.service) { + return; + } + this.service = await services.startSyslogService(this.udid); + this.service.start(this.onLog.bind(this)); + } + + override get isCapturing(): boolean { + return !!this.service; + } + + // eslint-disable-next-line require-await + override async stopCapture(): Promise { + if (!this.service) { + return; + } + this.service.close(); + this.service = null; + } + + private onLog(logLine: string): void { + this.broadcast(logLine); + if (this.showLogs) { + this.log.info(`[IOS_SYSLOG_ROW] ${logLine}`); + } + } +} + +export default IOSDeviceLog; diff --git a/lib/device-log/ios-log.js b/lib/device-log/ios-log.js deleted file mode 100644 index 19e79f760..000000000 --- a/lib/device-log/ios-log.js +++ /dev/null @@ -1,116 +0,0 @@ -import {EventEmitter} from 'events'; -import { LRUCache } from 'lru-cache'; -import { toLogEntry } from './helpers'; - -// We keep only the most recent log entries to avoid out of memory error -const MAX_LOG_ENTRIES_COUNT = 10000; - -// TODO: Rewrite this class to typescript for better generic typing - -export class IOSLog extends EventEmitter { - constructor(maxBufferSize = MAX_LOG_ENTRIES_COUNT) { - super(); - this.maxBufferSize = maxBufferSize; - /** @type {LRUCache} */ - this.logs = new LRUCache({ - max: this.maxBufferSize, - }); - /** @type {number?} */ - this.logIndexSinceLastRequest = null; - } - - /** @returns {Promise} */ - // eslint-disable-next-line require-await - async startCapture() { - throw new Error(`Sub-classes need to implement a 'startCapture' function`); - } - - /** @returns {Promise} */ - // eslint-disable-next-line require-await - async stopCapture() { - throw new Error(`Sub-classes need to implement a 'stopCapture' function`); - } - - /** @returns {boolean} */ - get isCapturing() { - throw new Error(`Sub-classes need to implement a 'isCapturing' function`); - } - - /** - * - * @param {any} entry - * @returns {void} - */ - broadcast(entry) { - let recentIndex = -1; - for (const key of this.logs.rkeys()) { - recentIndex = key; - break; - } - const serializedEntry = this._serializeEntry(entry); - this.logs.set(++recentIndex, serializedEntry); - if (this.listenerCount('output')) { - this.emit('output', this._deserializeEntry(serializedEntry)); - } - } - - /** - * - * @returns {import('../commands/types').LogEntry[]} - */ - getLogs() { - /** @type {import('../commands/types').LogEntry[]} */ - const result = []; - /** @type {number?} */ - let recentLogIndex = null; - for (const [index, value] of this.logs.entries()) { - if (this.logIndexSinceLastRequest && index > this.logIndexSinceLastRequest - || !this.logIndexSinceLastRequest) { - recentLogIndex = index; - result.push(this._deserializeEntry(value)); - } - } - if (recentLogIndex !== null) { - this.logIndexSinceLastRequest = recentLogIndex; - } - return result; - } - - /** - * - * @returns {import('../commands/types').LogEntry[]} - */ - getAllLogs() { - /** @type {import('../commands/types').LogEntry[]} */ - const result = []; - for (const value of this.logs.values()) { - result.push(this._deserializeEntry(value)); - } - return result; - } - - /** - * - * @param {any} value - * @returns {any} - */ - _serializeEntry(value) { - return [value, Date.now()]; - } - - /** - * - * @param {any} value - * @returns {any} - */ - _deserializeEntry(value) { - const [message, timestamp] = value; - return toLogEntry(message, timestamp); - } - - _clearEntries() { - this.logs.clear(); - } -} - -export default IOSLog; diff --git a/lib/device-log/ios-log.ts b/lib/device-log/ios-log.ts new file mode 100644 index 000000000..3709e5fbe --- /dev/null +++ b/lib/device-log/ios-log.ts @@ -0,0 +1,87 @@ +import {EventEmitter} from 'events'; +import { LRUCache } from 'lru-cache'; +import type { LogEntry } from '../commands/types'; +import type { AppiumLogger } from '@appium/types'; +import {logger} from 'appium/support'; + +// We keep only the most recent log entries to avoid out of memory error +const MAX_LOG_ENTRIES_COUNT = 10000; + +export interface IOSLogOptions { + maxBufferSize?: number; + log?: AppiumLogger; +} + +export abstract class IOSLog< + TRawEntry, + TSerializedEntry extends object +> extends EventEmitter { + private maxBufferSize: number; + private logs: LRUCache; + private logIndexSinceLastRequest: number | null; + private _log: AppiumLogger; + + constructor(opts: IOSLogOptions = {}) { + super(); + this.maxBufferSize = opts.maxBufferSize ?? MAX_LOG_ENTRIES_COUNT; + this.logs = new LRUCache({ + max: this.maxBufferSize, + }); + this.logIndexSinceLastRequest = null; + this._log = opts.log ?? logger.getLogger(this.constructor.name); + } + + abstract startCapture(): Promise; + abstract stopCapture(): Promise; + abstract get isCapturing(): boolean; + + get log(): AppiumLogger { + return this._log; + } + + broadcast(entry: TRawEntry): void { + let recentIndex = -1; + for (const key of this.logs.rkeys()) { + recentIndex = key; + break; + } + const serializedEntry = this._serializeEntry(entry); + this.logs.set(++recentIndex, serializedEntry); + if (this.listenerCount('output')) { + this.emit('output', this._deserializeEntry(serializedEntry)); + } + } + + getLogs(): LogEntry[] { + const result: LogEntry[] = []; + let recentLogIndex: number | null = null; + for (const [index, value] of this.logs.entries()) { + if (this.logIndexSinceLastRequest && index > this.logIndexSinceLastRequest + || !this.logIndexSinceLastRequest) { + recentLogIndex = index; + result.push(this._deserializeEntry(value)); + } + } + if (recentLogIndex !== null) { + this.logIndexSinceLastRequest = recentLogIndex; + } + return result; + } + + getAllLogs(): LogEntry[] { + const result: LogEntry[] = []; + for (const value of this.logs.values()) { + result.push(this._deserializeEntry(value)); + } + return result; + } + + protected abstract _serializeEntry(value: TRawEntry): TSerializedEntry; + protected abstract _deserializeEntry(value: TSerializedEntry): LogEntry; + + protected _clearEntries() { + this.logs.clear(); + } +} + +export default IOSLog; diff --git a/lib/device-log/ios-performance-log.js b/lib/device-log/ios-performance-log.js deleted file mode 100644 index 5fb75aa7a..000000000 --- a/lib/device-log/ios-performance-log.js +++ /dev/null @@ -1,68 +0,0 @@ -import {logger} from 'appium/support'; -import _ from 'lodash'; -import { IOSLog } from './ios-log'; - -const log = logger.getLogger('IOSPerformanceLog'); -const MAX_EVENTS = 5000; - -export class IOSPerformanceLog extends IOSLog { - constructor(remoteDebugger, maxEvents = MAX_EVENTS) { - super(maxEvents); - this.remoteDebugger = remoteDebugger; - this.maxEvents = parseInt(String(maxEvents), 10); - this._started = false; - } - - /** - * @override - */ - async startCapture() { - log.debug('Starting performance (Timeline) log capture'); - this._clearEntries(); - const result = await this.remoteDebugger.startTimeline(this.onTimelineEvent.bind(this)); - this._started = true; - return result; - } - - /** - * @override - */ - async stopCapture() { - log.debug('Stopping performance (Timeline) log capture'); - const result = await this.remoteDebugger.stopTimeline(); - this._started = false; - return result; - } - - /** - * @override - */ - get isCapturing() { - return this._started; - } - - /** - * @override - */ - _serializeEntry(value) { - return value; - } - - /** - * @override - */ - _deserializeEntry(value) { - return value; - } - - /** - * - * @param {import('../commands/types').LogEntry} event - */ - onTimelineEvent(event) { - log.debug(`Received Timeline event: ${_.truncate(JSON.stringify(event))}`); - this.broadcast(event); - } -} - -export default IOSPerformanceLog; diff --git a/lib/device-log/ios-performance-log.ts b/lib/device-log/ios-performance-log.ts new file mode 100644 index 000000000..e5fb5e564 --- /dev/null +++ b/lib/device-log/ios-performance-log.ts @@ -0,0 +1,61 @@ +import _ from 'lodash'; +import { IOSLog } from './ios-log'; +import type { LogEntry } from '../commands/types'; +import type { AppiumLogger } from '@appium/types'; + +const MAX_EVENTS = 5000; + +type PerformanceLogEntry = object; +export interface IOSPerformanceLogOptions { + remoteDebugger: any; + maxEvents?: number; + log?: AppiumLogger; +} + +export class IOSPerformanceLog extends IOSLog { + private remoteDebugger: any; + private _started: boolean; + + constructor(opts: IOSPerformanceLogOptions) { + super({ + maxBufferSize: opts.maxEvents ?? MAX_EVENTS, + log: opts.log, + }); + this.remoteDebugger = opts.remoteDebugger; + this._started = false; + } + + override async startCapture(): Promise { + this.log.debug('Starting performance (Timeline) log capture'); + this._clearEntries(); + const result = await this.remoteDebugger.startTimeline(this.onTimelineEvent.bind(this)); + this._started = true; + return result; + } + + override async stopCapture(): Promise { + this.log.debug('Stopping performance (Timeline) log capture'); + const result = await this.remoteDebugger.stopTimeline(); + this._started = false; + return result; + } + + override get isCapturing(): boolean { + return this._started; + } + + protected override _serializeEntry(value: PerformanceLogEntry): PerformanceLogEntry { + return value; + } + + protected override _deserializeEntry(value: PerformanceLogEntry): LogEntry { + return value as LogEntry; + } + + private onTimelineEvent(event: PerformanceLogEntry): void { + this.log.debug(`Received Timeline event: ${_.truncate(JSON.stringify(event))}`); + this.broadcast(event); + } +} + +export default IOSPerformanceLog; diff --git a/lib/device-log/ios-simulator-log.js b/lib/device-log/ios-simulator-log.ts similarity index 60% rename from lib/device-log/ios-simulator-log.js rename to lib/device-log/ios-simulator-log.ts index b708e9135..f39eafb33 100644 --- a/lib/device-log/ios-simulator-log.js +++ b/lib/device-log/ios-simulator-log.ts @@ -1,27 +1,35 @@ import _ from 'lodash'; -import {IOSLog} from './ios-log'; -import {logger} from 'appium/support'; -import {exec} from 'teen_process'; +import {SubProcess, exec} from 'teen_process'; +import { LineConsumingLog } from './line-consuming-log'; +import type { Simulator } from 'appium-ios-simulator'; +import type { AppiumLogger } from '@appium/types'; -const log = logger.getLogger('IOSSimulatorLog'); const EXECVP_ERROR_PATTERN = /execvp\(\)/; const START_TIMEOUT = 10000; -export class IOSSimulatorLog extends IOSLog { - constructor({sim, showLogs, xcodeVersion, iosSimulatorLogsPredicate}) { - super(); - this.sim = sim; - this.showLogs = !!showLogs; - this.xcodeVersion = xcodeVersion; - this.predicate = iosSimulatorLogsPredicate; +export interface IOSSimulatorLogOptions { + sim: Simulator; + showLogs?: boolean; + iosSimulatorLogsPredicate?: string; + log?: AppiumLogger; +} + +export class IOSSimulatorLog extends LineConsumingLog { + private sim: Simulator; + private showLogs: boolean; + private predicate?: string; + private proc: SubProcess | null; + + constructor(opts: IOSSimulatorLogOptions) { + super({log: opts.log}); + this.sim = opts.sim; + this.showLogs = !!opts.showLogs; + this.predicate = opts.iosSimulatorLogsPredicate; this.proc = null; } - /** - * @override - */ - async startCapture() { + override async startCapture(): Promise { if (_.isUndefined(this.sim.udid)) { throw new Error(`Log capture requires a sim udid`); } @@ -33,7 +41,7 @@ export class IOSSimulatorLog extends IOSLog { if (this.predicate) { spawnArgs.push('--predicate', this.predicate); } - log.debug( + this.log.debug( `Starting log capture for iOS Simulator with udid '${this.sim.udid}' ` + `using simctl`, ); try { @@ -48,10 +56,7 @@ export class IOSSimulatorLog extends IOSLog { } } - /** - * @override - */ - async stopCapture() { + override async stopCapture(): Promise { if (!this.proc) { return; } @@ -59,44 +64,38 @@ export class IOSSimulatorLog extends IOSLog { this.proc = null; } - /** - * @override - */ - get isCapturing() { - return this.proc && this.proc.isRunning; + override get isCapturing(): boolean { + return Boolean(this.proc && this.proc.isRunning); } - /** - * @param {string} logRow - * @param {string} [prefix=''] - */ - onOutput(logRow, prefix = '') { + private onOutput(logRow: string, prefix: string = ''): void { this.broadcast(logRow); if (this.showLogs) { const space = prefix.length > 0 ? ' ' : ''; - log.info(`[IOS_SYSLOG_ROW${space}${prefix}] ${logRow}`); + this.log.info(`[IOS_SYSLOG_ROW${space}${prefix}] ${logRow}`); } } - async killLogSubProcess() { - if (!this.proc.isRunning) { + private async killLogSubProcess(): Promise { + if (!this.proc?.isRunning) { return; } - log.debug('Stopping iOS log capture'); + + this.log.debug('Stopping iOS log capture'); try { await this.proc.stop('SIGTERM', 1000); } catch (e) { if (!this.proc.isRunning) { return; } - log.warn('Cannot stop log capture process. Sending SIGKILL'); + this.log.warn('Cannot stop log capture process. Sending SIGKILL'); await this.proc.stop('SIGKILL'); } } - async finishStartingLogCapture() { + private async finishStartingLogCapture() { if (!this.proc) { - log.errorAndThrow('Could not capture simulator log'); + throw this.log.errorWithException('Could not capture simulator log'); } for (const streamName of ['stdout', 'stderr']) { diff --git a/lib/device-log/line-consuming-log.ts b/lib/device-log/line-consuming-log.ts new file mode 100644 index 000000000..9cacf6b7d --- /dev/null +++ b/lib/device-log/line-consuming-log.ts @@ -0,0 +1,16 @@ +import {IOSLog} from './ios-log'; +import { toLogEntry } from './helpers'; +import type { LogEntry } from '../commands/types'; + +type TSerializedEntry = [string, number]; + +export abstract class LineConsumingLog extends IOSLog { + protected override _serializeEntry(value: string): TSerializedEntry { + return [value, Date.now()]; + } + + protected override _deserializeEntry(value: TSerializedEntry): LogEntry { + const [message, timestamp] = value; + return toLogEntry(message, timestamp); + } +} From 0f45dd568ddb55917a94abf647bb0fd27e5761b8 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 1 Jul 2024 05:52:58 +0000 Subject: [PATCH 2/2] chore(release): 7.21.2 [skip ci] ## [7.21.2](https://github.com/appium/appium-xcuitest-driver/compare/v7.21.1...v7.21.2) (2024-07-01) ### Miscellaneous Chores * Rewrite logging-related classes to typescript ([#2420](https://github.com/appium/appium-xcuitest-driver/issues/2420)) ([9789575](https://github.com/appium/appium-xcuitest-driver/commit/97895755c41a3a729a8f4fd972c0f900a41f383a)) --- CHANGELOG.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0de1513e9..f017163e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [7.21.2](https://github.com/appium/appium-xcuitest-driver/compare/v7.21.1...v7.21.2) (2024-07-01) + +### Miscellaneous Chores + +* Rewrite logging-related classes to typescript ([#2420](https://github.com/appium/appium-xcuitest-driver/issues/2420)) ([9789575](https://github.com/appium/appium-xcuitest-driver/commit/97895755c41a3a729a8f4fd972c0f900a41f383a)) + ## [7.21.1](https://github.com/appium/appium-xcuitest-driver/compare/v7.21.0...v7.21.1) (2024-06-30) ### Miscellaneous Chores diff --git a/package.json b/package.json index 9188dbf11..18c3be373 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "xcuitest", "xctest" ], - "version": "7.21.1", + "version": "7.21.2", "author": "Appium Contributors", "license": "Apache-2.0", "repository": {