diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /b2g/components/LogShake.jsm | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'b2g/components/LogShake.jsm')
-rw-r--r-- | b2g/components/LogShake.jsm | 588 |
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; |