diff --git a/b2g/chrome/content/settings.js b/b2g/chrome/content/settings.js index d6cb4df10ae..11a37c4c36f 100644 --- a/b2g/chrome/content/settings.js +++ b/b2g/chrome/content/settings.js @@ -194,6 +194,24 @@ SettingsListener.observe('devtools.overlay', false, (value) => { } }); +#ifdef MOZ_WIDGET_GONK +let LogShake; +SettingsListener.observe('devtools.logshake', false, (value) => { + if (value) { + if (!LogShake) { + let scope = {}; + Cu.import('resource://gre/modules/LogShake.jsm', scope); + LogShake = scope.LogShake; + } + LogShake.init(); + } else { + if (LogShake) { + LogShake.uninit(); + } + } +}); +#endif + // =================== Device Storage ==================== SettingsListener.observe('device.storage.writable.name', 'sdcard', function(value) { if (Services.prefs.getPrefType('device.storage.writable.name') != Ci.nsIPrefBranch.PREF_STRING) { diff --git a/b2g/components/LogCapture.jsm b/b2g/components/LogCapture.jsm new file mode 100644 index 00000000000..adff54c6790 --- /dev/null +++ b/b2g/components/LogCapture.jsm @@ -0,0 +1,91 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* jshint moz: true */ +/* global Uint8Array, Components, dump */ + +'use strict'; + +this.EXPORTED_SYMBOLS = ['LogCapture']; + +/** + * readLogFile + * Read in /dev/log/{{log}} in nonblocking mode, which will return -1 if + * reading would block the thread. + * + * @param log {String} The log from which to read. Must be present in /dev/log + * @return {Uint8Array} Raw log data + */ +let readLogFile = function(logLocation) { + if (!this.ctypes) { + // load in everything on first use + Components.utils.import('resource://gre/modules/ctypes.jsm', this); + + this.lib = this.ctypes.open(this.ctypes.libraryName('c')); + + this.read = this.lib.declare('read', + this.ctypes.default_abi, + this.ctypes.int, // bytes read (out) + this.ctypes.int, // file descriptor (in) + this.ctypes.voidptr_t, // buffer to read into (in) + this.ctypes.size_t // size_t size of buffer (in) + ); + + this.open = this.lib.declare('open', + this.ctypes.default_abi, + this.ctypes.int, // file descriptor (returned) + this.ctypes.char.ptr, // path + this.ctypes.int // flags + ); + + this.close = this.lib.declare('close', + this.ctypes.default_abi, + this.ctypes.int, // error code (returned) + this.ctypes.int // file descriptor + ); + } + + const O_READONLY = 0; + const O_NONBLOCK = 1 << 11; + + const BUF_SIZE = 2048; + + let BufType = this.ctypes.ArrayType(this.ctypes.char); + let buf = new BufType(BUF_SIZE); + let logArray = []; + + let logFd = this.open(logLocation, O_READONLY | O_NONBLOCK); + if (logFd === -1) { + return null; + } + + let readStart = Date.now(); + let readCount = 0; + while (true) { + let count = this.read(logFd, buf, BUF_SIZE); + readCount += 1; + + if (count <= 0) { + // log has return due to being nonblocking or running out of things + break; + } + for(let i = 0; i < count; i++) { + logArray.push(buf[i]); + } + } + + let logTypedArray = new Uint8Array(logArray); + + this.close(logFd); + + return logTypedArray; +}; + +let cleanup = function() { + this.lib.close(); + this.read = this.open = this.close = null; + this.lib = null; + this.ctypes = null; +}; + +this.LogCapture = { readLogFile: readLogFile, cleanup: cleanup }; diff --git a/b2g/components/LogParser.jsm b/b2g/components/LogParser.jsm new file mode 100644 index 00000000000..b3ab436184d --- /dev/null +++ b/b2g/components/LogParser.jsm @@ -0,0 +1,301 @@ +/* jshint esnext: true */ +/* global DataView */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["LogParser"]; + +/** + * Parse an array read from a /dev/log/ file. Format taken from + * kernel/drivers/staging/android/logger.h and system/core/logcat/logcat.cpp + * + * @param array {Uint8Array} Array read from /dev/log/ file + * @return {Array} List of log messages + */ +function parseLogArray(array) { + let data = new DataView(array.buffer); + let byteString = String.fromCharCode.apply(null, array); + + let logMessages = []; + let pos = 0; + + while (pos < byteString.length) { + // Parse a single log entry + + // Track current offset from global position + let offset = 0; + + // Length of the entry, discarded + let length = data.getUint32(pos + offset, true); + offset += 4; + // Id of the process which generated the message + let processId = data.getUint32(pos + offset, true); + offset += 4; + // Id of the thread which generated the message + let threadId = data.getUint32(pos + offset, true); + offset += 4; + // Seconds since epoch when this message was logged + let seconds = data.getUint32(pos + offset, true); + offset += 4; + // Nanoseconds since the last second + let nanoseconds = data.getUint32(pos + offset, true); + offset += 4; + + // Priority in terms of the ANDROID_LOG_* constants (see below) + // This is where the length field begins counting + let priority = data.getUint8(pos + offset); + + // Reset pos and offset to count from here + pos += offset; + offset = 0; + offset += 1; + + // Read the tag and message, represented as null-terminated c-style strings + let tag = ""; + while (byteString[pos + offset] != "\0") { + tag += byteString[pos + offset]; + offset ++; + } + offset ++; + + let message = ""; + // The kernel log driver may have cut off the null byte (logprint.c) + while (byteString[pos + offset] != "\0" && offset < length) { + message += byteString[pos + offset]; + offset ++; + } + + // Un-skip the missing null terminator + if (offset === length) { + offset --; + } + + offset ++; + + pos += offset; + + // Log messages are occasionally delimited by newlines, but are also + // sometimes followed by newlines as well + if (message.charAt(message.length - 1) === "\n") { + message = message.substring(0, message.length - 1); + } + + // Add an aditional time property to mimic the milliseconds since UTC + // expected by Date + let time = seconds * 1000.0 + nanoseconds/1000000.0; + + // Log messages with interleaved newlines are considered to be separate log + // messages by logcat + for (let lineMessage of message.split("\n")) { + logMessages.push({ + processId: processId, + threadId: threadId, + seconds: seconds, + nanoseconds: nanoseconds, + time: time, + priority: priority, + tag: tag, + message: lineMessage + "\n" + }); + } + } + + return logMessages; +} + +/** + * Get a thread-time style formatted string from time + * @param time {Number} Milliseconds since epoch + * @return {String} Formatted time string + */ +function getTimeString(time) { + let date = new Date(time); + function pad(number) { + if ( number < 10 ) { + return "0" + number; + } + return number; + } + return pad( date.getMonth() + 1 ) + + "-" + pad( date.getDate() ) + + " " + pad( date.getHours() ) + + ":" + pad( date.getMinutes() ) + + ":" + pad( date.getSeconds() ) + + "." + (date.getMilliseconds() / 1000).toFixed(3).slice(2, 5); +} + +/** + * Pad a string using spaces on the left + * @param str {String} String to pad + * @param width {Number} Desired string length + */ +function padLeft(str, width) { + while (str.length < width) { + str = " " + str; + } + return str; +} + +/** + * Pad a string using spaces on the right + * @param str {String} String to pad + * @param width {Number} Desired string length + */ +function padRight(str, width) { + while (str.length < width) { + str = str + " "; + } + return str; +} + +/** Constant values taken from system/core/liblog */ +const ANDROID_LOG_UNKNOWN = 0; +const ANDROID_LOG_DEFAULT = 1; +const ANDROID_LOG_VERBOSE = 2; +const ANDROID_LOG_DEBUG = 3; +const ANDROID_LOG_INFO = 4; +const ANDROID_LOG_WARN = 5; +const ANDROID_LOG_ERROR = 6; +const ANDROID_LOG_FATAL = 7; +const ANDROID_LOG_SILENT = 8; + +/** + * Map a priority number to its abbreviated string equivalent + * @param priorityNumber {Number} Log-provided priority number + * @return {String} Priority number's abbreviation + */ +function getPriorityString(priorityNumber) { + switch (priorityNumber) { + case ANDROID_LOG_VERBOSE: + return "V"; + case ANDROID_LOG_DEBUG: + return "D"; + case ANDROID_LOG_INFO: + return "I"; + case ANDROID_LOG_WARN: + return "W"; + case ANDROID_LOG_ERROR: + return "E"; + case ANDROID_LOG_FATAL: + return "F"; + case ANDROID_LOG_SILENT: + return "S"; + default: + return "?"; + } +} + + +/** + * Mimic the logcat "threadtime" format, generating a formatted string from a + * log message object. + * @param logMessage {Object} A log message from the list returned by parseLogArray + * @return {String} threadtime formatted summary of the message + */ +function formatLogMessage(logMessage) { + // MM-DD HH:MM:SS.ms pid tid priority tag: message + // from system/core/liblog/logprint.c: + return getTimeString(logMessage.time) + + " " + padLeft(""+logMessage.processId, 5) + + " " + padLeft(""+logMessage.threadId, 5) + + " " + getPriorityString(logMessage.priority) + + " " + padRight(logMessage.tag, 8) + + ": " + logMessage.message; +} + +/** + * Pretty-print an array of bytes read from a log file by parsing then + * threadtime formatting its entries. + * @param array {Uint8Array} Array of a log file's bytes + * @return {String} Pretty-printed log + */ +function prettyPrintLogArray(array) { + let logMessages = parseLogArray(array); + return logMessages.map(formatLogMessage).join(""); +} + +/** + * Parse an array of bytes as a properties file. The structure of the + * properties file is derived from bionic/libc/bionic/system_properties.c + * @param array {Uint8Array} Array containing property data + * @return {Object} Map from property name to property value, both strings + */ +function parsePropertiesArray(array) { + let data = new DataView(array.buffer); + let byteString = String.fromCharCode.apply(null, array); + + let properties = {}; + + let propIndex = 0; + let propCount = data.getUint32(0, true); + + // first TOC entry is at 32 + let tocOffset = 32; + + const PROP_NAME_MAX = 32; + const PROP_VALUE_MAX = 92; + + while (propIndex < propCount) { + // Retrieve offset from file start + let infoOffset = data.getUint32(tocOffset, true) & 0xffffff; + + // Now read the name, integer serial, and value + let propName = ""; + let nameOffset = infoOffset; + while (byteString[nameOffset] != "\0" && + (nameOffset - infoOffset) < PROP_NAME_MAX) { + propName += byteString[nameOffset]; + nameOffset ++; + } + + infoOffset += PROP_NAME_MAX; + // Skip serial number + infoOffset += 4; + + let propValue = ""; + nameOffset = infoOffset; + while (byteString[nameOffset] != "\0" && + (nameOffset - infoOffset) < PROP_VALUE_MAX) { + propValue += byteString[nameOffset]; + nameOffset ++; + } + + // Move to next table of contents entry + tocOffset += 4; + + properties[propName] = propValue; + propIndex += 1; + } + + return properties; +} + +/** + * Pretty-print an array read from the /dev/__properties__ file. + * @param array {Uint8Array} File data array + * @return {String} Human-readable string of property name: property value + */ +function prettyPrintPropertiesArray(array) { + let properties = parsePropertiesArray(array); + let propertiesString = ""; + for(let propName in properties) { + propertiesString += propName + ": " + properties[propName] + "\n"; + } + return propertiesString; +} + +/** + * Pretty-print a normal array. Does nothing. + * @param array {Uint8Array} Input array + */ +function prettyPrintArray(array) { + return array; +} + +this.LogParser = { + parseLogArray: parseLogArray, + parsePropertiesArray: parsePropertiesArray, + prettyPrintArray: prettyPrintArray, + prettyPrintLogArray: prettyPrintLogArray, + prettyPrintPropertiesArray: prettyPrintPropertiesArray +}; diff --git a/b2g/components/LogShake.jsm b/b2g/components/LogShake.jsm new file mode 100644 index 00000000000..a1acffa50fd --- /dev/null +++ b/b2g/components/LogShake.jsm @@ -0,0 +1,301 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * LogShake is a module which listens for log requests sent by Gaia. In + * response to a sufficiently large acceleration (a shake), it will save log + * files to an arbitrary directory which it will then return on a + * 'capture-logs-success' event with detail.logFilenames representing each log + * file's filename in the directory. If an error occurs it will instead produce + * a 'capture-logs-error' event. + */ + +/* enable Mozilla javascript extensions and global strictness declaration, + * disable valid this checking */ +/* jshint moz: true */ +/* jshint -W097 */ +/* jshint -W040 */ +/* global Services, Components, dump, LogCapture, LogParser, + OS, Promise, volumeService, XPCOMUtils, SystemAppProxy */ + +'use strict'; + +const Cu = Components.utils; +const Ci = Components.interfaces; + +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); + +XPCOMUtils.defineLazyModuleGetter(this, 'LogCapture', 'resource://gre/modules/LogCapture.jsm'); +XPCOMUtils.defineLazyModuleGetter(this, 'LogParser', 'resource://gre/modules/LogParser.jsm'); +XPCOMUtils.defineLazyModuleGetter(this, 'OS', 'resource://gre/modules/osfile.jsm'); +XPCOMUtils.defineLazyModuleGetter(this, 'Promise', 'resource://gre/modules/Promise.jsm'); +XPCOMUtils.defineLazyModuleGetter(this, 'Services', 'resource://gre/modules/Services.jsm'); +XPCOMUtils.defineLazyModuleGetter(this, 'SystemAppProxy', 'resource://gre/modules/SystemAppProxy.jsm'); + +XPCOMUtils.defineLazyServiceGetter(this, 'powerManagerService', + '@mozilla.org/power/powermanagerservice;1', + 'nsIPowerManagerService'); + +XPCOMUtils.defineLazyServiceGetter(this, 'volumeService', + '@mozilla.org/telephony/volume-service;1', + 'nsIVolumeService'); + +this.EXPORTED_SYMBOLS = ['LogShake']; + +function debug(msg) { + dump('LogShake.jsm: '+msg+'\n'); +} + +/** + * An empirically determined amount of acceleration corresponding to a + * shake + */ +const EXCITEMENT_THRESHOLD = 500; +const DEVICE_MOTION_EVENT = 'devicemotion'; +const SCREEN_CHANGE_EVENT = 'screenchange'; +const CAPTURE_LOGS_ERROR_EVENT = 'capture-logs-error'; +const CAPTURE_LOGS_SUCCESS_EVENT = 'capture-logs-success'; + +// Map of files which have log-type information to their parsers +const LOGS_WITH_PARSERS = { + '/dev/__properties__': LogParser.prettyPrintPropertiesArray, + '/dev/log/main': LogParser.prettyPrintLogArray, + '/dev/log/system': LogParser.prettyPrintLogArray, + '/dev/log/radio': LogParser.prettyPrintLogArray, + '/dev/log/events': LogParser.prettyPrintLogArray, + '/proc/cmdline': LogParser.prettyPrintArray, + '/proc/kmsg': LogParser.prettyPrintArray, + '/proc/meminfo': LogParser.prettyPrintArray, + '/proc/uptime': LogParser.prettyPrintArray, + '/proc/version': LogParser.prettyPrintArray, + '/proc/vmallocinfo': LogParser.prettyPrintArray, + '/proc/vmstat': LogParser.prettyPrintArray +}; + +let LogShake = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), + /** + * If LogShake is listening for device motion events. Required due to lag + * between HAL layer of device motion events and listening for device motion + * events. + */ + deviceMotionEnabled: false, + + /** + * If a capture has been requested and is waiting for reads/parsing. Used for + * debouncing. + */ + captureRequested: false, + + /** + * Start existing, observing motion events if the screen is turned on. + */ + init: function() { + // TODO: no way of querying screen state from power manager + // this.handleScreenChangeEvent({ detail: { + // screenEnabled: powerManagerService.screenEnabled + // }}); + + // However, the screen is always on when we are being enabled because it is + // either due to the phone starting up or a user enabling us directly. + this.handleScreenChangeEvent({ detail: { + screenEnabled: true + }}); + + SystemAppProxy.addEventListener(SCREEN_CHANGE_EVENT, this, false); + + Services.obs.addObserver(this, 'xpcom-shutdown', false); + }, + + /** + * Handle an arbitrary event, passing it along to the proper function + */ + handleEvent: function(event) { + switch (event.type) { + case DEVICE_MOTION_EVENT: + if (!this.deviceMotionEnabled) { + return; + } + this.handleDeviceMotionEvent(event); + break; + + case SCREEN_CHANGE_EVENT: + this.handleScreenChangeEvent(event); + break; + } + }, + + /** + * Handle an observation from Services.obs + */ + observe: function(subject, topic) { + if (topic === 'xpcom-shutdown') { + this.uninit(); + } + }, + + startDeviceMotionListener: function() { + if (!this.deviceMotionEnabled) { + SystemAppProxy.addEventListener(DEVICE_MOTION_EVENT, this, false); + this.deviceMotionEnabled = true; + } + }, + + stopDeviceMotionListener: function() { + SystemAppProxy.removeEventListener(DEVICE_MOTION_EVENT, this, false); + this.deviceMotionEnabled = false; + }, + + /** + * Handle a motion event, keeping track of 'excitement', the magnitude + * of the device's acceleration. + */ + handleDeviceMotionEvent: function(event) { + // There is a lag between disabling the event listener and event arrival + // ceasing. + if (!this.deviceMotionEnabled) { + return; + } + + var acc = event.accelerationIncludingGravity; + + var excitement = acc.x * acc.x + acc.y * acc.y + acc.z * acc.z; + + if (excitement > EXCITEMENT_THRESHOLD) { + if (!this.captureRequested) { + this.captureRequested = true; + captureLogs().then(logResults => { + // On resolution send the success event to the requester + SystemAppProxy._sendCustomEvent(CAPTURE_LOGS_SUCCESS_EVENT, { + logFilenames: logResults.logFilenames, + logPrefix: logResults.logPrefix + }); + this.captureRequested = false; + }, + error => { + // On an error send the error event + SystemAppProxy._sendCustomEvent(CAPTURE_LOGS_ERROR_EVENT, {error: error}); + this.captureRequested = false; + }); + } + } + }, + + handleScreenChangeEvent: function(event) { + if (event.detail.screenEnabled) { + this.startDeviceMotionListener(); + } else { + this.stopDeviceMotionListener(); + } + }, + + /** + * Stop logshake, removing all listeners + */ + uninit: function() { + this.stopDeviceMotionListener(); + SystemAppProxy.removeEventListener(SCREEN_CHANGE_EVENT, this, false); + Services.obs.removeObserver(this, 'xpcom-shutdown'); + } +}; + +function getLogFilename(logLocation) { + // sanitize the log location + let logName = logLocation.replace(/\//g, '-'); + if (logName[0] === '-') { + logName = logName.substring(1); + } + return logName + '.log'; +} + +function getSdcardPrefix() { + return volumeService.getVolumeByName('sdcard').mountPoint; +} + +function getLogDirectory() { + let d = new Date(); + d = new Date(d.getTime() - d.getTimezoneOffset() * 60000); + let timestamp = d.toISOString().slice(0, -5).replace(/[:T]/g, '-'); + // return directory name of format 'logs/timestamp/' + return OS.Path.join('logs', timestamp, ''); +} + +/** + * Captures and saves the current device logs, returning a promise that will + * resolve to an array of log filenames. + */ +function captureLogs() { + let logArrays = readLogs(); + return saveLogs(logArrays); +} + +/** + * Read in all log files, returning their formatted contents + */ +function readLogs() { + let logArrays = {}; + for (let loc in LOGS_WITH_PARSERS) { + let logArray = LogCapture.readLogFile(loc); + if (!logArray) { + continue; + } + let prettyLogArray = LOGS_WITH_PARSERS[loc](logArray); + + logArrays[loc] = prettyLogArray; + } + return logArrays; +} + +/** + * Save the formatted arrays of log files to an sdcard if available + */ +function saveLogs(logArrays) { + if (!logArrays || Object.keys(logArrays).length === 0) { + return Promise.resolve({ + logFilenames: [], + logPrefix: '' + }); + } + + let sdcardPrefix, dirName; + try { + sdcardPrefix = getSdcardPrefix(); + dirName = getLogDirectory(); + } catch(e) { + // Return promise failed with exception e + // Handles missing sdcard + return Promise.reject(e); + } + + debug('making a directory all the way from '+sdcardPrefix+' to '+(sdcardPrefix + '/' + dirName)); + return OS.File.makeDir(OS.Path.join(sdcardPrefix, dirName), {from: sdcardPrefix}) + .then(function() { + // Now the directory is guaranteed to exist, save the logs + let logFilenames = []; + let saveRequests = []; + + for (let logLocation in logArrays) { + debug('requesting save of ' + logLocation); + let logArray = logArrays[logLocation]; + // The filename represents the relative path within the SD card, not the + // absolute path because Gaia will refer to it using the DeviceStorage + // API + let filename = dirName + getLogFilename(logLocation); + logFilenames.push(filename); + let saveRequest = OS.File.writeAtomic(OS.Path.join(sdcardPrefix, filename), logArray); + saveRequests.push(saveRequest); + } + + return Promise.all(saveRequests).then(function() { + debug('returning logfilenames: '+logFilenames.toSource()); + return { + logFilenames: logFilenames, + logPrefix: dirName + }; + }); + }); +} + +LogShake.init(); +this.LogShake = LogShake; diff --git a/b2g/components/moz.build b/b2g/components/moz.build index 856d9d898c4..841e450dee3 100644 --- a/b2g/components/moz.build +++ b/b2g/components/moz.build @@ -52,6 +52,9 @@ EXTRA_JS_MODULES += [ 'ContentRequestHelper.jsm', 'ErrorPage.jsm', 'FxAccountsMgmtService.jsm', + 'LogCapture.jsm', + 'LogParser.jsm', + 'LogShake.jsm', 'SignInToWebsite.jsm', 'SystemAppProxy.jsm', 'TelURIParser.jsm', diff --git a/b2g/components/test/unit/data/test_logger_file b/b2g/components/test/unit/data/test_logger_file new file mode 100644 index 00000000000..b1ed7f10ae8 Binary files /dev/null and b/b2g/components/test/unit/data/test_logger_file differ diff --git a/b2g/components/test/unit/data/test_properties b/b2g/components/test/unit/data/test_properties new file mode 100644 index 00000000000..a8254853738 Binary files /dev/null and b/b2g/components/test/unit/data/test_properties differ diff --git a/b2g/components/test/unit/test_logcapture.js b/b2g/components/test/unit/test_logcapture.js new file mode 100644 index 00000000000..b1102a8c8f9 --- /dev/null +++ b/b2g/components/test/unit/test_logcapture.js @@ -0,0 +1,25 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + + +/** + * Test that LogCapture successfully reads from the /dev/log devices, returning + * a Uint8Array of some length, including zero. This tests a few standard + * log devices + */ +function run_test() { + Components.utils.import('resource:///modules/LogCapture.jsm'); + + function verifyLog(log) { + // log exists + notEqual(log, null); + // log has a length and it is non-negative (is probably array-like) + ok(log.length >= 0); + } + + let mainLog = LogCapture.readLogFile('/dev/log/main'); + verifyLog(mainLog); + + let meminfoLog = LogCapture.readLogFile('/proc/meminfo'); + verifyLog(meminfoLog); +} diff --git a/b2g/components/test/unit/test_logparser.js b/b2g/components/test/unit/test_logparser.js new file mode 100644 index 00000000000..04c39bb9983 --- /dev/null +++ b/b2g/components/test/unit/test_logparser.js @@ -0,0 +1,49 @@ +/* jshint moz: true */ + +const {utils: Cu, classes: Cc, interfaces: Ci} = Components; + +function run_test() { + Cu.import('resource:///modules/LogParser.jsm'); + + let propertiesFile = do_get_file('data/test_properties'); + let loggerFile = do_get_file('data/test_logger_file'); + + let propertiesStream = makeStream(propertiesFile); + let loggerStream = makeStream(loggerFile); + + // Initialize arrays to hold the file contents (lengths are hardcoded) + let propertiesArray = new Uint8Array(propertiesStream.readByteArray(65536)); + let loggerArray = new Uint8Array(loggerStream.readByteArray(4037)); + + propertiesStream.close(); + loggerStream.close(); + + let properties = LogParser.parsePropertiesArray(propertiesArray); + let logMessages = LogParser.parseLogArray(loggerArray); + + // Test arbitrary property entries for correctness + equal(properties['ro.boot.console'], 'ttyHSL0'); + equal(properties['net.tcp.buffersize.lte'], + '524288,1048576,2097152,262144,524288,1048576'); + + ok(logMessages.length === 58, 'There should be 58 messages in the log'); + + let expectedLogEntry = { + processId: 271, threadId: 271, + seconds: 790796, nanoseconds: 620000001, time: 790796620.000001, + priority: 4, tag: 'Vold', + message: 'Vold 2.1 (the revenge) firing up\n' + }; + + deepEqual(expectedLogEntry, logMessages[0]); +} + +function makeStream(file) { + var fileStream = Cc['@mozilla.org/network/file-input-stream;1'] + .createInstance(Ci.nsIFileInputStream); + fileStream.init(file, -1, -1, 0); + var bis = Cc['@mozilla.org/binaryinputstream;1'] + .createInstance(Ci.nsIBinaryInputStream); + bis.setInputStream(fileStream); + return bis; +} diff --git a/b2g/components/test/unit/test_logshake.js b/b2g/components/test/unit/test_logshake.js new file mode 100644 index 00000000000..51727d98d0c --- /dev/null +++ b/b2g/components/test/unit/test_logshake.js @@ -0,0 +1,141 @@ +/** + * Test the log capturing capabilities of LogShake.jsm + */ + +/* jshint moz: true */ +/* global Components, LogCapture, LogShake, ok, add_test, run_next_test, dump */ +/* exported run_test */ + +/* disable use strict warning */ +/* jshint -W097 */ +'use strict'; + +const Cu = Components.utils; + +Cu.import('resource://gre/modules/LogCapture.jsm'); +Cu.import('resource://gre/modules/LogShake.jsm'); + +// Force logshake to handle a device motion event with given components +// Does not use SystemAppProxy because event needs special +// accelerationIncludingGravity property +function sendDeviceMotionEvent(x, y, z) { + let event = { + type: 'devicemotion', + accelerationIncludingGravity: { + x: x, + y: y, + z: z + } + }; + LogShake.handleEvent(event); +} + +// Send a screen change event directly, does not use SystemAppProxy due to race +// conditions. +function sendScreenChangeEvent(screenEnabled) { + let event = { + type: 'screenchange', + detail: { + screenEnabled: screenEnabled + } + }; + LogShake.handleEvent(event); +} + +function debug(msg) { + var timestamp = Date.now(); + dump('LogShake: ' + timestamp + ': ' + msg); +} + +add_test(function test_do_log_capture_after_shaking() { + // Enable LogShake + LogShake.init(); + + let readLocations = []; + LogCapture.readLogFile = function(loc) { + readLocations.push(loc); + return null; // we don't want to provide invalid data to a parser + }; + + // Fire a devicemotion event that is of shake magnitude + sendDeviceMotionEvent(9001, 9001, 9001); + + ok(readLocations.length > 0, + 'LogShake should attempt to read at least one log'); + + LogShake.uninit(); + run_next_test(); +}); + +add_test(function test_do_nothing_when_resting() { + // Enable LogShake + LogShake.init(); + + let readLocations = []; + LogCapture.readLogFile = function(loc) { + readLocations.push(loc); + return null; // we don't want to provide invalid data to a parser + }; + + // Fire a devicemotion event that is relatively tiny + sendDeviceMotionEvent(0, 9.8, 9.8); + + ok(readLocations.length === 0, + 'LogShake should not read any logs'); + + debug('test_do_nothing_when_resting: stop'); + LogShake.uninit(); + run_next_test(); +}); + +add_test(function test_do_nothing_when_disabled() { + debug('test_do_nothing_when_disabled: start'); + // Disable LogShake + LogShake.uninit(); + + let readLocations = []; + LogCapture.readLogFile = function(loc) { + readLocations.push(loc); + return null; // we don't want to provide invalid data to a parser + }; + + // Fire a devicemotion event that would normally be a shake + sendDeviceMotionEvent(0, 9001, 9001); + + ok(readLocations.length === 0, + 'LogShake should not read any logs'); + + run_next_test(); +}); + +add_test(function test_do_nothing_when_screen_off() { + // Enable LogShake + LogShake.init(); + + + // Send an event as if the screen has been turned off + sendScreenChangeEvent(false); + + let readLocations = []; + LogCapture.readLogFile = function(loc) { + readLocations.push(loc); + return null; // we don't want to provide invalid data to a parser + }; + + // Fire a devicemotion event that would normally be a shake + sendDeviceMotionEvent(0, 9001, 9001); + + ok(readLocations.length === 0, + 'LogShake should not read any logs'); + + // Restore the screen + sendScreenChangeEvent(true); + + LogShake.uninit(); + run_next_test(); +}); + +function run_test() { + debug('Starting'); + run_next_test(); +} diff --git a/b2g/components/test/unit/xpcshell.ini b/b2g/components/test/unit/xpcshell.ini index bf7a26e4961..84626cef801 100644 --- a/b2g/components/test/unit/xpcshell.ini +++ b/b2g/components/test/unit/xpcshell.ini @@ -2,6 +2,10 @@ head = tail = +support-files = + data/test_logger_file + data/test_properties + [test_bug793310.js] [test_bug832946.js] @@ -11,4 +15,10 @@ tail = head = head_identity.js tail = +[test_logcapture.js] +# only run on b2g builds due to requiring b2g-specific log files to exist +skip-if = toolkit != "gonk" +[test_logparser.js] + +[test_logshake.js]