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