diff --git a/lib/logcat.js b/lib/logcat.js index fb7f5b3c..7bc65793 100644 --- a/lib/logcat.js +++ b/lib/logcat.js @@ -1,14 +1,57 @@ import _ from 'lodash'; -import { SubProcess, exec } from 'teen_process'; -import { logger } from 'appium-support'; +import { logger, util } from 'appium-support'; import B from 'bluebird'; import events from 'events'; const { EventEmitter } = events; - +import { SubProcess, exec } from 'teen_process'; const log = logger.getLogger('Logcat'); const MAX_BUFFER_SIZE = 10000; const LOGCAT_PROC_STARTUP_TIMEOUT = 10000; +const SUPPORTED_FORMATS = ['brief', 'process', 'tag', 'thread', 'raw', 'time', 'threadtime', 'long']; +const SUPPORTED_PRIORITIES = ['v', 'd', 'i', 'w', 'e', 'f', 's']; +const DEFAULT_PRIORITY = 'v'; +const DEFAULT_TAG = '*'; +const DEFAULT_FORMAT = 'threadtime'; + +function requireFormat (format) { + if (!SUPPORTED_FORMATS.includes(format)) { + log.info(`The format value '${format}' is unknown. Supported values are: ${SUPPORTED_FORMATS}`); + log.info(`Defaulting to '${DEFAULT_FORMAT}'`); + return DEFAULT_FORMAT; + } + return format; +} + +function requireSpec (spec) { + const [tag, priority] = spec.split(':'); + let resultTag = tag; + if (!resultTag) { + log.info(`The tag value in spec '${spec}' cannot be empty`); + log.info(`Defaulting to '${DEFAULT_TAG}'`); + resultTag = DEFAULT_TAG; + } + if (!priority) { + log.info(`The priority value in spec '${spec}' is empty. Defaulting to Verbose (${DEFAULT_PRIORITY})`); + return `${resultTag}:${DEFAULT_PRIORITY}`; + } + if (!SUPPORTED_PRIORITIES.some((p) => _.toLower(priority) === _.toLower(p))) { + log.info(`The priority value in spec '${spec}' is unknown. Supported values are: ${SUPPORTED_PRIORITIES}`); + log.info(`Defaulting to Verbose (${DEFAULT_PRIORITY})`); + return `${resultTag}:${DEFAULT_PRIORITY}`; + } + return spec; +} + +function formatFilterSpecs (filterSpecs) { + if (!_.isArray(filterSpecs)) { + filterSpecs = [filterSpecs]; + } + return filterSpecs + .filter((spec) => spec && _.isString(spec) && !spec.startsWith('-')) + .map((spec) => spec.includes(':') ? requireSpec(spec) : spec); +} + class Logcat extends EventEmitter { constructor (opts = {}) { @@ -22,7 +65,7 @@ class Logcat extends EventEmitter { this.logIdxSinceLastRequest = 0; } - async startCapture () { + async startCapture (opts = {}) { let started = false; return await new B(async (_resolve, _reject) => { // eslint-disable-line promise/param-names const resolve = function (...args) { @@ -38,8 +81,18 @@ class Logcat extends EventEmitter { await this.clear(); } - log.debug('Starting logcat capture'); - this.proc = new SubProcess(this.adb.path, this.adb.defaultArgs.concat(['logcat', '-v', 'threadtime'])); + const { + format = DEFAULT_FORMAT, + filterSpecs = [], + } = opts; + const cmd = [ + ...this.adb.defaultArgs, + 'logcat', + '-v', requireFormat(format), + ...formatFilterSpecs(filterSpecs), + ]; + log.debug(`Starting logs capture with command: ${util.quote([this.adb.path, ...cmd])}`); + this.proc = new SubProcess(this.adb.path, cmd); this.proc.on('exit', (code, signal) => { log.error(`Logcat terminated with code ${code}, signal ${signal}`); this.proc = null; @@ -122,11 +175,10 @@ class Logcat extends EventEmitter { async clear () { log.debug('Clearing logcat logs from device'); try { - const args = this.adb.defaultArgs.concat(['logcat', '-c']); - log.debug(`Running '${this.adb.path} ${args.join(' ')}'`); + const args = [...this.adb.defaultArgs, 'logcat', '-c']; await exec(this.adb.path, args); } catch (err) { - log.warn(`Failed to clear logcat logs: ${err.message}`); + log.warn(`Failed to clear logcat logs: ${err.stderr || err.message}`); } } } diff --git a/lib/tools/adb-commands.js b/lib/tools/adb-commands.js index 4cc9dc5c..07e77505 100644 --- a/lib/tools/adb-commands.js +++ b/lib/tools/adb-commands.js @@ -979,22 +979,45 @@ methods.restart = async function restart () { } }; +/** + * @typedef {Object} LogcatOpts + * @property {string} format The log print format, where is one of: + * brief process tag thread raw time threadtime long + * `threadtime` is the default value. + * @property {Array} filterSpecs Series of [:priority] + * where is a log component tag (or * for all) and priority is: + * V Verbose + * D Debug + * I Info + * W Warn + * E Error + * F Fatal + * S Silent (supress all output) + * + * '*' means '*:d' and by itself means :v + * + * If not specified on the commandline, filterspec is set from ANDROID_LOG_TAGS. + * If no filterspec is found, filter defaults to '*:I' + */ + /** * Start the logcat process to gather logs. * - * @throws {error} If restart fails. + * @param {?LogcatOpts} opts + * @throws {Error} If restart fails. */ -methods.startLogcat = async function startLogcat () { +methods.startLogcat = async function startLogcat (opts = {}) { if (!_.isEmpty(this.logcat)) { throw new Error("Trying to start logcat capture but it's already started!"); } + this.logcat = new Logcat({ adb: this.executable, debug: false, debugTrace: false, clearDeviceLogsOnStart: !!this.clearDeviceLogsOnStart, }); - await this.logcat.startCapture(); + await this.logcat.startCapture(opts); }; /** diff --git a/lib/tools/system-calls.js b/lib/tools/system-calls.js index 4499d155..2559c7d5 100644 --- a/lib/tools/system-calls.js +++ b/lib/tools/system-calls.js @@ -414,7 +414,7 @@ systemCallMethods.shell = async function shell (cmd, opts = {}) { systemCallMethods.createSubProcess = function createSubProcess (args = []) { // add the default arguments - args = this.executable.defaultArgs.concat(args); + args = [...this.executable.defaultArgs, ...args]; log.debug(`Creating ADB subprocess with args: ${JSON.stringify(args)}`); return new SubProcess(this.getAdbPath(), args); }; diff --git a/test/unit/logcat-specs.js b/test/unit/logcat-specs.js index 9c2defdc..b2884fe5 100644 --- a/test/unit/logcat-specs.js +++ b/test/unit/logcat-specs.js @@ -9,8 +9,8 @@ import { withMocks } from 'appium-test-support'; chai.use(chaiAsPromised); describe('logcat', withMocks({teen_process}, function (mocks) { - let adb = {path: 'dummyPath', defaultArgs: []}; - let logcat = new Logcat({adb, debug: false, debugTrace: false}); + const adb = {path: 'dummyPath', defaultArgs: []}; + const logcat = new Logcat({adb, debug: false, debugTrace: false}); afterEach(function () { mocks.verify(); @@ -21,13 +21,16 @@ describe('logcat', withMocks({teen_process}, function (mocks) { let conn = new events.EventEmitter(); conn.start = () => { }; mocks.teen_process.expects('SubProcess') - .withArgs('dummyPath', ['logcat', '-v', 'threadtime']) + .withArgs('dummyPath', ['logcat', '-v', 'brief', 'yolo2:d', '*:v']) .onFirstCall() .returns(conn); setTimeout(function () { conn.emit('lines-stdout', ['- beginning of system\r']); }, 0); - await logcat.startCapture(); + await logcat.startCapture({ + format: 'brief', + filterSpecs: ['yolo2:d', ':k', '-asd:e'], + }); let logs = logcat.getLogs(); logs.should.have.length.above(0); });