summaryrefslogtreecommitdiffstats
path: root/toolkit/crashreporter/CrashSubmit.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/crashreporter/CrashSubmit.jsm')
-rw-r--r--toolkit/crashreporter/CrashSubmit.jsm570
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: []
+};