summaryrefslogtreecommitdiffstats
path: root/toolkit/components/crashes/CrashService.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--toolkit/components/crashes/CrashService.sys.mjs234
1 files changed, 234 insertions, 0 deletions
diff --git a/toolkit/components/crashes/CrashService.sys.mjs b/toolkit/components/crashes/CrashService.sys.mjs
new file mode 100644
index 0000000000..cedbca53b9
--- /dev/null
+++ b/toolkit/components/crashes/CrashService.sys.mjs
@@ -0,0 +1,234 @@
+/* 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 minidump-analyzer
+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 getMinidumpAnalyzerPath() {
+ const binSuffix = AppConstants.platform === "win" ? ".exe" : "";
+ const exeName = "minidump-analyzer" + binSuffix;
+
+ let exe = Services.dirsvc.get("GreBinD", Ci.nsIFile);
+ exe.append(exeName);
+
+ return exe;
+}
+
+/**
+ * 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 {
+ let exe = getMinidumpAnalyzerPath();
+ 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);
+ 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;
+ }
+ })();
+}
+
+/**
+ * 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, 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;
+ }
+ },
+});