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/. */
5 const SUCCESS = "success";
6 const FAILED = "failed";
7 const SUBMITTING = "submitting";
10 /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
11 const SUBMISSION_REGEX =
12 /^bp-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
14 function getDir(name) {
15 let uAppDataPath = Services.dirsvc.get("UAppData", Ci.nsIFile).path;
16 return PathUtils.join(uAppDataPath, "Crash Reports", name);
19 async function writeFileAsync(dirName, fileName, data) {
20 let dirPath = getDir(dirName);
21 let filePath = PathUtils.join(dirPath, fileName);
22 await IOUtils.makeDirectory(dirPath, { permissions: 0o700 });
23 await IOUtils.writeUTF8(filePath, data);
26 function getPendingMinidump(id) {
27 let pendingDir = getDir("pending");
29 return [".dmp", ".extra", ".memory.json.gz"].map(suffix => {
30 return PathUtils.join(pendingDir, `${id}${suffix}`);
34 async function writeSubmittedReportAsync(crashID, viewURL) {
35 // Since we're basically re-implementing (with async) part of the
36 // crashreporter client here, we'll use the strings we need from the
37 // crashreporter fluent file.
38 const l10n = new Localization(["crashreporter/crashreporter.ftl"]);
39 let data = await l10n.formatValue("crashreporter-crash-identifier", {
45 (await l10n.formatValue("crashreporter-crash-details", { url: viewURL }));
48 await writeFileAsync("submitted", `${crashID}.txt`, data);
51 // the Submitter class represents an individual submission.
52 function Submitter(id, recordSubmission, noThrottle, extraExtraKeyVals) {
54 this.recordSubmission = recordSubmission;
55 this.noThrottle = noThrottle;
56 this.additionalDumps = [];
57 this.extraKeyVals = extraExtraKeyVals;
58 // mimic deferred Promise behavior
59 this.submitStatusPromise = new Promise((resolve, reject) => {
60 this.resolveSubmitStatusPromise = resolve;
61 this.rejectSubmitStatusPromise = reject;
65 Submitter.prototype = {
66 submitSuccess: async function Submitter_submitSuccess(ret) {
67 // Write out the details file to submitted
68 await writeSubmittedReportAsync(ret.CrashID, ret.ViewURL);
71 let toDelete = [this.dump, this.extra];
74 toDelete.push(this.memory);
77 for (let entry of this.additionalDumps) {
78 toDelete.push(entry.dump);
82 toDelete.map(path => {
83 return IOUtils.remove(path, { ignoreAbsent: true });
90 this.notifyStatus(SUCCESS, ret);
94 cleanup: function Submitter_cleanup() {
95 // drop some references just to be nice
100 this.additionalDumps = null;
101 // remove this object from the list of active submissions
102 let idx = CrashSubmit._activeSubmissions.indexOf(this);
104 CrashSubmit._activeSubmissions.splice(idx, 1);
108 parseResponse: function Submitter_parseResponse(response) {
109 let parsedResponse = {};
111 for (let line of response.split("\n")) {
112 let data = line.split("=");
116 data[0] == "CrashID" &&
117 SUBMISSION_REGEX.test(data[1])) ||
120 parsedResponse[data[0]] = data[1];
124 return parsedResponse;
127 submitForm: function Submitter_submitForm() {
128 if (!("ServerURL" in this.extraKeyVals)) {
131 let serverURL = this.extraKeyVals.ServerURL;
132 delete this.extraKeyVals.ServerURL;
134 // Override the submission URL from the environment
135 let envOverride = Services.env.get("MOZ_CRASHREPORTER_URL");
136 if (envOverride != "") {
137 serverURL = envOverride;
140 let xhr = new XMLHttpRequest();
141 xhr.open("POST", serverURL, true);
143 let formData = new FormData();
145 // tell the server not to throttle this if requested
146 this.extraKeyVals.Throttleable = this.noThrottle ? "0" : "1";
149 let payload = Object.assign({}, this.extraKeyVals);
150 let json = new Blob([JSON.stringify(payload)], {
151 type: "application/json",
153 formData.append("extra", json);
157 File.createFromFileName(this.dump, {
158 type: "application/octet-stream",
160 formData.append("upload_file_minidump", file);
166 File.createFromFileName(this.memory, {
167 type: "application/gzip",
169 formData.append("memory_report", file);
174 if (this.additionalDumps.length) {
176 for (let i of this.additionalDumps) {
179 File.createFromFileName(i.dump, {
180 type: "application/octet-stream",
182 formData.append("upload_file_minidump_" + i.name, file);
188 let manager = Services.crashmanager;
189 let submissionID = manager.generateSubmissionID();
191 xhr.addEventListener("readystatechange", () => {
192 if (xhr.readyState == 4) {
194 xhr.status === 200 ? this.parseResponse(xhr.responseText) : {};
195 let submitted = !!ret.CrashID;
196 let p = Promise.resolve();
198 if (this.recordSubmission) {
199 let result = submitted
200 ? manager.SUBMISSION_RESULT_OK
201 : manager.SUBMISSION_RESULT_FAILED;
202 p = manager.addSubmissionResult(
209 manager.setRemoteCrashID(this.id, ret.CrashID);
215 this.submitSuccess(ret);
217 this.notifyStatus(FAILED);
224 let p = Promise.all(promises);
227 if (this.recordSubmission) {
229 return manager.addSubmissionAttempt(id, submissionID, new Date());
238 notifyStatus: function Submitter_notify(status, ret) {
239 let propBag = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
240 Ci.nsIWritablePropertyBag2
242 propBag.setPropertyAsAString("minidumpID", this.id);
243 if (status == SUCCESS) {
244 propBag.setPropertyAsAString("serverCrashID", ret.CrashID);
247 let extraKeyValsBag = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
248 Ci.nsIWritablePropertyBag2
250 for (let key in this.extraKeyVals) {
251 extraKeyValsBag.setPropertyAsAString(key, this.extraKeyVals[key]);
253 propBag.setPropertyAsInterface("extra", extraKeyValsBag);
255 Services.obs.notifyObservers(propBag, "crash-report-status", status);
259 this.resolveSubmitStatusPromise(ret.CrashID);
262 this.rejectSubmitStatusPromise(FAILED);
265 // no callbacks invoked.
269 readAnnotations: async function Submitter_readAnnotations(extra) {
270 // These annotations are used only by the crash reporter client and should
271 // not be submitted to Socorro.
272 const strippedAnnotations = [
275 "TelemetrySessionId",
276 "TelemetryServerURL",
278 let extraKeyVals = await IOUtils.readJSON(extra);
280 this.extraKeyVals = { ...extraKeyVals, ...this.extraKeyVals };
281 strippedAnnotations.forEach(key => delete this.extraKeyVals[key]);
284 submit: async function Submitter_submit() {
285 if (this.recordSubmission) {
286 await Services.crashmanager.ensureCrashIsPresent(this.id);
289 let [dump, extra, memory] = getPendingMinidump(this.id);
290 let [dumpExists, extraExists, memoryExists] = await Promise.all([
291 IOUtils.exists(dump),
292 IOUtils.exists(extra),
293 IOUtils.exists(memory),
296 if (!dumpExists || !extraExists) {
297 this.notifyStatus(FAILED);
299 return this.submitStatusPromise;
304 this.memory = memoryExists ? memory : null;
305 await this.readAnnotations(extra);
307 let additionalDumps = [];
309 if ("additional_minidumps" in this.extraKeyVals) {
310 let dumpsExistsPromises = [];
311 let names = this.extraKeyVals.additional_minidumps.split(",");
313 for (let name of names) {
314 let [dump /* , extra, memory */] = getPendingMinidump(
318 dumpsExistsPromises.push(IOUtils.exists(dump));
319 additionalDumps.push({ name, dump });
322 let dumpsExist = await Promise.all(dumpsExistsPromises);
323 let allDumpsExist = dumpsExist.every(exists => exists);
325 if (!allDumpsExist) {
326 this.notifyStatus(FAILED);
328 return this.submitStatusPromise;
332 this.notifyStatus(SUBMITTING);
333 this.additionalDumps = additionalDumps;
335 if (!(await this.submitForm())) {
336 this.notifyStatus(FAILED);
340 return this.submitStatusPromise;
344 // ===================================
345 // External API goes here
346 export var CrashSubmit = {
347 // A set of strings representing how a user subnmitted a given crash
348 SUBMITTED_FROM_AUTO: "Auto",
349 SUBMITTED_FROM_INFOBAR: "Infobar",
350 SUBMITTED_FROM_ABOUT_CRASHES: "AboutCrashes",
351 SUBMITTED_FROM_CRASH_TAB: "CrashedTab",
354 * Submit the crash report named id.dmp from the "pending" directory.
357 * Filename (minus .dmp extension) of the minidump to submit.
358 * @param submittedFrom
359 * One of the SUBMITTED_FROM_* constants representing how the
360 * user submitted this crash.
362 * An object containing any of the following optional parameters:
364 * If true, a submission event is recorded in CrashManager.
366 * If true, this crash report should be submitted with
367 * the Throttleable annotation set to "0" indicating that
368 * it should be processed right away. This should be set
369 * when the report is being submitted and the user expects
370 * to see the results immediately. Defaults to false.
371 * - extraExtraKeyVals
372 * An object whose key-value pairs will be merged with the data from
373 * the ".extra" file submitted with the report. The properties of
374 * this object will override properties of the same name in the
377 * @return a Promise that is fulfilled with the server crash ID when the
378 * submission succeeds and rejected otherwise.
380 submit: function CrashSubmit_submit(id, submittedFrom, params) {
381 params = params || {};
382 let recordSubmission = false;
383 let noThrottle = false;
384 let extraExtraKeyVals = {};
386 if ("recordSubmission" in params) {
387 recordSubmission = params.recordSubmission;
390 if ("noThrottle" in params) {
391 noThrottle = params.noThrottle;
394 if ("extraExtraKeyVals" in params) {
395 extraExtraKeyVals = params.extraExtraKeyVals;
398 extraExtraKeyVals.SubmittedFrom = submittedFrom;
400 let submitter = new Submitter(
406 CrashSubmit._activeSubmissions.push(submitter);
407 return submitter.submit();
411 * Delete the minidup from the "pending" directory.
414 * Filename (minus .dmp extension) of the minidump to delete.
416 * @return a Promise that is fulfilled when the minidump is deleted and
419 delete: async function CrashSubmit_delete(id) {
421 getPendingMinidump(id).map(path => {
422 return IOUtils.remove(path);
428 * Add a .dmg.ignore file along side the .dmp file to indicate that the user
429 * shouldn't be prompted to submit this crash report again.
432 * Filename (minus .dmp extension) of the report to ignore
434 * @return a Promise that is fulfilled when (if) the .dmg.ignore is created
435 * and rejected otherwise.
437 ignore: async function CrashSubmit_ignore(id) {
438 let [dump /* , extra, memory */] = getPendingMinidump(id);
439 const ignorePath = `${dump}.ignore`;
440 await IOUtils.writeUTF8(ignorePath, "", { mode: "create" });
444 * Get the list of pending crash IDs, excluding those marked to be ignored
446 * A Date object. Any files last modified before that date will be ignored
448 * @return a Promise that is fulfilled with an array of string, each
449 * being an ID as expected to be passed to submit() or ignore()
451 pendingIDs: async function CrashSubmit_pendingIDs(minFileDate) {
453 let pendingDir = getDir("pending");
455 if (!(await IOUtils.exists(pendingDir))) {
461 children = await IOUtils.getChildren(pendingDir);
468 const entries = Object.create(null);
469 const ignored = Object.create(null);
471 for (const child of children) {
472 const info = await IOUtils.stat(child);
474 if (info.type !== "directory") {
475 const name = PathUtils.filename(child);
476 const matches = name.match(/(.+)\.dmp$/);
478 const id = matches[1];
480 if (UUID_REGEX.test(id)) {
484 // maybe it's a .ignore file
485 const matchesIgnore = name.match(/(.+)\.dmp.ignore$/);
487 const id = matchesIgnore[1];
489 if (UUID_REGEX.test(id)) {
497 for (const [id, info] of Object.entries(entries)) {
498 const accessDate = new Date(info.lastAccessed);
499 if (!(id in ignored) && accessDate > minFileDate) {
512 * Prune the saved dumps.
514 * @return a Promise that is fulfilled when the daved dumps are deleted and
517 pruneSavedDumps: async function CrashSubmit_pruneSavedDumps() {
521 let pendingDir = getDir("pending");
525 children = await IOUtils.getChildren(pendingDir);
527 if (DOMException.isInstance(ex) && ex.name === "NotFoundError") {
534 for (const path of children) {
537 infoPromise = IOUtils.stat(path);
543 const name = PathUtils.filename(path);
545 if (name.match(/(.+)\.extra$/)) {
554 dirEntries.sort(async (a, b) => {
555 let dateA = (await a.infoPromise).lastModified;
556 let dateB = (await b.infoPromise).lastModified;
569 if (dirEntries.length > KEEP) {
572 for (let i = 0; i < dirEntries.length - KEEP; ++i) {
573 let extra = dirEntries[i];
574 let matches = extra.leafName.match(/(.+)\.extra$/);
577 let pathComponents = PathUtils.split(extra.path);
578 pathComponents[pathComponents.length - 1] = matches[1];
579 let path = PathUtils.join(...pathComponents);
581 toDelete.push(extra.path, `${path}.dmp`, `${path}.memory.json.gz`);
586 toDelete.map(path => {
587 return IOUtils.remove(path, { ignoreAbsent: true });
593 // List of currently active submit objects
594 _activeSubmissions: [],