Files
tubestation/toolkit/components/crashes/CrashService.js
Gabriele Svelto b71bbf9689 Bug 1408806 - Gracefully deal with errors when killing the minidump-analyzer on shutdown; r=mconley
This solves two problems that were causing tests to fail intermittently. The
first issue is that calling nsIProcess.kill() on a process that had already
terminated would throw an exception which wouldn't be caught. Since this might
happen in cases where the minidump-analyzer run significantly faster than the
main event loop during the test. The second issue is that we tried to
unconditionally escape both the 'TelemetryEnvironment' and 'StackTraces'
field, but the latter would not be present in cases where the
minidump-analyzer would fail or be killed. This led to another spurious
exception.

MozReview-Commit-ID: 7srQtzig7xw
2017-10-26 22:59:29 +02:00

257 lines
7.8 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/. */
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/AppConstants.jsm", this);
Cu.import("resource://gre/modules/AsyncShutdown.jsm", this);
Cu.import("resource://gre/modules/KeyValueParser.jsm");
Cu.import("resource://gre/modules/osfile.jsm", this);
Cu.import("resource://gre/modules/PromiseUtils.jsm", this);
Cu.import("resource://gre/modules/Services.jsm", this);
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
// Set to true if the application is quitting
var gQuitting = false;
// Tracks all the running instances of the minidump-analyzer
var gRunningProcesses = new Set();
/**
* Run the minidump analyzer tool to gather stack traces from the minidump. The
* stack traces will be stored in the .extra file under the StackTraces= entry.
*
* @param minidumpPath {string} The path to the minidump file
* @param allThreads {bool} Gather stack traces for all threads, not just the
* crashing thread.
*
* @returns {Promise} A promise that gets resolved once minidump analysis has
* finished.
*/
function runMinidumpAnalyzer(minidumpPath, allThreads) {
return new Promise((resolve, reject) => {
try {
const binSuffix = AppConstants.platform === "win" ? ".exe" : "";
const exeName = "minidump-analyzer" + binSuffix;
let exe = Services.dirsvc.get("GreBinD", Ci.nsIFile);
if (AppConstants.platform === "macosx") {
exe.append("crashreporter.app");
exe.append("Contents");
exe.append("MacOS");
}
exe.append(exeName);
let args = [ minidumpPath ];
let process = Cc["@mozilla.org/process/util;1"]
.createInstance(Ci.nsIProcess);
process.init(exe);
process.startHidden = true;
process.noShell = true;
if (allThreads) {
args.unshift("--full");
}
process.runAsync(args, args.length, (subject, topic, data) => {
switch (topic) {
case "process-finished":
gRunningProcesses.delete(process);
resolve();
break;
case "process-failed":
gRunningProcesses.delete(process);
reject();
break;
default:
reject(new Error("Unexpected topic received " + topic));
break;
}
});
gRunningProcesses.add(process);
} catch (e) {
Cu.reportError(e);
}
});
}
/**
* Computes the SHA256 hash of a minidump file
*
* @param minidumpPath {string} The path to the minidump file
*
* @returns {Promise} A promise that resolves to the hash value of the
* minidump.
*/
function computeMinidumpHash(minidumpPath) {
return (async function() {
try {
let minidumpData = await OS.File.read(minidumpPath);
let hasher = Cc["@mozilla.org/security/hash;1"]
.createInstance(Ci.nsICryptoHash);
hasher.init(hasher.SHA256);
hasher.update(minidumpData, minidumpData.length);
let hashBin = hasher.finish(false);
let hash = "";
for (let i = 0; i < hashBin.length; i++) {
// Every character in the hash string contains a byte of the hash data
hash += ("0" + hashBin.charCodeAt(i).toString(16)).slice(-2);
}
return hash;
} catch (e) {
Cu.reportError(e);
return null;
}
})();
}
/**
* Process the given .extra file and return the annotations it contains in an
* object.
*
* @param extraPath {string} The path to the .extra file
*
* @return {Promise} A promise that resolves to an object holding the crash
* annotations.
*/
function processExtraFile(extraPath) {
return (async function() {
try {
let decoder = new TextDecoder();
let extraData = await OS.File.read(extraPath);
let keyValuePairs = parseKeyValuePairs(decoder.decode(extraData));
// When reading from an .extra file literal '\\n' sequences are
// automatically unescaped to two backslashes plus a newline, so we need
// to re-escape them into '\\n' again so that the fields holding JSON
// strings are valid.
[ "TelemetryEnvironment", "StackTraces" ].forEach(field => {
if (field in keyValuePairs) {
keyValuePairs[field] = keyValuePairs[field].replace(/\n/g, "n");
}
});
return keyValuePairs;
} catch (e) {
Cu.reportError(e);
return {};
}
})();
}
/**
* This component makes crash data available throughout the application.
*
* It is a service because some background activity will eventually occur.
*/
this.CrashService = function() {
Services.obs.addObserver(this, "quit-application");
};
CrashService.prototype = Object.freeze({
classID: Components.ID("{92668367-1b17-4190-86b2-1061b2179744}"),
QueryInterface: XPCOMUtils.generateQI([
Ci.nsICrashService,
Ci.nsIObserver,
]),
async addCrash(processType, crashType, id) {
switch (processType) {
case Ci.nsICrashService.PROCESS_TYPE_MAIN:
processType = Services.crashmanager.PROCESS_TYPE_MAIN;
break;
case Ci.nsICrashService.PROCESS_TYPE_CONTENT:
processType = Services.crashmanager.PROCESS_TYPE_CONTENT;
break;
case Ci.nsICrashService.PROCESS_TYPE_PLUGIN:
processType = Services.crashmanager.PROCESS_TYPE_PLUGIN;
break;
case Ci.nsICrashService.PROCESS_TYPE_GMPLUGIN:
processType = Services.crashmanager.PROCESS_TYPE_GMPLUGIN;
break;
case Ci.nsICrashService.PROCESS_TYPE_GPU:
processType = Services.crashmanager.PROCESS_TYPE_GPU;
break;
default:
throw new Error("Unrecognized PROCESS_TYPE: " + processType);
}
let allThreads = false;
switch (crashType) {
case Ci.nsICrashService.CRASH_TYPE_CRASH:
crashType = Services.crashmanager.CRASH_TYPE_CRASH;
break;
case Ci.nsICrashService.CRASH_TYPE_HANG:
crashType = Services.crashmanager.CRASH_TYPE_HANG;
allThreads = true;
break;
default:
throw new Error("Unrecognized CRASH_TYPE: " + crashType);
}
let cr = Cc["@mozilla.org/toolkit/crash-reporter;1"]
.getService(Components.interfaces.nsICrashReporter);
let minidumpPath = cr.getMinidumpForID(id).path;
let extraPath = cr.getExtraFileForID(id).path;
let metadata = {};
let hash = null;
if (!gQuitting) {
// Minidump analysis can take a long time, don't start it if the browser
// is already quitting.
await runMinidumpAnalyzer(minidumpPath, allThreads);
}
metadata = await processExtraFile(extraPath);
hash = await computeMinidumpHash(minidumpPath);
if (hash) {
metadata.MinidumpSha256Hash = hash;
}
let blocker = Services.crashmanager.addCrash(processType, crashType, id,
new Date(), metadata);
AsyncShutdown.profileBeforeChange.addBlocker(
"CrashService waiting for content crash ping to be sent", blocker
);
blocker.then(AsyncShutdown.profileBeforeChange.removeBlocker(blocker));
await blocker;
},
observe(subject, topic, data) {
switch (topic) {
case "profile-after-change":
// Side-effect is the singleton is instantiated.
Services.crashmanager;
break;
case "quit-application":
gQuitting = true;
gRunningProcesses.forEach((process) => {
try {
process.kill();
} catch (e) {
// If the process has already quit then kill() fails, but since
// this failure is benign it is safe to silently ignore it.
}
Services.obs.notifyObservers(null, "test-minidump-analyzer-killed");
});
break;
}
},
});
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([CrashService]);