summaryrefslogtreecommitdiffstats
path: root/b2g/components/LogShake.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'b2g/components/LogShake.jsm')
-rw-r--r--b2g/components/LogShake.jsm588
1 files changed, 588 insertions, 0 deletions
diff --git a/b2g/components/LogShake.jsm b/b2g/components/LogShake.jsm
new file mode 100644
index 000000000..6426c21de
--- /dev/null
+++ b/b2g/components/LogShake.jsm
@@ -0,0 +1,588 @@
+/* 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 name and detail.logPaths representing the patch to each log file or
+ * the path to the archive.
+ * If an error occurs it will instead produce a 'capture-logs-error' event.
+ * We send a capture-logs-start events to notify the system app and the user,
+ * since dumping can be a bit long sometimes.
+ */
+
+/* enable Mozilla javascript extensions and global strictness declaration,
+ * disable valid this checking */
+/* jshint moz: true, esnext: true */
+/* jshint -W097 */
+/* jshint -W040 */
+/* global Services, Components, dump, LogCapture, LogParser,
+ OS, Promise, volumeService, XPCOMUtils, SystemAppProxy */
+
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+// Constants for creating zip file taken from toolkit/webapps/tests/head.js
+const PR_RDWR = 0x04;
+const PR_CREATE_FILE = 0x08;
+const PR_TRUNCATE = 0x20;
+
+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;
+/**
+ * The maximum fraction to update the excitement value per frame. This
+ * corresponds to requiring shaking for approximately 10 motion events (1.6
+ * seconds)
+ */
+const EXCITEMENT_FILTER_ALPHA = 0.2;
+const DEVICE_MOTION_EVENT = "devicemotion";
+const SCREEN_CHANGE_EVENT = "screenchange";
+const CAPTURE_LOGS_CONTENT_EVENT = "requestSystemLogs";
+const CAPTURE_LOGS_START_EVENT = "capture-logs-start";
+const CAPTURE_LOGS_ERROR_EVENT = "capture-logs-error";
+const CAPTURE_LOGS_SUCCESS_EVENT = "capture-logs-success";
+
+var LogShake = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+
+ /**
+ * If LogShake is in QA Mode, which bundles all files into a compressed archive
+ */
+ qaModeEnabled: false,
+
+ /**
+ * 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,
+
+ /**
+ * We only listen to motion events when the screen is enabled, keep track
+ * of its state.
+ */
+ screenEnabled: true,
+
+ /**
+ * Flag monitoring if the preference to enable shake to capture is
+ * enabled in gaia.
+ */
+ listenToDeviceMotion: true,
+
+ /**
+ * If a capture has been requested and is waiting for reads/parsing. Used for
+ * debouncing.
+ */
+ captureRequested: false,
+
+ /**
+ * The current excitement (movement) level
+ */
+ excitement: 0,
+
+ /**
+ * Map of files which have log-type information to their parsers
+ */
+ LOGS_WITH_PARSERS: {
+ "/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/last_kmsg": LogParser.prettyPrintArray,
+ "/proc/meminfo": LogParser.prettyPrintArray,
+ "/proc/uptime": LogParser.prettyPrintArray,
+ "/proc/version": LogParser.prettyPrintArray,
+ "/proc/vmallocinfo": LogParser.prettyPrintArray,
+ "/proc/vmstat": LogParser.prettyPrintArray,
+ "/system/b2g/application.ini": LogParser.prettyPrintArray,
+ "/cache/recovery/last_install": LogParser.prettyPrintArray,
+ "/cache/recovery/last_kmsg": LogParser.prettyPrintArray,
+ "/cache/recovery/last_log": LogParser.prettyPrintArray
+ },
+
+ /**
+ * 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
+ }});
+
+ // Reset excitement to clear residual motion
+ this.excitement = 0;
+
+ SystemAppProxy.addEventListener(CAPTURE_LOGS_CONTENT_EVENT, this, false);
+ 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;
+
+ case CAPTURE_LOGS_CONTENT_EVENT:
+ this.startCapture();
+ break;
+ }
+ },
+
+ /**
+ * Handle an observation from Services.obs
+ */
+ observe: function(subject, topic) {
+ if (topic === "xpcom-shutdown") {
+ this.uninit();
+ }
+ },
+
+ enableQAMode: function() {
+ debug("Enabling QA Mode");
+ this.qaModeEnabled = true;
+ },
+
+ disableQAMode: function() {
+ debug("Disabling QA Mode");
+ this.qaModeEnabled = false;
+ },
+
+ enableDeviceMotionListener: function() {
+ this.listenToDeviceMotion = true;
+ this.startDeviceMotionListener();
+ },
+
+ disableDeviceMotionListener: function() {
+ this.listenToDeviceMotion = false;
+ this.stopDeviceMotionListener();
+ },
+
+ startDeviceMotionListener: function() {
+ if (!this.deviceMotionEnabled &&
+ this.listenToDeviceMotion &&
+ this.screenEnabled) {
+ 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;
+ }
+
+ let acc = event.accelerationIncludingGravity;
+
+ // Updates excitement by a factor of at most alpha, ignoring sudden device
+ // motion. See bug #1101994 for more information.
+ let newExcitement = acc.x * acc.x + acc.y * acc.y + acc.z * acc.z;
+ this.excitement += (newExcitement - this.excitement) * EXCITEMENT_FILTER_ALPHA;
+
+ if (this.excitement > EXCITEMENT_THRESHOLD) {
+ this.startCapture();
+ }
+ },
+
+ startCapture: function() {
+ if (this.captureRequested) {
+ return;
+ }
+ this.captureRequested = true;
+ SystemAppProxy._sendCustomEvent(CAPTURE_LOGS_START_EVENT, {});
+ this.captureLogs().then(logResults => {
+ // On resolution send the success event to the requester
+ SystemAppProxy._sendCustomEvent(CAPTURE_LOGS_SUCCESS_EVENT, {
+ logPaths: logResults.logPaths,
+ logFilenames: logResults.logFilenames
+ });
+ 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) {
+ this.screenEnabled = event.detail.screenEnabled;
+ if (this.screenEnabled) {
+ this.startDeviceMotionListener();
+ } else {
+ this.stopDeviceMotionListener();
+ }
+ },
+
+ /**
+ * Captures and saves the current device logs, returning a promise that will
+ * resolve to an array of log filenames.
+ */
+ captureLogs: function() {
+ return this.readLogs().then(logArrays => {
+ return this.saveLogs(logArrays);
+ });
+ },
+
+ /**
+ * Read in all log files, returning their formatted contents
+ * @return {Promise<Array>}
+ */
+ readLogs: function() {
+ let logArrays = {};
+ let readPromises = [];
+
+ try {
+ logArrays["properties"] =
+ LogParser.prettyPrintPropertiesArray(LogCapture.readProperties());
+ } catch (ex) {
+ Cu.reportError("Unable to get device properties: " + ex);
+ }
+
+ // Let Gecko perfom the dump to a file, and just collect it
+ let readAboutMemoryPromise = new Promise(resolve => {
+ // Wrap the readAboutMemory promise to make it infallible
+ LogCapture.readAboutMemory().then(aboutMemory => {
+ let file = OS.Path.basename(aboutMemory);
+ let logArray;
+ try {
+ logArray = LogCapture.readLogFile(aboutMemory);
+ if (!logArray) {
+ debug("LogCapture.readLogFile() returned nothing for about:memory");
+ }
+ // We need to remove the dumped file, now that we have it in memory
+ OS.File.remove(aboutMemory);
+ } catch (ex) {
+ Cu.reportError("Unable to handle about:memory dump: " + ex);
+ }
+ logArrays[file] = LogParser.prettyPrintArray(logArray);
+ resolve();
+ }, ex => {
+ Cu.reportError("Unable to get about:memory dump: " + ex);
+ resolve();
+ });
+ });
+ readPromises.push(readAboutMemoryPromise);
+
+ // Wrap the promise to make it infallible
+ let readScreenshotPromise = new Promise(resolve => {
+ LogCapture.getScreenshot().then(screenshot => {
+ logArrays["screenshot.png"] = screenshot;
+ resolve();
+ }, ex => {
+ Cu.reportError("Unable to get screenshot dump: " + ex);
+ resolve();
+ });
+ });
+ readPromises.push(readScreenshotPromise);
+
+ for (let loc in this.LOGS_WITH_PARSERS) {
+ let logArray;
+ try {
+ logArray = LogCapture.readLogFile(loc);
+ if (!logArray) {
+ debug("LogCapture.readLogFile() returned nothing for: " + loc);
+ continue;
+ }
+ } catch (ex) {
+ Cu.reportError("Unable to LogCapture.readLogFile('" + loc + "'): " + ex);
+ continue;
+ }
+
+ try {
+ logArrays[loc] = this.LOGS_WITH_PARSERS[loc](logArray);
+ } catch (ex) {
+ Cu.reportError("Unable to parse content of '" + loc + "': " + ex);
+ continue;
+ }
+ }
+
+ // Because the promises we depend upon can't fail this means that the
+ // blocking log reads will always be honored.
+ return Promise.all(readPromises).then(() => {
+ return logArrays;
+ });
+ },
+
+ /**
+ * Save the formatted arrays of log files to an sdcard if available
+ */
+ saveLogs: function(logArrays) {
+ if (!logArrays || Object.keys(logArrays).length === 0) {
+ return Promise.reject("Zero logs saved");
+ }
+
+ if (this.qaModeEnabled) {
+ return makeBaseLogsDirectory().then(writeLogArchive(logArrays),
+ rejectFunction("Error making base log directory"));
+ } else {
+ return makeBaseLogsDirectory().then(makeLogsDirectory,
+ rejectFunction("Error making base log directory"))
+ .then(writeLogFiles(logArrays),
+ rejectFunction("Error creating log directory"));
+ }
+ },
+
+ /**
+ * 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);
+ }
+
+ // If no extension is provided, default to forcing .log
+ let extension = ".log";
+ let logLocationExt = logLocation.split(".");
+ if (logLocationExt.length > 1) {
+ // otherwise, just append nothing
+ extension = "";
+ }
+
+ return logName + extension;
+}
+
+function getSdcardPrefix() {
+ return volumeService.getVolumeByName("sdcard").mountPoint;
+}
+
+function getLogDirectoryRoot() {
+ return "logs";
+}
+
+function getLogIdentifier() {
+ let d = new Date();
+ d = new Date(d.getTime() - d.getTimezoneOffset() * 60000);
+ let timestamp = d.toISOString().slice(0, -5).replace(/[:T]/g, "-");
+ return timestamp;
+}
+
+function rejectFunction(message) {
+ return function(err) {
+ debug(message + ": " + err);
+ return Promise.reject(err);
+ };
+}
+
+function makeBaseLogsDirectory() {
+ let sdcardPrefix;
+ try {
+ sdcardPrefix = getSdcardPrefix();
+ } catch(e) {
+ // Handles missing sdcard
+ return Promise.reject(e);
+ }
+
+ let dirNameRoot = getLogDirectoryRoot();
+
+ let logsRoot = OS.Path.join(sdcardPrefix, dirNameRoot);
+
+ debug("Creating base log directory at root " + sdcardPrefix);
+
+ return OS.File.makeDir(logsRoot, {from: sdcardPrefix}).then(
+ function() {
+ return {
+ sdcardPrefix: sdcardPrefix,
+ basePrefix: dirNameRoot
+ };
+ }
+ );
+}
+
+function makeLogsDirectory({sdcardPrefix, basePrefix}) {
+ let dirName = getLogIdentifier();
+
+ let logsRoot = OS.Path.join(sdcardPrefix, basePrefix);
+ let logsDir = OS.Path.join(logsRoot, dirName);
+
+ debug("Creating base log directory at root " + sdcardPrefix);
+ debug("Final created directory will be " + logsDir);
+
+ return OS.File.makeDir(logsDir, {ignoreExisting: false}).then(
+ function() {
+ debug("Created: " + logsDir);
+ return {
+ logPrefix: OS.Path.join(basePrefix, dirName),
+ sdcardPrefix: sdcardPrefix
+ };
+ },
+ rejectFunction("Error at OS.File.makeDir for " + logsDir)
+ );
+}
+
+function getFile(filename) {
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.initWithPath(filename);
+ return file;
+}
+
+/**
+ * Make a zip file
+ * @param {String} absoluteZipFilename - Fully qualified desired location of the zip file
+ * @param {Map<String, Uint8Array>} logArrays - Map from log location to log data
+ * @return {Array<String>} Paths of entries in the archive
+ */
+function makeZipFile(absoluteZipFilename, logArrays) {
+ let logFilenames = [];
+ let zipWriter = Cc["@mozilla.org/zipwriter;1"].createInstance(Ci.nsIZipWriter);
+ let zipFile = getFile(absoluteZipFilename);
+ zipWriter.open(zipFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE);
+
+ for (let logLocation in logArrays) {
+ let logArray = logArrays[logLocation];
+ let logFilename = getLogFilename(logLocation);
+ logFilenames.push(logFilename);
+
+ debug("Adding " + logFilename + " to the zip");
+ let logArrayStream = Cc["@mozilla.org/io/arraybuffer-input-stream;1"]
+ .createInstance(Ci.nsIArrayBufferInputStream);
+ // Set data to be copied, default offset to 0 because it is not present on
+ // ArrayBuffer objects
+ logArrayStream.setData(logArray.buffer, logArray.byteOffset || 0,
+ logArray.byteLength);
+
+ zipWriter.addEntryStream(logFilename, Date.now(),
+ Ci.nsIZipWriter.COMPRESSION_DEFAULT,
+ logArrayStream, false);
+ }
+ zipWriter.close();
+
+ return logFilenames;
+}
+
+function writeLogArchive(logArrays) {
+ return function({sdcardPrefix, basePrefix}) {
+ // Now the directory is guaranteed to exist, save the logs into their
+ // archive file
+
+ let zipFilename = getLogIdentifier() + "-logs.zip";
+ let zipPath = OS.Path.join(basePrefix, zipFilename);
+ let zipPrefix = OS.Path.dirname(zipPath);
+ let absoluteZipPath = OS.Path.join(sdcardPrefix, zipPath);
+
+ debug("Creating zip file at " + zipPath);
+ let logFilenames = [];
+ try {
+ logFilenames = makeZipFile(absoluteZipPath, logArrays);
+ } catch(e) {
+ return Promise.reject(e);
+ }
+ debug("Zip file created");
+
+ return {
+ logFilenames: logFilenames,
+ logPaths: [zipPath],
+ compressed: true
+ };
+ };
+}
+
+function writeLogFiles(logArrays) {
+ return function({sdcardPrefix, logPrefix}) {
+ // Now the directory is guaranteed to exist, save the logs
+ let logFilenames = [];
+ let logPaths = [];
+ let saveRequests = [];
+
+ for (let logLocation in logArrays) {
+ debug("Requesting save of " + logLocation);
+ let logArray = logArrays[logLocation];
+ let logFilename = getLogFilename(logLocation);
+ // The local pathrepresents the relative path within the SD card, not the
+ // absolute path because Gaia will refer to it using the DeviceStorage
+ // API
+ let localPath = OS.Path.join(logPrefix, logFilename);
+
+ logFilenames.push(logFilename);
+ logPaths.push(localPath);
+
+ let absolutePath = OS.Path.join(sdcardPrefix, localPath);
+ let saveRequest = OS.File.writeAtomic(absolutePath, logArray);
+ saveRequests.push(saveRequest);
+ }
+
+ return Promise.all(saveRequests).then(
+ function() {
+ return {
+ logFilenames: logFilenames,
+ logPaths: logPaths,
+ compressed: false
+ };
+ },
+ rejectFunction("Error at some save request")
+ );
+ };
+}
+
+LogShake.init();
+this.LogShake = LogShake;