mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-31 22:25:30 +00:00
589 lines
18 KiB
JavaScript
589 lines
18 KiB
JavaScript
/* 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;
|