diff options
Diffstat (limited to 'toolkit/crashreporter/CrashSubmit.jsm')
-rw-r--r-- | toolkit/crashreporter/CrashSubmit.jsm | 570 |
1 files changed, 570 insertions, 0 deletions
diff --git a/toolkit/crashreporter/CrashSubmit.jsm b/toolkit/crashreporter/CrashSubmit.jsm new file mode 100644 index 000000000..76eafbdad --- /dev/null +++ b/toolkit/crashreporter/CrashSubmit.jsm @@ -0,0 +1,570 @@ +/* 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/. */ + +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/KeyValueParser.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.importGlobalProperties(['File']); + +XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils", + "resource://gre/modules/PromiseUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "OS", + "resource://gre/modules/osfile.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); + +this.EXPORTED_SYMBOLS = [ + "CrashSubmit" +]; + +const STATE_START = Ci.nsIWebProgressListener.STATE_START; +const STATE_STOP = Ci.nsIWebProgressListener.STATE_STOP; + +const SUCCESS = "success"; +const FAILED = "failed"; +const SUBMITTING = "submitting"; + +const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +function parseINIStrings(file) { + var factory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"]. + getService(Ci.nsIINIParserFactory); + var parser = factory.createINIParser(file); + var obj = {}; + var en = parser.getKeys("Strings"); + while (en.hasMore()) { + var key = en.getNext(); + obj[key] = parser.getString("Strings", key); + } + return obj; +} + +// Since we're basically re-implementing part of the crashreporter +// client here, we'll just steal the strings we need from crashreporter.ini +function getL10nStrings() { + let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties); + let path = dirSvc.get("GreD", Ci.nsIFile); + path.append("crashreporter.ini"); + if (!path.exists()) { + // see if we're on a mac + path = path.parent; + path = path.parent; + path.append("MacOS"); + path.append("crashreporter.app"); + path.append("Contents"); + path.append("Resources"); + path.append("crashreporter.ini"); + if (!path.exists()) { + // very bad, but I don't know how to recover + return null; + } + } + let crstrings = parseINIStrings(path); + let strings = { + 'crashid': crstrings.CrashID, + 'reporturl': crstrings.CrashDetailsURL + }; + + path = dirSvc.get("XCurProcD", Ci.nsIFile); + path.append("crashreporter-override.ini"); + if (path.exists()) { + crstrings = parseINIStrings(path); + if ('CrashID' in crstrings) + strings['crashid'] = crstrings.CrashID; + if ('CrashDetailsURL' in crstrings) + strings['reporturl'] = crstrings.CrashDetailsURL; + } + return strings; +} + +XPCOMUtils.defineLazyGetter(this, "strings", getL10nStrings); + +function getDir(name) { + let directoryService = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties); + let dir = directoryService.get("UAppData", Ci.nsIFile); + dir.append("Crash Reports"); + dir.append(name); + return dir; +} + +function writeFile(dirName, fileName, data) { + let path = getDir(dirName); + if (!path.exists()) + path.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt('0700', 8)); + path.append(fileName); + var fs = Cc["@mozilla.org/network/file-output-stream;1"]. + createInstance(Ci.nsIFileOutputStream); + // open, write, truncate + fs.init(path, -1, -1, 0); + var os = Cc["@mozilla.org/intl/converter-output-stream;1"]. + createInstance(Ci.nsIConverterOutputStream); + os.init(fs, "UTF-8", 0, 0x0000); + os.writeString(data); + os.close(); + fs.close(); +} + +function getPendingMinidump(id) { + let pendingDir = getDir("pending"); + let dump = pendingDir.clone(); + let extra = pendingDir.clone(); + let memory = pendingDir.clone(); + dump.append(id + ".dmp"); + extra.append(id + ".extra"); + memory.append(id + ".memory.json.gz"); + return [dump, extra, memory]; +} + +function getAllPendingMinidumpsIDs() { + let minidumps = []; + let pendingDir = getDir("pending"); + + if (!(pendingDir.exists() && pendingDir.isDirectory())) + return []; + let entries = pendingDir.directoryEntries; + + while (entries.hasMoreElements()) { + let entry = entries.getNext().QueryInterface(Ci.nsIFile); + if (entry.isFile()) { + let matches = entry.leafName.match(/(.+)\.extra$/); + if (matches) + minidumps.push(matches[1]); + } + } + + return minidumps; +} + +function pruneSavedDumps() { + const KEEP = 10; + + let pendingDir = getDir("pending"); + if (!(pendingDir.exists() && pendingDir.isDirectory())) + return; + let entries = pendingDir.directoryEntries; + let entriesArray = []; + + while (entries.hasMoreElements()) { + let entry = entries.getNext().QueryInterface(Ci.nsIFile); + if (entry.isFile()) { + let matches = entry.leafName.match(/(.+)\.extra$/); + if (matches) + entriesArray.push(entry); + } + } + + entriesArray.sort(function(a, b) { + let dateA = a.lastModifiedTime; + let dateB = b.lastModifiedTime; + if (dateA < dateB) + return -1; + if (dateB < dateA) + return 1; + return 0; + }); + + if (entriesArray.length > KEEP) { + for (let i = 0; i < entriesArray.length - KEEP; ++i) { + let extra = entriesArray[i]; + let matches = extra.leafName.match(/(.+)\.extra$/); + if (matches) { + let dump = extra.clone(); + dump.leafName = matches[1] + '.dmp'; + dump.remove(false); + + let memory = extra.clone(); + memory.leafName = matches[1] + '.memory.json.gz'; + if (memory.exists()) { + memory.remove(false); + } + + extra.remove(false); + } + } + } +} + +function addFormEntry(doc, form, name, value) { + var input = doc.createElement("input"); + input.type = "hidden"; + input.name = name; + input.value = value; + form.appendChild(input); +} + +function writeSubmittedReport(crashID, viewURL) { + var data = strings.crashid.replace("%s", crashID); + if (viewURL) + data += "\n" + strings.reporturl.replace("%s", viewURL); + + writeFile("submitted", crashID + ".txt", data); +} + +// the Submitter class represents an individual submission. +function Submitter(id, recordSubmission, noThrottle, extraExtraKeyVals) { + this.id = id; + this.recordSubmission = recordSubmission; + this.noThrottle = noThrottle; + this.additionalDumps = []; + this.extraKeyVals = extraExtraKeyVals || {}; + this.deferredSubmit = PromiseUtils.defer(); +} + +Submitter.prototype = { + submitSuccess: function Submitter_submitSuccess(ret) + { + // Write out the details file to submitted/ + writeSubmittedReport(ret.CrashID, ret.ViewURL); + + // Delete from pending dir + try { + this.dump.remove(false); + this.extra.remove(false); + + if (this.memory) { + this.memory.remove(false); + } + + for (let i of this.additionalDumps) { + i.dump.remove(false); + } + } + catch (ex) { + // report an error? not much the user can do here. + } + + this.notifyStatus(SUCCESS, ret); + this.cleanup(); + }, + + cleanup: function Submitter_cleanup() { + // drop some references just to be nice + this.iframe = null; + this.dump = null; + this.extra = null; + this.memory = null; + this.additionalDumps = null; + // remove this object from the list of active submissions + let idx = CrashSubmit._activeSubmissions.indexOf(this); + if (idx != -1) + CrashSubmit._activeSubmissions.splice(idx, 1); + }, + + submitForm: function Submitter_submitForm() + { + if (!('ServerURL' in this.extraKeyVals)) { + return false; + } + let serverURL = this.extraKeyVals.ServerURL; + + // Override the submission URL from the environment + + var envOverride = Cc['@mozilla.org/process/environment;1']. + getService(Ci.nsIEnvironment).get("MOZ_CRASHREPORTER_URL"); + if (envOverride != '') { + serverURL = envOverride; + } + + let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] + .createInstance(Ci.nsIXMLHttpRequest); + xhr.open("POST", serverURL, true); + + let formData = Cc["@mozilla.org/files/formdata;1"] + .createInstance(Ci.nsIDOMFormData); + // add the data + for (let [name, value] of Object.entries(this.extraKeyVals)) { + if (name != "ServerURL") { + formData.append(name, value); + } + } + if (this.noThrottle) { + // tell the server not to throttle this, since it was manually submitted + formData.append("Throttleable", "0"); + } + // add the minidumps + formData.append("upload_file_minidump", File.createFromFileName(this.dump.path)); + if (this.memory) { + formData.append("memory_report", File.createFromFileName(this.memory.path)); + } + if (this.additionalDumps.length > 0) { + let names = []; + for (let i of this.additionalDumps) { + names.push(i.name); + formData.append("upload_file_minidump_"+i.name, + File.createFromFileName(i.dump.path)); + } + } + + let manager = Services.crashmanager; + let submissionID = manager.generateSubmissionID(); + + xhr.addEventListener("readystatechange", (evt) => { + if (xhr.readyState == 4) { + let ret = + xhr.status == 200 ? parseKeyValuePairs(xhr.responseText) : {}; + let submitted = !!ret.CrashID; + + if (this.recordSubmission) { + let result = submitted ? manager.SUBMISSION_RESULT_OK : + manager.SUBMISSION_RESULT_FAILED; + manager.addSubmissionResult(this.id, submissionID, new Date(), + result); + if (submitted) { + manager.setRemoteCrashID(this.id, ret.CrashID); + } + } + + if (submitted) { + this.submitSuccess(ret); + } + else { + this.notifyStatus(FAILED); + this.cleanup(); + } + } + }, false); + + if (this.recordSubmission) { + manager.addSubmissionAttempt(this.id, submissionID, new Date()); + } + xhr.send(formData); + return true; + }, + + notifyStatus: function Submitter_notify(status, ret) + { + let propBag = Cc["@mozilla.org/hash-property-bag;1"]. + createInstance(Ci.nsIWritablePropertyBag2); + propBag.setPropertyAsAString("minidumpID", this.id); + if (status == SUCCESS) { + propBag.setPropertyAsAString("serverCrashID", ret.CrashID); + } + + let extraKeyValsBag = Cc["@mozilla.org/hash-property-bag;1"]. + createInstance(Ci.nsIWritablePropertyBag2); + for (let key in this.extraKeyVals) { + extraKeyValsBag.setPropertyAsAString(key, this.extraKeyVals[key]); + } + propBag.setPropertyAsInterface("extra", extraKeyValsBag); + + Services.obs.notifyObservers(propBag, "crash-report-status", status); + + switch (status) { + case SUCCESS: + this.deferredSubmit.resolve(ret.CrashID); + break; + case FAILED: + this.deferredSubmit.reject(); + break; + default: + // no callbacks invoked. + } + }, + + submit: function Submitter_submit() + { + let [dump, extra, memory] = getPendingMinidump(this.id); + + if (!dump.exists() || !extra.exists()) { + this.notifyStatus(FAILED); + this.cleanup(); + return this.deferredSubmit.promise; + } + this.dump = dump; + this.extra = extra; + + // The memory file may or may not exist + if (memory.exists()) { + this.memory = memory; + } + + let extraKeyVals = parseKeyValuePairsFromFile(extra); + for (let key in extraKeyVals) { + if (!(key in this.extraKeyVals)) { + this.extraKeyVals[key] = extraKeyVals[key]; + } + } + + let additionalDumps = []; + if ("additional_minidumps" in this.extraKeyVals) { + let names = this.extraKeyVals.additional_minidumps.split(','); + for (let name of names) { + let [dump, extra, memory] = getPendingMinidump(this.id + "-" + name); + if (!dump.exists()) { + this.notifyStatus(FAILED); + this.cleanup(); + return this.deferredSubmit.promise; + } + additionalDumps.push({'name': name, 'dump': dump}); + } + } + + this.notifyStatus(SUBMITTING); + + this.additionalDumps = additionalDumps; + + if (!this.submitForm()) { + this.notifyStatus(FAILED); + this.cleanup(); + } + return this.deferredSubmit.promise; + } +}; + +// =================================== +// External API goes here +this.CrashSubmit = { + /** + * Submit the crash report named id.dmp from the "pending" directory. + * + * @param id + * Filename (minus .dmp extension) of the minidump to submit. + * @param params + * An object containing any of the following optional parameters: + * - recordSubmission + * If true, a submission event is recorded in CrashManager. + * - noThrottle + * If true, this crash report should be submitted with + * an extra parameter of "Throttleable=0" indicating that + * it should be processed right away. This should be set + * when the report is being submitted and the user expects + * to see the results immediately. Defaults to false. + * - extraExtraKeyVals + * An object whose key-value pairs will be merged with the data from + * the ".extra" file submitted with the report. The properties of + * this object will override properties of the same name in the + * .extra file. + * + * @return a Promise that is fulfilled with the server crash ID when the + * submission succeeds and rejected otherwise. + */ + submit: function CrashSubmit_submit(id, params) + { + params = params || {}; + let recordSubmission = false; + let submitSuccess = null; + let submitError = null; + let noThrottle = false; + let extraExtraKeyVals = null; + + if ('recordSubmission' in params) + recordSubmission = params.recordSubmission; + if ('noThrottle' in params) + noThrottle = params.noThrottle; + if ('extraExtraKeyVals' in params) + extraExtraKeyVals = params.extraExtraKeyVals; + + let submitter = new Submitter(id, recordSubmission, + noThrottle, extraExtraKeyVals); + CrashSubmit._activeSubmissions.push(submitter); + return submitter.submit(); + }, + + /** + * Delete the minidup from the "pending" directory. + * + * @param id + * Filename (minus .dmp extension) of the minidump to delete. + */ + delete: function CrashSubmit_delete(id) { + let [dump, extra, memory] = getPendingMinidump(id); + dump.remove(false); + extra.remove(false); + if (memory.exists()) { + memory.remove(false); + } + }, + + /** + * Add a .dmg.ignore file along side the .dmp file to indicate that the user + * shouldn't be prompted to submit this crash report again. + * + * @param id + * Filename (minus .dmp extension) of the report to ignore + */ + + ignore: function CrashSubmit_ignore(id) { + let [dump, extra, mem] = getPendingMinidump(id); + return OS.File.open(dump.path + ".ignore", {create: true}, + {unixFlags: OS.Constants.libc.O_CREAT}) + .then((file) => { file.close(); }); + }, + + /** + * Get the list of pending crash IDs. + * + * @return an array of string, each being an ID as + * expected to be passed to submit() + */ + pendingIDs: function CrashSubmit_pendingIDs() { + return getAllPendingMinidumpsIDs(); + }, + + /** + * Get the list of pending crash IDs, excluding those marked to be ignored + * @param maxFileDate + * A Date object. Any files last modified before that date will be ignored + * + * @return a Promise that is fulfilled with an array of string, each + * being an ID as expected to be passed to submit() or ignore() + */ + pendingIDsAsync: Task.async(function* CrashSubmit_pendingIDsAsync(maxFileDate) { + let ids = []; + let info = null; + try { + info = yield OS.File.stat(getDir("pending").path) + } catch (ex) { + /* pending dir doesn't exist, ignore */ + return ids; + } + + if (info.isDir) { + let iterator = new OS.File.DirectoryIterator(getDir("pending").path); + try { + yield iterator.forEach( + function onEntry(file) { + if (file.name.endsWith(".dmp")) { + return OS.File.exists(file.path + ".ignore") + .then(ignoreExists => { + if (!ignoreExists) { + let id = file.name.slice(0, -4); + if (UUID_REGEX.test(id)) { + return OS.File.stat(file.path) + .then(info => { + if (info.lastAccessDate.valueOf() > + maxFileDate.valueOf()) { + ids.push(id); + } + }); + } + } + return null; + }); + } + return null; + } + ); + } catch (ex) { + Cu.reportError(ex); + } finally { + iterator.close(); + } + } + return ids; + }), + + /** + * Prune the saved dumps. + */ + pruneSavedDumps: function CrashSubmit_pruneSavedDumps() { + pruneSavedDumps(); + }, + + // List of currently active submit objects + _activeSubmissions: [] +}; |