michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: Components.utils.import("resource://gre/modules/Services.jsm"); michael@0: Components.utils.import("resource://gre/modules/KeyValueParser.jsm"); michael@0: michael@0: this.EXPORTED_SYMBOLS = [ michael@0: "CrashSubmit" michael@0: ]; michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const STATE_START = Ci.nsIWebProgressListener.STATE_START; michael@0: const STATE_STOP = Ci.nsIWebProgressListener.STATE_STOP; michael@0: michael@0: const SUCCESS = "success"; michael@0: const FAILED = "failed"; michael@0: const SUBMITTING = "submitting"; michael@0: michael@0: let reportURL = null; michael@0: let strings = null; michael@0: let myListener = null; michael@0: michael@0: function parseINIStrings(file) { michael@0: var factory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"]. michael@0: getService(Ci.nsIINIParserFactory); michael@0: var parser = factory.createINIParser(file); michael@0: var obj = {}; michael@0: var en = parser.getKeys("Strings"); michael@0: while (en.hasMore()) { michael@0: var key = en.getNext(); michael@0: obj[key] = parser.getString("Strings", key); michael@0: } michael@0: return obj; michael@0: } michael@0: michael@0: // Since we're basically re-implementing part of the crashreporter michael@0: // client here, we'll just steal the strings we need from crashreporter.ini michael@0: function getL10nStrings() { michael@0: let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. michael@0: getService(Ci.nsIProperties); michael@0: let path = dirSvc.get("GreD", Ci.nsIFile); michael@0: path.append("crashreporter.ini"); michael@0: if (!path.exists()) { michael@0: // see if we're on a mac michael@0: path = path.parent; michael@0: path.append("crashreporter.app"); michael@0: path.append("Contents"); michael@0: path.append("MacOS"); michael@0: path.append("crashreporter.ini"); michael@0: if (!path.exists()) { michael@0: // very bad, but I don't know how to recover michael@0: return; michael@0: } michael@0: } michael@0: let crstrings = parseINIStrings(path); michael@0: strings = { michael@0: 'crashid': crstrings.CrashID, michael@0: 'reporturl': crstrings.CrashDetailsURL michael@0: }; michael@0: michael@0: path = dirSvc.get("XCurProcD", Ci.nsIFile); michael@0: path.append("crashreporter-override.ini"); michael@0: if (path.exists()) { michael@0: crstrings = parseINIStrings(path); michael@0: if ('CrashID' in crstrings) michael@0: strings['crashid'] = crstrings.CrashID; michael@0: if ('CrashDetailsURL' in crstrings) michael@0: strings['reporturl'] = crstrings.CrashDetailsURL; michael@0: } michael@0: } michael@0: michael@0: function getPendingDir() { michael@0: let directoryService = Cc["@mozilla.org/file/directory_service;1"]. michael@0: getService(Ci.nsIProperties); michael@0: let pendingDir = directoryService.get("UAppData", Ci.nsIFile); michael@0: pendingDir.append("Crash Reports"); michael@0: pendingDir.append("pending"); michael@0: return pendingDir; michael@0: } michael@0: michael@0: function getPendingMinidump(id) { michael@0: let pendingDir = getPendingDir(); michael@0: let dump = pendingDir.clone(); michael@0: let extra = pendingDir.clone(); michael@0: dump.append(id + ".dmp"); michael@0: extra.append(id + ".extra"); michael@0: return [dump, extra]; michael@0: } michael@0: michael@0: function getAllPendingMinidumpsIDs() { michael@0: let minidumps = []; michael@0: let pendingDir = getPendingDir(); michael@0: michael@0: if (!(pendingDir.exists() && pendingDir.isDirectory())) michael@0: return []; michael@0: let entries = pendingDir.directoryEntries; michael@0: michael@0: while (entries.hasMoreElements()) { michael@0: let entry = entries.getNext().QueryInterface(Ci.nsIFile); michael@0: if (entry.isFile()) { michael@0: let matches = entry.leafName.match(/(.+)\.extra$/); michael@0: if (matches) michael@0: minidumps.push(matches[1]); michael@0: } michael@0: } michael@0: michael@0: return minidumps; michael@0: } michael@0: michael@0: function pruneSavedDumps() { michael@0: const KEEP = 10; michael@0: michael@0: let pendingDir = getPendingDir(); michael@0: if (!(pendingDir.exists() && pendingDir.isDirectory())) michael@0: return; michael@0: let entries = pendingDir.directoryEntries; michael@0: let entriesArray = []; michael@0: michael@0: while (entries.hasMoreElements()) { michael@0: let entry = entries.getNext().QueryInterface(Ci.nsIFile); michael@0: if (entry.isFile()) { michael@0: let matches = entry.leafName.match(/(.+)\.extra$/); michael@0: if (matches) michael@0: entriesArray.push(entry); michael@0: } michael@0: } michael@0: michael@0: entriesArray.sort(function(a,b) { michael@0: let dateA = a.lastModifiedTime; michael@0: let dateB = b.lastModifiedTime; michael@0: if (dateA < dateB) michael@0: return -1; michael@0: if (dateB < dateA) michael@0: return 1; michael@0: return 0; michael@0: }); michael@0: michael@0: if (entriesArray.length > KEEP) { michael@0: for (let i = 0; i < entriesArray.length - KEEP; ++i) { michael@0: let extra = entriesArray[i]; michael@0: let matches = extra.leafName.match(/(.+)\.extra$/); michael@0: if (matches) { michael@0: let dump = extra.clone(); michael@0: dump.leafName = matches[1] + '.dmp'; michael@0: dump.remove(false); michael@0: extra.remove(false); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: function addFormEntry(doc, form, name, value) { michael@0: var input = doc.createElement("input"); michael@0: input.type = "hidden"; michael@0: input.name = name; michael@0: input.value = value; michael@0: form.appendChild(input); michael@0: } michael@0: michael@0: function writeSubmittedReport(crashID, viewURL) { michael@0: let directoryService = Cc["@mozilla.org/file/directory_service;1"]. michael@0: getService(Ci.nsIProperties); michael@0: let reportFile = directoryService.get("UAppData", Ci.nsIFile); michael@0: reportFile.append("Crash Reports"); michael@0: reportFile.append("submitted"); michael@0: if (!reportFile.exists()) michael@0: reportFile.create(Ci.nsIFile.DIRECTORY_TYPE, 0700); michael@0: reportFile.append(crashID + ".txt"); michael@0: var fstream = Cc["@mozilla.org/network/file-output-stream;1"]. michael@0: createInstance(Ci.nsIFileOutputStream); michael@0: // open, write, truncate michael@0: fstream.init(reportFile, -1, -1, 0); michael@0: var os = Cc["@mozilla.org/intl/converter-output-stream;1"]. michael@0: createInstance(Ci.nsIConverterOutputStream); michael@0: os.init(fstream, "UTF-8", 0, 0x0000); michael@0: michael@0: var data = strings.crashid.replace("%s", crashID); michael@0: if (viewURL) michael@0: data += "\n" + strings.reporturl.replace("%s", viewURL); michael@0: michael@0: os.writeString(data); michael@0: os.close(); michael@0: fstream.close(); michael@0: } michael@0: michael@0: // the Submitter class represents an individual submission. michael@0: function Submitter(id, submitSuccess, submitError, noThrottle, michael@0: extraExtraKeyVals) { michael@0: this.id = id; michael@0: this.successCallback = submitSuccess; michael@0: this.errorCallback = submitError; michael@0: this.noThrottle = noThrottle; michael@0: this.additionalDumps = []; michael@0: this.extraKeyVals = extraExtraKeyVals || {}; michael@0: } michael@0: michael@0: Submitter.prototype = { michael@0: submitSuccess: function Submitter_submitSuccess(ret) michael@0: { michael@0: if (!ret.CrashID) { michael@0: this.notifyStatus(FAILED); michael@0: this.cleanup(); michael@0: return; michael@0: } michael@0: michael@0: // Write out the details file to submitted/ michael@0: writeSubmittedReport(ret.CrashID, ret.ViewURL); michael@0: michael@0: // Delete from pending dir michael@0: try { michael@0: this.dump.remove(false); michael@0: this.extra.remove(false); michael@0: for (let i of this.additionalDumps) { michael@0: i.dump.remove(false); michael@0: } michael@0: } michael@0: catch (ex) { michael@0: // report an error? not much the user can do here. michael@0: } michael@0: michael@0: this.notifyStatus(SUCCESS, ret); michael@0: this.cleanup(); michael@0: }, michael@0: michael@0: cleanup: function Submitter_cleanup() { michael@0: // drop some references just to be nice michael@0: this.successCallback = null; michael@0: this.errorCallback = null; michael@0: this.iframe = null; michael@0: this.dump = null; michael@0: this.extra = null; michael@0: this.additionalDumps = null; michael@0: // remove this object from the list of active submissions michael@0: let idx = CrashSubmit._activeSubmissions.indexOf(this); michael@0: if (idx != -1) michael@0: CrashSubmit._activeSubmissions.splice(idx, 1); michael@0: }, michael@0: michael@0: submitForm: function Submitter_submitForm() michael@0: { michael@0: if (!('ServerURL' in this.extraKeyVals)) { michael@0: return false; michael@0: } michael@0: let serverURL = this.extraKeyVals.ServerURL; michael@0: michael@0: // Override the submission URL from the environment or prefs. michael@0: michael@0: var envOverride = Cc['@mozilla.org/process/environment;1']. michael@0: getService(Ci.nsIEnvironment).get("MOZ_CRASHREPORTER_URL"); michael@0: if (envOverride != '') { michael@0: serverURL = envOverride; michael@0: } michael@0: else if ('PluginHang' in this.extraKeyVals) { michael@0: try { michael@0: serverURL = Services.prefs. michael@0: getCharPref("toolkit.crashreporter.pluginHangSubmitURL"); michael@0: } catch(e) { } michael@0: } michael@0: michael@0: let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] michael@0: .createInstance(Ci.nsIXMLHttpRequest); michael@0: xhr.open("POST", serverURL, true); michael@0: michael@0: let formData = Cc["@mozilla.org/files/formdata;1"] michael@0: .createInstance(Ci.nsIDOMFormData); michael@0: // add the data michael@0: for (let [name, value] in Iterator(this.extraKeyVals)) { michael@0: if (name != "ServerURL") { michael@0: formData.append(name, value); michael@0: } michael@0: } michael@0: if (this.noThrottle) { michael@0: // tell the server not to throttle this, since it was manually submitted michael@0: formData.append("Throttleable", "0"); michael@0: } michael@0: // add the minidumps michael@0: formData.append("upload_file_minidump", File(this.dump.path)); michael@0: if (this.additionalDumps.length > 0) { michael@0: let names = []; michael@0: for (let i of this.additionalDumps) { michael@0: names.push(i.name); michael@0: formData.append("upload_file_minidump_"+i.name, michael@0: File(i.dump.path)); michael@0: } michael@0: } michael@0: michael@0: let self = this; michael@0: xhr.addEventListener("readystatechange", function (aEvt) { michael@0: if (xhr.readyState == 4) { michael@0: if (xhr.status != 200) { michael@0: self.notifyStatus(FAILED); michael@0: self.cleanup(); michael@0: } else { michael@0: let ret = parseKeyValuePairs(xhr.responseText); michael@0: self.submitSuccess(ret); michael@0: } michael@0: } michael@0: }, false); michael@0: michael@0: xhr.send(formData); michael@0: return true; michael@0: }, michael@0: michael@0: notifyStatus: function Submitter_notify(status, ret) michael@0: { michael@0: let propBag = Cc["@mozilla.org/hash-property-bag;1"]. michael@0: createInstance(Ci.nsIWritablePropertyBag2); michael@0: propBag.setPropertyAsAString("minidumpID", this.id); michael@0: if (status == SUCCESS) { michael@0: propBag.setPropertyAsAString("serverCrashID", ret.CrashID); michael@0: } michael@0: michael@0: let extraKeyValsBag = Cc["@mozilla.org/hash-property-bag;1"]. michael@0: createInstance(Ci.nsIWritablePropertyBag2); michael@0: for (let key in this.extraKeyVals) { michael@0: extraKeyValsBag.setPropertyAsAString(key, this.extraKeyVals[key]); michael@0: } michael@0: propBag.setPropertyAsInterface("extra", extraKeyValsBag); michael@0: michael@0: Services.obs.notifyObservers(propBag, "crash-report-status", status); michael@0: michael@0: switch (status) { michael@0: case SUCCESS: michael@0: if (this.successCallback) michael@0: this.successCallback(this.id, ret); michael@0: break; michael@0: case FAILED: michael@0: if (this.errorCallback) michael@0: this.errorCallback(this.id); michael@0: break; michael@0: default: michael@0: // no callbacks invoked. michael@0: } michael@0: }, michael@0: michael@0: submit: function Submitter_submit() michael@0: { michael@0: let [dump, extra] = getPendingMinidump(this.id); michael@0: if (!dump.exists() || !extra.exists()) { michael@0: this.notifyStatus(FAILED); michael@0: this.cleanup(); michael@0: return false; michael@0: } michael@0: michael@0: let extraKeyVals = parseKeyValuePairsFromFile(extra); michael@0: for (let key in extraKeyVals) { michael@0: if (!(key in this.extraKeyVals)) { michael@0: this.extraKeyVals[key] = extraKeyVals[key]; michael@0: } michael@0: } michael@0: michael@0: let additionalDumps = []; michael@0: if ("additional_minidumps" in this.extraKeyVals) { michael@0: let names = this.extraKeyVals.additional_minidumps.split(','); michael@0: for (let name of names) { michael@0: let [dump, extra] = getPendingMinidump(this.id + "-" + name); michael@0: if (!dump.exists()) { michael@0: this.notifyStatus(FAILED); michael@0: this.cleanup(); michael@0: return false; michael@0: } michael@0: additionalDumps.push({'name': name, 'dump': dump}); michael@0: } michael@0: } michael@0: michael@0: this.notifyStatus(SUBMITTING); michael@0: michael@0: this.dump = dump; michael@0: this.extra = extra; michael@0: this.additionalDumps = additionalDumps; michael@0: michael@0: if (!this.submitForm()) { michael@0: this.notifyStatus(FAILED); michael@0: this.cleanup(); michael@0: return false; michael@0: } michael@0: return true; michael@0: } michael@0: }; michael@0: michael@0: //=================================== michael@0: // External API goes here michael@0: this.CrashSubmit = { michael@0: /** michael@0: * Submit the crash report named id.dmp from the "pending" directory. michael@0: * michael@0: * @param id michael@0: * Filename (minus .dmp extension) of the minidump to submit. michael@0: * @param params michael@0: * An object containing any of the following optional parameters: michael@0: * - submitSuccess michael@0: * A function that will be called if the report is submitted michael@0: * successfully with two parameters: the id that was passed michael@0: * to this function, and an object containing the key/value michael@0: * data returned from the server in its properties. michael@0: * - submitError michael@0: * A function that will be called with one parameter if the michael@0: * report fails to submit: the id that was passed to this michael@0: * function. michael@0: * - noThrottle michael@0: * If true, this crash report should be submitted with michael@0: * an extra parameter of "Throttleable=0" indicating that michael@0: * it should be processed right away. This should be set michael@0: * when the report is being submitted and the user expects michael@0: * to see the results immediately. Defaults to false. michael@0: * - extraExtraKeyVals michael@0: * An object whose key-value pairs will be merged with the data from michael@0: * the ".extra" file submitted with the report. The properties of michael@0: * this object will override properties of the same name in the michael@0: * .extra file. michael@0: * michael@0: * @return true if the submission began successfully, or false if michael@0: * it failed for some reason. (If the dump file does not michael@0: * exist, for example.) michael@0: */ michael@0: submit: function CrashSubmit_submit(id, params) michael@0: { michael@0: params = params || {}; michael@0: let submitSuccess = null; michael@0: let submitError = null; michael@0: let noThrottle = false; michael@0: let extraExtraKeyVals = null; michael@0: michael@0: if ('submitSuccess' in params) michael@0: submitSuccess = params.submitSuccess; michael@0: if ('submitError' in params) michael@0: submitError = params.submitError; michael@0: if ('noThrottle' in params) michael@0: noThrottle = params.noThrottle; michael@0: if ('extraExtraKeyVals' in params) michael@0: extraExtraKeyVals = params.extraExtraKeyVals; michael@0: michael@0: let submitter = new Submitter(id, michael@0: submitSuccess, michael@0: submitError, michael@0: noThrottle, michael@0: extraExtraKeyVals); michael@0: CrashSubmit._activeSubmissions.push(submitter); michael@0: return submitter.submit(); michael@0: }, michael@0: michael@0: /** michael@0: * Delete the minidup from the "pending" directory. michael@0: * michael@0: * @param id michael@0: * Filename (minus .dmp extension) of the minidump to delete. michael@0: */ michael@0: delete: function CrashSubmit_delete(id) { michael@0: let [dump, extra] = getPendingMinidump(id); michael@0: dump.QueryInterface(Ci.nsIFile).remove(false); michael@0: extra.QueryInterface(Ci.nsIFile).remove(false); michael@0: }, michael@0: michael@0: /** michael@0: * Get the list of pending crash IDs. michael@0: * michael@0: * @return an array of string, each being an ID as michael@0: * expected to be passed to submit() michael@0: */ michael@0: pendingIDs: function CrashSubmit_pendingIDs() { michael@0: return getAllPendingMinidumpsIDs(); michael@0: }, michael@0: michael@0: /** michael@0: * Prune the saved dumps. michael@0: */ michael@0: pruneSavedDumps: function CrashSubmit_pruneSavedDumps() { michael@0: pruneSavedDumps(); michael@0: }, michael@0: michael@0: // List of currently active submit objects michael@0: _activeSubmissions: [] michael@0: }; michael@0: michael@0: // Run this when first loaded michael@0: getL10nStrings();