1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5"use strict";
6
7const { AppConstants } = ChromeUtils.import(
8  "resource://gre/modules/AppConstants.jsm"
9);
10const { AsyncShutdown } = ChromeUtils.import(
11  "resource://gre/modules/AsyncShutdown.jsm"
12);
13const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
14
15// Set to true if the application is quitting
16var gQuitting = false;
17
18// Tracks all the running instances of the minidump-analyzer
19var gRunningProcesses = new Set();
20
21/**
22 * Run the minidump-analyzer with the given options unless we're already
23 * shutting down or the main process has been instructed to shut down in the
24 * case a content process crashes. Minidump analysis can take a while so we
25 * don't want to block shutdown waiting for it.
26 */
27async function maybeRunMinidumpAnalyzer(minidumpPath, allThreads) {
28  let env = Cc["@mozilla.org/process/environment;1"].getService(
29    Ci.nsIEnvironment
30  );
31  let shutdown = env.exists("MOZ_CRASHREPORTER_SHUTDOWN");
32
33  if (gQuitting || shutdown) {
34    return;
35  }
36
37  await runMinidumpAnalyzer(minidumpPath, allThreads).catch(e =>
38    Cu.reportError(e)
39  );
40}
41
42function getMinidumpAnalyzerPath() {
43  const binSuffix = AppConstants.platform === "win" ? ".exe" : "";
44  const exeName = "minidump-analyzer" + binSuffix;
45
46  let exe = Services.dirsvc.get("GreBinD", Ci.nsIFile);
47  exe.append(exeName);
48
49  return exe;
50}
51
52/**
53 * Run the minidump analyzer tool to gather stack traces from the minidump. The
54 * stack traces will be stored in the .extra file under the StackTraces= entry.
55 *
56 * @param minidumpPath {string} The path to the minidump file
57 * @param allThreads {bool} Gather stack traces for all threads, not just the
58 *                   crashing thread.
59 *
60 * @returns {Promise} A promise that gets resolved once minidump analysis has
61 *          finished.
62 */
63function runMinidumpAnalyzer(minidumpPath, allThreads) {
64  return new Promise((resolve, reject) => {
65    try {
66      let exe = getMinidumpAnalyzerPath();
67      let args = [minidumpPath];
68      let process = Cc["@mozilla.org/process/util;1"].createInstance(
69        Ci.nsIProcess
70      );
71      process.init(exe);
72      process.startHidden = true;
73      process.noShell = true;
74
75      if (allThreads) {
76        args.unshift("--full");
77      }
78
79      process.runAsync(args, args.length, (subject, topic, data) => {
80        switch (topic) {
81          case "process-finished":
82            gRunningProcesses.delete(process);
83            resolve();
84            break;
85          case "process-failed":
86            gRunningProcesses.delete(process);
87            resolve();
88            break;
89          default:
90            reject(new Error("Unexpected topic received " + topic));
91            break;
92        }
93      });
94
95      gRunningProcesses.add(process);
96    } catch (e) {
97      reject(e);
98    }
99  });
100}
101
102/**
103 * Computes the SHA256 hash of a minidump file
104 *
105 * @param minidumpPath {string} The path to the minidump file
106 *
107 * @returns {Promise} A promise that resolves to the hash value of the
108 *          minidump.
109 */
110function computeMinidumpHash(minidumpPath) {
111  return (async function() {
112    try {
113      let minidumpData = await IOUtils.read(minidumpPath);
114      let hasher = Cc["@mozilla.org/security/hash;1"].createInstance(
115        Ci.nsICryptoHash
116      );
117      hasher.init(hasher.SHA256);
118      hasher.update(minidumpData, minidumpData.length);
119
120      let hashBin = hasher.finish(false);
121      let hash = "";
122
123      for (let i = 0; i < hashBin.length; i++) {
124        // Every character in the hash string contains a byte of the hash data
125        hash += ("0" + hashBin.charCodeAt(i).toString(16)).slice(-2);
126      }
127
128      return hash;
129    } catch (e) {
130      Cu.reportError(e);
131      return null;
132    }
133  })();
134}
135
136/**
137 * Process the given .extra file and return the annotations it contains in an
138 * object.
139 *
140 * @param extraPath {string} The path to the .extra file
141 *
142 * @return {Promise} A promise that resolves to an object holding the crash
143 *         annotations.
144 */
145function processExtraFile(extraPath) {
146  return (async function() {
147    try {
148      let decoder = new TextDecoder();
149      let extraData = await IOUtils.read(extraPath);
150
151      return JSON.parse(decoder.decode(extraData));
152    } catch (e) {
153      Cu.reportError(e);
154      return {};
155    }
156  })();
157}
158
159/**
160 * This component makes crash data available throughout the application.
161 *
162 * It is a service because some background activity will eventually occur.
163 */
164this.CrashService = function() {
165  Services.obs.addObserver(this, "quit-application");
166};
167
168CrashService.prototype = Object.freeze({
169  classID: Components.ID("{92668367-1b17-4190-86b2-1061b2179744}"),
170  QueryInterface: ChromeUtils.generateQI(["nsICrashService", "nsIObserver"]),
171
172  async addCrash(processType, crashType, id) {
173    switch (processType) {
174      case Ci.nsICrashService.PROCESS_TYPE_MAIN:
175        processType = Services.crashmanager.PROCESS_TYPE_MAIN;
176        break;
177      case Ci.nsICrashService.PROCESS_TYPE_CONTENT:
178        processType = Services.crashmanager.PROCESS_TYPE_CONTENT;
179        break;
180      case Ci.nsICrashService.PROCESS_TYPE_GMPLUGIN:
181        processType = Services.crashmanager.PROCESS_TYPE_GMPLUGIN;
182        break;
183      case Ci.nsICrashService.PROCESS_TYPE_GPU:
184        processType = Services.crashmanager.PROCESS_TYPE_GPU;
185        break;
186      case Ci.nsICrashService.PROCESS_TYPE_VR:
187        processType = Services.crashmanager.PROCESS_TYPE_VR;
188        break;
189      case Ci.nsICrashService.PROCESS_TYPE_RDD:
190        processType = Services.crashmanager.PROCESS_TYPE_RDD;
191        break;
192      case Ci.nsICrashService.PROCESS_TYPE_SOCKET:
193        processType = Services.crashmanager.PROCESS_TYPE_SOCKET;
194        break;
195      case Ci.nsICrashService.PROCESS_TYPE_IPDLUNITTEST:
196        // We'll never send crash reports for this type of process.
197        return;
198      default:
199        throw new Error("Unrecognized PROCESS_TYPE: " + processType);
200    }
201
202    let allThreads = false;
203
204    switch (crashType) {
205      case Ci.nsICrashService.CRASH_TYPE_CRASH:
206        crashType = Services.crashmanager.CRASH_TYPE_CRASH;
207        break;
208      case Ci.nsICrashService.CRASH_TYPE_HANG:
209        crashType = Services.crashmanager.CRASH_TYPE_HANG;
210        allThreads = true;
211        break;
212      default:
213        throw new Error("Unrecognized CRASH_TYPE: " + crashType);
214    }
215
216    let cr = Cc["@mozilla.org/toolkit/crash-reporter;1"].getService(
217      Ci.nsICrashReporter
218    );
219    let minidumpPath = cr.getMinidumpForID(id).path;
220    let extraPath = cr.getExtraFileForID(id).path;
221    let metadata = {};
222    let hash = null;
223
224    await maybeRunMinidumpAnalyzer(minidumpPath, allThreads);
225    metadata = await processExtraFile(extraPath);
226    hash = await computeMinidumpHash(minidumpPath);
227
228    if (hash) {
229      metadata.MinidumpSha256Hash = hash;
230    }
231
232    let blocker = Services.crashmanager.addCrash(
233      processType,
234      crashType,
235      id,
236      new Date(),
237      metadata
238    );
239
240    AsyncShutdown.profileBeforeChange.addBlocker(
241      "CrashService waiting for content crash ping to be sent",
242      blocker
243    );
244
245    blocker.then(AsyncShutdown.profileBeforeChange.removeBlocker(blocker));
246
247    await blocker;
248  },
249
250  observe(subject, topic, data) {
251    switch (topic) {
252      case "profile-after-change":
253        // Side-effect is the singleton is instantiated.
254        Services.crashmanager;
255        break;
256      case "quit-application":
257        gQuitting = true;
258        gRunningProcesses.forEach(process => {
259          try {
260            process.kill();
261          } catch (e) {
262            // If the process has already quit then kill() fails, but since
263            // this failure is benign it is safe to silently ignore it.
264          }
265          Services.obs.notifyObservers(null, "test-minidump-analyzer-killed");
266        });
267        break;
268    }
269  },
270});
271
272var EXPORTED_SYMBOLS = ["CrashService"];
273