Bug 1019816 - Developer option to store logcat to sdcard by shaking the phone. r=gerard-majax
author<jhobin@mozilla.com>
Fri, 22 Aug 2014 10:32:00 +0200
changeset 203348 780b3083916c30200707779ea6996c05f94166c9
parent 203347 ddd9f4ddaf4a48be1bd3bb38951756d70b3e0aa7
child 203349 59611423c368154340dc87981f4e9c92a9a2bb73
push id48665
push userryanvm@gmail.com
push dateWed, 03 Sep 2014 20:40:15 +0000
treeherdermozilla-inbound@0da762e6868a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgerard-majax
bugs1019816
milestone35.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1019816 - Developer option to store logcat to sdcard by shaking the phone. r=gerard-majax
b2g/chrome/content/settings.js
b2g/components/LogCapture.jsm
b2g/components/LogParser.jsm
b2g/components/LogShake.jsm
b2g/components/moz.build
b2g/components/test/unit/data/test_logger_file
b2g/components/test/unit/data/test_properties
b2g/components/test/unit/test_logcapture.js
b2g/components/test/unit/test_logparser.js
b2g/components/test/unit/test_logshake.js
b2g/components/test/unit/xpcshell.ini
--- a/b2g/chrome/content/settings.js
+++ b/b2g/chrome/content/settings.js
@@ -189,16 +189,34 @@ SettingsListener.observe('devtools.overl
     developerHUD.init();
   } else {
     if (developerHUD) {
       developerHUD.uninit();
     }
   }
 });
 
+#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) {
     // We clear the pref because it used to be erroneously written as a bool
     // and we need to clear it before we can change it to have the correct type.
     Services.prefs.clearUserPref('device.storage.writable.name');
   }
   Services.prefs.setCharPref('device.storage.writable.name', value);
new file mode 100644
--- /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 };
new file mode 100644
--- /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
+};
new file mode 100644
--- /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;
--- a/b2g/components/moz.build
+++ b/b2g/components/moz.build
@@ -47,16 +47,19 @@ if CONFIG['MOZ_UPDATER']:
     ]
 
 EXTRA_JS_MODULES += [
     'AlertsHelper.jsm',
     'AppFrames.jsm',
     'ContentRequestHelper.jsm',
     'ErrorPage.jsm',
     'FxAccountsMgmtService.jsm',
+    'LogCapture.jsm',
+    'LogParser.jsm',
+    'LogShake.jsm',
     'SignInToWebsite.jsm',
     'SystemAppProxy.jsm',
     'TelURIParser.jsm',
     'WebappsUpdater.jsm',
 ]
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] != 'gonk':
     EXTRA_JS_MODULES += [
new file mode 100644
index 0000000000000000000000000000000000000000..b1ed7f10ae86e16e9e6f1b14006182ceb278f5e3
GIT binary patch
literal 4037
zc%1E*zi!h|6o+p_(;=#aSn0rNg%k(kI!QqlAPNzsfKV$Sf`tLy`ko|Koa@McB47(M
z8xN3~M_}kwsiHz+gEwGd?gQW($Hhr)a$}MK(IJlFocrDLJB~jqc|yqfOz>K|utYM>
z@#TdVmdS`XKxM5AirYl6v5UNc1*jXg!5iRp$b*=r=fb648v$RtXme_U<5CwvZ&Kb6
zYgM25h#Rg!P2;V9q5$e9lt9_F)E#67Es*<@&<@k7&87!$55d(Coc~mi07n^uDfVF|
zs?T}b^0<racgrEUxBv8t1o<>c@GGD^LC!>k)8L=)u1bI-h=h)p$7j)7E@lVdEw5u5
zy1)fcrCi9JmH(UNy{#YWS^0lfULBS{6Al&S2S=H}>KenYwr5&;OY8cdwerS|o9iXm
zL*WeF;*P=4Mh11uhHlg^xf6Frp{WhG4$8eI>aBs|F2<09bhVbz=?F^Q@ri!DlOrz^
z!);;L&=KM@7Qv-<16|M@*Rp8?wV>Y;4Eut+ptZTHIjDQK|E)_!|Kv`(*wKI@@HJ3&
zLi0v1&$o8Jw!m*)l0Zfff_NR2;Y<fZyd8ml7tmEQe3Fj`*48Lzwq>waK~!)&9UZ6c
zndTet!plU|WME`W)PI)+eEg7)@96IVCySye@-PM<G3e3BCOkfO)3M`khYe6_P3qrx
zNjvOY{o0pOc12=`I95W2>evpCkH4<Y)Dt0e_MQmIa-J9sH+N4=5nI+1pMB(^+z&wB
z6GuLDdu4{67z0l42?03VEGnrzF##~y6Cq$8o|uB2(i4k%zm}~~nZF(>Jt1~je1A}!
ZsV73{>^%{Z<vcMOZtk9#BKDs=@eBW@U8n#6
new file mode 100644
index 0000000000000000000000000000000000000000..a8254853738e2083fbea3c659e4346c141214f2f
GIT binary patch
literal 65536
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
new file mode 100644
--- /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);
+}
new file mode 100644
--- /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;
+}
new file mode 100644
--- /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();
+}
--- a/b2g/components/test/unit/xpcshell.ini
+++ b/b2g/components/test/unit/xpcshell.ini
@@ -1,14 +1,24 @@
 [DEFAULT]
 head =
 tail =
 
+support-files =
+  data/test_logger_file
+  data/test_properties
+
 [test_bug793310.js]
 
 [test_bug832946.js]
 
 [test_fxaccounts.js]
 [test_signintowebsite.js]
 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]