1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/crashreporter/CrashSubmit.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,478 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +Components.utils.import("resource://gre/modules/Services.jsm"); 1.9 +Components.utils.import("resource://gre/modules/KeyValueParser.jsm"); 1.10 + 1.11 +this.EXPORTED_SYMBOLS = [ 1.12 + "CrashSubmit" 1.13 +]; 1.14 + 1.15 +const Cc = Components.classes; 1.16 +const Ci = Components.interfaces; 1.17 +const STATE_START = Ci.nsIWebProgressListener.STATE_START; 1.18 +const STATE_STOP = Ci.nsIWebProgressListener.STATE_STOP; 1.19 + 1.20 +const SUCCESS = "success"; 1.21 +const FAILED = "failed"; 1.22 +const SUBMITTING = "submitting"; 1.23 + 1.24 +let reportURL = null; 1.25 +let strings = null; 1.26 +let myListener = null; 1.27 + 1.28 +function parseINIStrings(file) { 1.29 + var factory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"]. 1.30 + getService(Ci.nsIINIParserFactory); 1.31 + var parser = factory.createINIParser(file); 1.32 + var obj = {}; 1.33 + var en = parser.getKeys("Strings"); 1.34 + while (en.hasMore()) { 1.35 + var key = en.getNext(); 1.36 + obj[key] = parser.getString("Strings", key); 1.37 + } 1.38 + return obj; 1.39 +} 1.40 + 1.41 +// Since we're basically re-implementing part of the crashreporter 1.42 +// client here, we'll just steal the strings we need from crashreporter.ini 1.43 +function getL10nStrings() { 1.44 + let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. 1.45 + getService(Ci.nsIProperties); 1.46 + let path = dirSvc.get("GreD", Ci.nsIFile); 1.47 + path.append("crashreporter.ini"); 1.48 + if (!path.exists()) { 1.49 + // see if we're on a mac 1.50 + path = path.parent; 1.51 + path.append("crashreporter.app"); 1.52 + path.append("Contents"); 1.53 + path.append("MacOS"); 1.54 + path.append("crashreporter.ini"); 1.55 + if (!path.exists()) { 1.56 + // very bad, but I don't know how to recover 1.57 + return; 1.58 + } 1.59 + } 1.60 + let crstrings = parseINIStrings(path); 1.61 + strings = { 1.62 + 'crashid': crstrings.CrashID, 1.63 + 'reporturl': crstrings.CrashDetailsURL 1.64 + }; 1.65 + 1.66 + path = dirSvc.get("XCurProcD", Ci.nsIFile); 1.67 + path.append("crashreporter-override.ini"); 1.68 + if (path.exists()) { 1.69 + crstrings = parseINIStrings(path); 1.70 + if ('CrashID' in crstrings) 1.71 + strings['crashid'] = crstrings.CrashID; 1.72 + if ('CrashDetailsURL' in crstrings) 1.73 + strings['reporturl'] = crstrings.CrashDetailsURL; 1.74 + } 1.75 +} 1.76 + 1.77 +function getPendingDir() { 1.78 + let directoryService = Cc["@mozilla.org/file/directory_service;1"]. 1.79 + getService(Ci.nsIProperties); 1.80 + let pendingDir = directoryService.get("UAppData", Ci.nsIFile); 1.81 + pendingDir.append("Crash Reports"); 1.82 + pendingDir.append("pending"); 1.83 + return pendingDir; 1.84 +} 1.85 + 1.86 +function getPendingMinidump(id) { 1.87 + let pendingDir = getPendingDir(); 1.88 + let dump = pendingDir.clone(); 1.89 + let extra = pendingDir.clone(); 1.90 + dump.append(id + ".dmp"); 1.91 + extra.append(id + ".extra"); 1.92 + return [dump, extra]; 1.93 +} 1.94 + 1.95 +function getAllPendingMinidumpsIDs() { 1.96 + let minidumps = []; 1.97 + let pendingDir = getPendingDir(); 1.98 + 1.99 + if (!(pendingDir.exists() && pendingDir.isDirectory())) 1.100 + return []; 1.101 + let entries = pendingDir.directoryEntries; 1.102 + 1.103 + while (entries.hasMoreElements()) { 1.104 + let entry = entries.getNext().QueryInterface(Ci.nsIFile); 1.105 + if (entry.isFile()) { 1.106 + let matches = entry.leafName.match(/(.+)\.extra$/); 1.107 + if (matches) 1.108 + minidumps.push(matches[1]); 1.109 + } 1.110 + } 1.111 + 1.112 + return minidumps; 1.113 +} 1.114 + 1.115 +function pruneSavedDumps() { 1.116 + const KEEP = 10; 1.117 + 1.118 + let pendingDir = getPendingDir(); 1.119 + if (!(pendingDir.exists() && pendingDir.isDirectory())) 1.120 + return; 1.121 + let entries = pendingDir.directoryEntries; 1.122 + let entriesArray = []; 1.123 + 1.124 + while (entries.hasMoreElements()) { 1.125 + let entry = entries.getNext().QueryInterface(Ci.nsIFile); 1.126 + if (entry.isFile()) { 1.127 + let matches = entry.leafName.match(/(.+)\.extra$/); 1.128 + if (matches) 1.129 + entriesArray.push(entry); 1.130 + } 1.131 + } 1.132 + 1.133 + entriesArray.sort(function(a,b) { 1.134 + let dateA = a.lastModifiedTime; 1.135 + let dateB = b.lastModifiedTime; 1.136 + if (dateA < dateB) 1.137 + return -1; 1.138 + if (dateB < dateA) 1.139 + return 1; 1.140 + return 0; 1.141 + }); 1.142 + 1.143 + if (entriesArray.length > KEEP) { 1.144 + for (let i = 0; i < entriesArray.length - KEEP; ++i) { 1.145 + let extra = entriesArray[i]; 1.146 + let matches = extra.leafName.match(/(.+)\.extra$/); 1.147 + if (matches) { 1.148 + let dump = extra.clone(); 1.149 + dump.leafName = matches[1] + '.dmp'; 1.150 + dump.remove(false); 1.151 + extra.remove(false); 1.152 + } 1.153 + } 1.154 + } 1.155 +} 1.156 + 1.157 +function addFormEntry(doc, form, name, value) { 1.158 + var input = doc.createElement("input"); 1.159 + input.type = "hidden"; 1.160 + input.name = name; 1.161 + input.value = value; 1.162 + form.appendChild(input); 1.163 +} 1.164 + 1.165 +function writeSubmittedReport(crashID, viewURL) { 1.166 + let directoryService = Cc["@mozilla.org/file/directory_service;1"]. 1.167 + getService(Ci.nsIProperties); 1.168 + let reportFile = directoryService.get("UAppData", Ci.nsIFile); 1.169 + reportFile.append("Crash Reports"); 1.170 + reportFile.append("submitted"); 1.171 + if (!reportFile.exists()) 1.172 + reportFile.create(Ci.nsIFile.DIRECTORY_TYPE, 0700); 1.173 + reportFile.append(crashID + ".txt"); 1.174 + var fstream = Cc["@mozilla.org/network/file-output-stream;1"]. 1.175 + createInstance(Ci.nsIFileOutputStream); 1.176 + // open, write, truncate 1.177 + fstream.init(reportFile, -1, -1, 0); 1.178 + var os = Cc["@mozilla.org/intl/converter-output-stream;1"]. 1.179 + createInstance(Ci.nsIConverterOutputStream); 1.180 + os.init(fstream, "UTF-8", 0, 0x0000); 1.181 + 1.182 + var data = strings.crashid.replace("%s", crashID); 1.183 + if (viewURL) 1.184 + data += "\n" + strings.reporturl.replace("%s", viewURL); 1.185 + 1.186 + os.writeString(data); 1.187 + os.close(); 1.188 + fstream.close(); 1.189 +} 1.190 + 1.191 +// the Submitter class represents an individual submission. 1.192 +function Submitter(id, submitSuccess, submitError, noThrottle, 1.193 + extraExtraKeyVals) { 1.194 + this.id = id; 1.195 + this.successCallback = submitSuccess; 1.196 + this.errorCallback = submitError; 1.197 + this.noThrottle = noThrottle; 1.198 + this.additionalDumps = []; 1.199 + this.extraKeyVals = extraExtraKeyVals || {}; 1.200 +} 1.201 + 1.202 +Submitter.prototype = { 1.203 + submitSuccess: function Submitter_submitSuccess(ret) 1.204 + { 1.205 + if (!ret.CrashID) { 1.206 + this.notifyStatus(FAILED); 1.207 + this.cleanup(); 1.208 + return; 1.209 + } 1.210 + 1.211 + // Write out the details file to submitted/ 1.212 + writeSubmittedReport(ret.CrashID, ret.ViewURL); 1.213 + 1.214 + // Delete from pending dir 1.215 + try { 1.216 + this.dump.remove(false); 1.217 + this.extra.remove(false); 1.218 + for (let i of this.additionalDumps) { 1.219 + i.dump.remove(false); 1.220 + } 1.221 + } 1.222 + catch (ex) { 1.223 + // report an error? not much the user can do here. 1.224 + } 1.225 + 1.226 + this.notifyStatus(SUCCESS, ret); 1.227 + this.cleanup(); 1.228 + }, 1.229 + 1.230 + cleanup: function Submitter_cleanup() { 1.231 + // drop some references just to be nice 1.232 + this.successCallback = null; 1.233 + this.errorCallback = null; 1.234 + this.iframe = null; 1.235 + this.dump = null; 1.236 + this.extra = null; 1.237 + this.additionalDumps = null; 1.238 + // remove this object from the list of active submissions 1.239 + let idx = CrashSubmit._activeSubmissions.indexOf(this); 1.240 + if (idx != -1) 1.241 + CrashSubmit._activeSubmissions.splice(idx, 1); 1.242 + }, 1.243 + 1.244 + submitForm: function Submitter_submitForm() 1.245 + { 1.246 + if (!('ServerURL' in this.extraKeyVals)) { 1.247 + return false; 1.248 + } 1.249 + let serverURL = this.extraKeyVals.ServerURL; 1.250 + 1.251 + // Override the submission URL from the environment or prefs. 1.252 + 1.253 + var envOverride = Cc['@mozilla.org/process/environment;1']. 1.254 + getService(Ci.nsIEnvironment).get("MOZ_CRASHREPORTER_URL"); 1.255 + if (envOverride != '') { 1.256 + serverURL = envOverride; 1.257 + } 1.258 + else if ('PluginHang' in this.extraKeyVals) { 1.259 + try { 1.260 + serverURL = Services.prefs. 1.261 + getCharPref("toolkit.crashreporter.pluginHangSubmitURL"); 1.262 + } catch(e) { } 1.263 + } 1.264 + 1.265 + let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] 1.266 + .createInstance(Ci.nsIXMLHttpRequest); 1.267 + xhr.open("POST", serverURL, true); 1.268 + 1.269 + let formData = Cc["@mozilla.org/files/formdata;1"] 1.270 + .createInstance(Ci.nsIDOMFormData); 1.271 + // add the data 1.272 + for (let [name, value] in Iterator(this.extraKeyVals)) { 1.273 + if (name != "ServerURL") { 1.274 + formData.append(name, value); 1.275 + } 1.276 + } 1.277 + if (this.noThrottle) { 1.278 + // tell the server not to throttle this, since it was manually submitted 1.279 + formData.append("Throttleable", "0"); 1.280 + } 1.281 + // add the minidumps 1.282 + formData.append("upload_file_minidump", File(this.dump.path)); 1.283 + if (this.additionalDumps.length > 0) { 1.284 + let names = []; 1.285 + for (let i of this.additionalDumps) { 1.286 + names.push(i.name); 1.287 + formData.append("upload_file_minidump_"+i.name, 1.288 + File(i.dump.path)); 1.289 + } 1.290 + } 1.291 + 1.292 + let self = this; 1.293 + xhr.addEventListener("readystatechange", function (aEvt) { 1.294 + if (xhr.readyState == 4) { 1.295 + if (xhr.status != 200) { 1.296 + self.notifyStatus(FAILED); 1.297 + self.cleanup(); 1.298 + } else { 1.299 + let ret = parseKeyValuePairs(xhr.responseText); 1.300 + self.submitSuccess(ret); 1.301 + } 1.302 + } 1.303 + }, false); 1.304 + 1.305 + xhr.send(formData); 1.306 + return true; 1.307 + }, 1.308 + 1.309 + notifyStatus: function Submitter_notify(status, ret) 1.310 + { 1.311 + let propBag = Cc["@mozilla.org/hash-property-bag;1"]. 1.312 + createInstance(Ci.nsIWritablePropertyBag2); 1.313 + propBag.setPropertyAsAString("minidumpID", this.id); 1.314 + if (status == SUCCESS) { 1.315 + propBag.setPropertyAsAString("serverCrashID", ret.CrashID); 1.316 + } 1.317 + 1.318 + let extraKeyValsBag = Cc["@mozilla.org/hash-property-bag;1"]. 1.319 + createInstance(Ci.nsIWritablePropertyBag2); 1.320 + for (let key in this.extraKeyVals) { 1.321 + extraKeyValsBag.setPropertyAsAString(key, this.extraKeyVals[key]); 1.322 + } 1.323 + propBag.setPropertyAsInterface("extra", extraKeyValsBag); 1.324 + 1.325 + Services.obs.notifyObservers(propBag, "crash-report-status", status); 1.326 + 1.327 + switch (status) { 1.328 + case SUCCESS: 1.329 + if (this.successCallback) 1.330 + this.successCallback(this.id, ret); 1.331 + break; 1.332 + case FAILED: 1.333 + if (this.errorCallback) 1.334 + this.errorCallback(this.id); 1.335 + break; 1.336 + default: 1.337 + // no callbacks invoked. 1.338 + } 1.339 + }, 1.340 + 1.341 + submit: function Submitter_submit() 1.342 + { 1.343 + let [dump, extra] = getPendingMinidump(this.id); 1.344 + if (!dump.exists() || !extra.exists()) { 1.345 + this.notifyStatus(FAILED); 1.346 + this.cleanup(); 1.347 + return false; 1.348 + } 1.349 + 1.350 + let extraKeyVals = parseKeyValuePairsFromFile(extra); 1.351 + for (let key in extraKeyVals) { 1.352 + if (!(key in this.extraKeyVals)) { 1.353 + this.extraKeyVals[key] = extraKeyVals[key]; 1.354 + } 1.355 + } 1.356 + 1.357 + let additionalDumps = []; 1.358 + if ("additional_minidumps" in this.extraKeyVals) { 1.359 + let names = this.extraKeyVals.additional_minidumps.split(','); 1.360 + for (let name of names) { 1.361 + let [dump, extra] = getPendingMinidump(this.id + "-" + name); 1.362 + if (!dump.exists()) { 1.363 + this.notifyStatus(FAILED); 1.364 + this.cleanup(); 1.365 + return false; 1.366 + } 1.367 + additionalDumps.push({'name': name, 'dump': dump}); 1.368 + } 1.369 + } 1.370 + 1.371 + this.notifyStatus(SUBMITTING); 1.372 + 1.373 + this.dump = dump; 1.374 + this.extra = extra; 1.375 + this.additionalDumps = additionalDumps; 1.376 + 1.377 + if (!this.submitForm()) { 1.378 + this.notifyStatus(FAILED); 1.379 + this.cleanup(); 1.380 + return false; 1.381 + } 1.382 + return true; 1.383 + } 1.384 +}; 1.385 + 1.386 +//=================================== 1.387 +// External API goes here 1.388 +this.CrashSubmit = { 1.389 + /** 1.390 + * Submit the crash report named id.dmp from the "pending" directory. 1.391 + * 1.392 + * @param id 1.393 + * Filename (minus .dmp extension) of the minidump to submit. 1.394 + * @param params 1.395 + * An object containing any of the following optional parameters: 1.396 + * - submitSuccess 1.397 + * A function that will be called if the report is submitted 1.398 + * successfully with two parameters: the id that was passed 1.399 + * to this function, and an object containing the key/value 1.400 + * data returned from the server in its properties. 1.401 + * - submitError 1.402 + * A function that will be called with one parameter if the 1.403 + * report fails to submit: the id that was passed to this 1.404 + * function. 1.405 + * - noThrottle 1.406 + * If true, this crash report should be submitted with 1.407 + * an extra parameter of "Throttleable=0" indicating that 1.408 + * it should be processed right away. This should be set 1.409 + * when the report is being submitted and the user expects 1.410 + * to see the results immediately. Defaults to false. 1.411 + * - extraExtraKeyVals 1.412 + * An object whose key-value pairs will be merged with the data from 1.413 + * the ".extra" file submitted with the report. The properties of 1.414 + * this object will override properties of the same name in the 1.415 + * .extra file. 1.416 + * 1.417 + * @return true if the submission began successfully, or false if 1.418 + * it failed for some reason. (If the dump file does not 1.419 + * exist, for example.) 1.420 + */ 1.421 + submit: function CrashSubmit_submit(id, params) 1.422 + { 1.423 + params = params || {}; 1.424 + let submitSuccess = null; 1.425 + let submitError = null; 1.426 + let noThrottle = false; 1.427 + let extraExtraKeyVals = null; 1.428 + 1.429 + if ('submitSuccess' in params) 1.430 + submitSuccess = params.submitSuccess; 1.431 + if ('submitError' in params) 1.432 + submitError = params.submitError; 1.433 + if ('noThrottle' in params) 1.434 + noThrottle = params.noThrottle; 1.435 + if ('extraExtraKeyVals' in params) 1.436 + extraExtraKeyVals = params.extraExtraKeyVals; 1.437 + 1.438 + let submitter = new Submitter(id, 1.439 + submitSuccess, 1.440 + submitError, 1.441 + noThrottle, 1.442 + extraExtraKeyVals); 1.443 + CrashSubmit._activeSubmissions.push(submitter); 1.444 + return submitter.submit(); 1.445 + }, 1.446 + 1.447 + /** 1.448 + * Delete the minidup from the "pending" directory. 1.449 + * 1.450 + * @param id 1.451 + * Filename (minus .dmp extension) of the minidump to delete. 1.452 + */ 1.453 + delete: function CrashSubmit_delete(id) { 1.454 + let [dump, extra] = getPendingMinidump(id); 1.455 + dump.QueryInterface(Ci.nsIFile).remove(false); 1.456 + extra.QueryInterface(Ci.nsIFile).remove(false); 1.457 + }, 1.458 + 1.459 + /** 1.460 + * Get the list of pending crash IDs. 1.461 + * 1.462 + * @return an array of string, each being an ID as 1.463 + * expected to be passed to submit() 1.464 + */ 1.465 + pendingIDs: function CrashSubmit_pendingIDs() { 1.466 + return getAllPendingMinidumpsIDs(); 1.467 + }, 1.468 + 1.469 + /** 1.470 + * Prune the saved dumps. 1.471 + */ 1.472 + pruneSavedDumps: function CrashSubmit_pruneSavedDumps() { 1.473 + pruneSavedDumps(); 1.474 + }, 1.475 + 1.476 + // List of currently active submit objects 1.477 + _activeSubmissions: [] 1.478 +}; 1.479 + 1.480 +// Run this when first loaded 1.481 +getL10nStrings();