246 lines
6.9 KiB
JavaScript
246 lines
6.9 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/. */
|
|
|
|
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
|
|
import { AsyncShutdown } from "resource://gre/modules/AsyncShutdown.sys.mjs";
|
|
|
|
// Set to true if the application is quitting
|
|
var gQuitting = false;
|
|
|
|
// Tracks all the running instances of the crash reporter client (for minidump
|
|
// analysis).
|
|
var gRunningProcesses = new Set();
|
|
|
|
/**
|
|
* Run the minidump-analyzer with the given options unless we're already
|
|
* shutting down or the main process has been instructed to shut down in the
|
|
* case a content process crashes. Minidump analysis can take a while so we
|
|
* don't want to block shutdown waiting for it.
|
|
*/
|
|
async function maybeRunMinidumpAnalyzer(minidumpPath, allThreads) {
|
|
let shutdown = Services.env.exists("MOZ_CRASHREPORTER_SHUTDOWN");
|
|
|
|
if (gQuitting || shutdown) {
|
|
return;
|
|
}
|
|
|
|
await runMinidumpAnalyzer(minidumpPath, allThreads).catch(e =>
|
|
console.error(e)
|
|
);
|
|
}
|
|
|
|
function getCrashReporterPath() {
|
|
const binPrefix =
|
|
AppConstants.platform === "macosx"
|
|
? "crashreporter.app/Contents/MacOS/"
|
|
: "";
|
|
const binSuffix = AppConstants.platform === "win" ? ".exe" : "";
|
|
const exeName = binPrefix + "crashreporter" + binSuffix;
|
|
|
|
let exe = Services.dirsvc.get("GreBinD", Ci.nsIFile);
|
|
exe.appendRelativePath(exeName);
|
|
|
|
return exe;
|
|
}
|
|
|
|
/**
|
|
* Run the crash reporter's 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 {
|
|
let exe = getCrashReporterPath();
|
|
let args = ["--analyze", minidumpPath];
|
|
let process = Cc["@mozilla.org/process/util;1"].createInstance(
|
|
Ci.nsIProcess
|
|
);
|
|
process.init(exe);
|
|
process.startHidden = true;
|
|
process.noShell = true;
|
|
|
|
if (allThreads) {
|
|
args.push("--full");
|
|
}
|
|
|
|
process.runAsync(args, args.length, (subject, topic) => {
|
|
switch (topic) {
|
|
case "process-finished":
|
|
gRunningProcesses.delete(process);
|
|
resolve();
|
|
break;
|
|
case "process-failed":
|
|
gRunningProcesses.delete(process);
|
|
resolve();
|
|
break;
|
|
default:
|
|
reject(new Error("Unexpected topic received " + topic));
|
|
break;
|
|
}
|
|
});
|
|
|
|
gRunningProcesses.add(process);
|
|
} catch (e) {
|
|
reject(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 IOUtils.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) {
|
|
console.error(e);
|
|
return null;
|
|
}
|
|
})();
|
|
}
|
|
|
|
export var CrashServiceUtils = {
|
|
computeMinidumpHash: async function CrashServiceUtils_computeMinidumpHash(f) {
|
|
return computeMinidumpHash(f);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* 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 IOUtils.read(extraPath);
|
|
|
|
return JSON.parse(decoder.decode(extraData));
|
|
} catch (e) {
|
|
console.error(e);
|
|
return {};
|
|
}
|
|
})();
|
|
}
|
|
|
|
/**
|
|
* This component makes crash data available throughout the application.
|
|
*
|
|
* It is a service because some background activity will eventually occur.
|
|
*/
|
|
export function CrashService() {
|
|
Services.obs.addObserver(this, "quit-application");
|
|
}
|
|
|
|
CrashService.prototype = Object.freeze({
|
|
classID: Components.ID("{92668367-1b17-4190-86b2-1061b2179744}"),
|
|
QueryInterface: ChromeUtils.generateQI(["nsICrashService", "nsIObserver"]),
|
|
|
|
async addCrash(processType, crashType, id) {
|
|
if (processType === Ci.nsIXULRuntime.PROCESS_TYPE_IPDLUNITTEST) {
|
|
return;
|
|
}
|
|
|
|
processType = Services.crashmanager.processTypes[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 minidumpPath = Services.appinfo.getMinidumpForID(id).path;
|
|
let extraPath = Services.appinfo.getExtraFileForID(id).path;
|
|
let metadata = {};
|
|
let hash = null;
|
|
|
|
await maybeRunMinidumpAnalyzer(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) {
|
|
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;
|
|
}
|
|
},
|
|
});
|