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 import { FileUtils } from "resource://gre/modules/FileUtils.sys.mjs";
7 const SUCCESS = "success";
8 const FAILED = "failed";
9 const SUBMITTING = "submitting";
12 /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
13 const SUBMISSION_REGEX =
14 /^bp-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
16 // TODO: this is still synchronous; need an async INI parser to make it async
17 function parseINIStrings(path) {
18 let file = new FileUtils.File(path);
19 let factory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"].getService(
20 Ci.nsIINIParserFactory
22 let parser = factory.createINIParser(file);
24 for (let key of parser.getKeys("Strings")) {
25 obj[key] = parser.getString("Strings", key);
30 // Since we're basically re-implementing (with async) part of the crashreporter
31 // client here, we'll just steal the strings we need from crashreporter.ini
32 async function getL10nStrings() {
33 let path = PathUtils.join(
34 Services.dirsvc.get("GreD", Ci.nsIFile).path,
37 let pathExists = await IOUtils.exists(path);
40 // we if we're on a mac
41 let parentDir = PathUtils.parent(path);
42 path = PathUtils.join(
51 let pathExists = await IOUtils.exists(path);
54 // This happens on Android where everything is in an APK.
55 // Android users can't see the contents of the submitted files
56 // anyway, so just hardcode some fallback strings.
58 crashid: "Crash ID: %s",
59 reporturl: "You can view details of this crash at %s",
64 let crstrings = parseINIStrings(path);
66 crashid: crstrings.CrashID,
67 reporturl: crstrings.CrashDetailsURL,
70 path = PathUtils.join(
71 Services.dirsvc.get("XCurProcD", Ci.nsIFile).path,
72 "crashreporter-override.ini"
74 pathExists = await IOUtils.exists(path);
77 crstrings = parseINIStrings(path);
79 if ("CrashID" in crstrings) {
80 strings.crashid = crstrings.CrashID;
83 if ("CrashDetailsURL" in crstrings) {
84 strings.reporturl = crstrings.CrashDetailsURL;
91 function getDir(name) {
92 let uAppDataPath = Services.dirsvc.get("UAppData", Ci.nsIFile).path;
93 return PathUtils.join(uAppDataPath, "Crash Reports", name);
96 async function writeFileAsync(dirName, fileName, data) {
97 let dirPath = getDir(dirName);
98 let filePath = PathUtils.join(dirPath, fileName);
99 await IOUtils.makeDirectory(dirPath, { permissions: 0o700 });
100 await IOUtils.writeUTF8(filePath, data);
103 function getPendingMinidump(id) {
104 let pendingDir = getDir("pending");
106 return [".dmp", ".extra", ".memory.json.gz"].map(suffix => {
107 return PathUtils.join(pendingDir, `${id}${suffix}`);
111 async function writeSubmittedReportAsync(crashID, viewURL) {
112 let strings = await getL10nStrings();
113 let data = strings.crashid.replace("%s", crashID);
116 data += "\n" + strings.reporturl.replace("%s", viewURL);
119 await writeFileAsync("submitted", `${crashID}.txt`, data);
122 // the Submitter class represents an individual submission.
123 function Submitter(id, recordSubmission, noThrottle, extraExtraKeyVals) {
125 this.recordSubmission = recordSubmission;
126 this.noThrottle = noThrottle;
127 this.additionalDumps = [];
128 this.extraKeyVals = extraExtraKeyVals;
129 // mimic deferred Promise behavior
130 this.submitStatusPromise = new Promise((resolve, reject) => {
131 this.resolveSubmitStatusPromise = resolve;
132 this.rejectSubmitStatusPromise = reject;
136 Submitter.prototype = {
137 submitSuccess: async function Submitter_submitSuccess(ret) {
138 // Write out the details file to submitted
139 await writeSubmittedReportAsync(ret.CrashID, ret.ViewURL);
142 let toDelete = [this.dump, this.extra];
145 toDelete.push(this.memory);
148 for (let entry of this.additionalDumps) {
149 toDelete.push(entry.dump);
153 toDelete.map(path => {
154 return IOUtils.remove(path, { ignoreAbsent: true });
161 this.notifyStatus(SUCCESS, ret);
165 cleanup: function Submitter_cleanup() {
166 // drop some references just to be nice
171 this.additionalDumps = null;
172 // remove this object from the list of active submissions
173 let idx = CrashSubmit._activeSubmissions.indexOf(this);
175 CrashSubmit._activeSubmissions.splice(idx, 1);
179 parseResponse: function Submitter_parseResponse(response) {
180 let parsedResponse = {};
182 for (let line of response.split("\n")) {
183 let data = line.split("=");
187 data[0] == "CrashID" &&
188 SUBMISSION_REGEX.test(data[1])) ||
191 parsedResponse[data[0]] = data[1];
195 return parsedResponse;
198 submitForm: function Submitter_submitForm() {
199 if (!("ServerURL" in this.extraKeyVals)) {
202 let serverURL = this.extraKeyVals.ServerURL;
203 delete this.extraKeyVals.ServerURL;
205 // Override the submission URL from the environment
206 let envOverride = Services.env.get("MOZ_CRASHREPORTER_URL");
207 if (envOverride != "") {
208 serverURL = envOverride;
211 let xhr = new XMLHttpRequest();
212 xhr.open("POST", serverURL, true);
214 let formData = new FormData();
216 // tell the server not to throttle this if requested
217 this.extraKeyVals.Throttleable = this.noThrottle ? "0" : "1";
220 let payload = Object.assign({}, this.extraKeyVals);
221 let json = new Blob([JSON.stringify(payload)], {
222 type: "application/json",
224 formData.append("extra", json);
228 File.createFromFileName(this.dump, {
229 type: "application/octet-stream",
231 formData.append("upload_file_minidump", file);
237 File.createFromFileName(this.memory, {
238 type: "application/gzip",
240 formData.append("memory_report", file);
245 if (this.additionalDumps.length) {
247 for (let i of this.additionalDumps) {
250 File.createFromFileName(i.dump, {
251 type: "application/octet-stream",
253 formData.append("upload_file_minidump_" + i.name, file);
259 let manager = Services.crashmanager;
260 let submissionID = manager.generateSubmissionID();
262 xhr.addEventListener("readystatechange", evt => {
263 if (xhr.readyState == 4) {
265 xhr.status === 200 ? this.parseResponse(xhr.responseText) : {};
266 let submitted = !!ret.CrashID;
267 let p = Promise.resolve();
269 if (this.recordSubmission) {
270 let result = submitted
271 ? manager.SUBMISSION_RESULT_OK
272 : manager.SUBMISSION_RESULT_FAILED;
273 p = manager.addSubmissionResult(
280 manager.setRemoteCrashID(this.id, ret.CrashID);
286 this.submitSuccess(ret);
288 this.notifyStatus(FAILED);
295 let p = Promise.all(promises);
298 if (this.recordSubmission) {
300 return manager.addSubmissionAttempt(id, submissionID, new Date());
309 notifyStatus: function Submitter_notify(status, ret) {
310 let propBag = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
311 Ci.nsIWritablePropertyBag2
313 propBag.setPropertyAsAString("minidumpID", this.id);
314 if (status == SUCCESS) {
315 propBag.setPropertyAsAString("serverCrashID", ret.CrashID);
318 let extraKeyValsBag = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
319 Ci.nsIWritablePropertyBag2
321 for (let key in this.extraKeyVals) {
322 extraKeyValsBag.setPropertyAsAString(key, this.extraKeyVals[key]);
324 propBag.setPropertyAsInterface("extra", extraKeyValsBag);
326 Services.obs.notifyObservers(propBag, "crash-report-status", status);
330 this.resolveSubmitStatusPromise(ret.CrashID);
333 this.rejectSubmitStatusPromise(FAILED);
336 // no callbacks invoked.
340 readAnnotations: async function Submitter_readAnnotations(extra) {
341 // These annotations are used only by the crash reporter client and should
342 // not be submitted to Socorro.
343 const strippedAnnotations = [
346 "TelemetrySessionId",
347 "TelemetryServerURL",
349 let extraKeyVals = await IOUtils.readJSON(extra);
351 this.extraKeyVals = { ...extraKeyVals, ...this.extraKeyVals };
352 strippedAnnotations.forEach(key => delete this.extraKeyVals[key]);
355 submit: async function Submitter_submit() {
356 if (this.recordSubmission) {
357 await Services.crashmanager.ensureCrashIsPresent(this.id);
360 let [dump, extra, memory] = getPendingMinidump(this.id);
361 let [dumpExists, extraExists, memoryExists] = await Promise.all([
362 IOUtils.exists(dump),
363 IOUtils.exists(extra),
364 IOUtils.exists(memory),
367 if (!dumpExists || !extraExists) {
368 this.notifyStatus(FAILED);
370 return this.submitStatusPromise;
375 this.memory = memoryExists ? memory : null;
376 await this.readAnnotations(extra);
378 let additionalDumps = [];
380 if ("additional_minidumps" in this.extraKeyVals) {
381 let dumpsExistsPromises = [];
382 let names = this.extraKeyVals.additional_minidumps.split(",");
384 for (let name of names) {
385 let [dump /* , extra, memory */] = getPendingMinidump(
389 dumpsExistsPromises.push(IOUtils.exists(dump));
390 additionalDumps.push({ name, dump });
393 let dumpsExist = await Promise.all(dumpsExistsPromises);
394 let allDumpsExist = dumpsExist.every(exists => exists);
396 if (!allDumpsExist) {
397 this.notifyStatus(FAILED);
399 return this.submitStatusPromise;
403 this.notifyStatus(SUBMITTING);
404 this.additionalDumps = additionalDumps;
406 if (!(await this.submitForm())) {
407 this.notifyStatus(FAILED);
411 return this.submitStatusPromise;
415 // ===================================
416 // External API goes here
417 export var CrashSubmit = {
418 // A set of strings representing how a user subnmitted a given crash
419 SUBMITTED_FROM_AUTO: "Auto",
420 SUBMITTED_FROM_INFOBAR: "Infobar",
421 SUBMITTED_FROM_ABOUT_CRASHES: "AboutCrashes",
422 SUBMITTED_FROM_CRASH_TAB: "CrashedTab",
425 * Submit the crash report named id.dmp from the "pending" directory.
428 * Filename (minus .dmp extension) of the minidump to submit.
429 * @param submittedFrom
430 * One of the SUBMITTED_FROM_* constants representing how the
431 * user submitted this crash.
433 * An object containing any of the following optional parameters:
435 * If true, a submission event is recorded in CrashManager.
437 * If true, this crash report should be submitted with
438 * the Throttleable annotation set to "0" indicating that
439 * it should be processed right away. This should be set
440 * when the report is being submitted and the user expects
441 * to see the results immediately. Defaults to false.
442 * - extraExtraKeyVals
443 * An object whose key-value pairs will be merged with the data from
444 * the ".extra" file submitted with the report. The properties of
445 * this object will override properties of the same name in the
448 * @return a Promise that is fulfilled with the server crash ID when the
449 * submission succeeds and rejected otherwise.
451 submit: function CrashSubmit_submit(id, submittedFrom, params) {
452 params = params || {};
453 let recordSubmission = false;
454 let noThrottle = false;
455 let extraExtraKeyVals = {};
457 if ("recordSubmission" in params) {
458 recordSubmission = params.recordSubmission;
461 if ("noThrottle" in params) {
462 noThrottle = params.noThrottle;
465 if ("extraExtraKeyVals" in params) {
466 extraExtraKeyVals = params.extraExtraKeyVals;
469 extraExtraKeyVals.SubmittedFrom = submittedFrom;
471 let submitter = new Submitter(
477 CrashSubmit._activeSubmissions.push(submitter);
478 return submitter.submit();
482 * Delete the minidup from the "pending" directory.
485 * Filename (minus .dmp extension) of the minidump to delete.
487 * @return a Promise that is fulfilled when the minidump is deleted and
490 delete: async function CrashSubmit_delete(id) {
492 getPendingMinidump(id).map(path => {
493 return IOUtils.remove(path);
499 * Add a .dmg.ignore file along side the .dmp file to indicate that the user
500 * shouldn't be prompted to submit this crash report again.
503 * Filename (minus .dmp extension) of the report to ignore
505 * @return a Promise that is fulfilled when (if) the .dmg.ignore is created
506 * and rejected otherwise.
508 ignore: async function CrashSubmit_ignore(id) {
509 let [dump /* , extra, memory */] = getPendingMinidump(id);
510 const ignorePath = `${dump}.ignore`;
511 await IOUtils.writeUTF8(ignorePath, "", { mode: "create" });
515 * Get the list of pending crash IDs, excluding those marked to be ignored
517 * A Date object. Any files last modified before that date will be ignored
519 * @return a Promise that is fulfilled with an array of string, each
520 * being an ID as expected to be passed to submit() or ignore()
522 pendingIDs: async function CrashSubmit_pendingIDs(minFileDate) {
524 let pendingDir = getDir("pending");
526 if (!(await IOUtils.exists(pendingDir))) {
532 children = await IOUtils.getChildren(pendingDir);
539 const entries = Object.create(null);
540 const ignored = Object.create(null);
542 for (const child of children) {
543 const info = await IOUtils.stat(child);
545 if (info.type !== "directory") {
546 const name = PathUtils.filename(child);
547 const matches = name.match(/(.+)\.dmp$/);
549 const id = matches[1];
551 if (UUID_REGEX.test(id)) {
555 // maybe it's a .ignore file
556 const matchesIgnore = name.match(/(.+)\.dmp.ignore$/);
558 const id = matchesIgnore[1];
560 if (UUID_REGEX.test(id)) {
568 for (const [id, info] of Object.entries(entries)) {
569 const accessDate = new Date(info.lastAccessed);
570 if (!(id in ignored) && accessDate > minFileDate) {
583 * Prune the saved dumps.
585 * @return a Promise that is fulfilled when the daved dumps are deleted and
588 pruneSavedDumps: async function CrashSubmit_pruneSavedDumps() {
592 let pendingDir = getDir("pending");
596 children = await IOUtils.getChildren(pendingDir);
598 if (DOMException.isInstance(ex) && ex.name === "NotFoundError") {
605 for (const path of children) {
608 infoPromise = IOUtils.stat(path);
614 const name = PathUtils.filename(path);
616 if (name.match(/(.+)\.extra$/)) {
625 dirEntries.sort(async (a, b) => {
626 let dateA = (await a.infoPromise).lastModified;
627 let dateB = (await b.infoPromise).lastModified;
640 if (dirEntries.length > KEEP) {
643 for (let i = 0; i < dirEntries.length - KEEP; ++i) {
644 let extra = dirEntries[i];
645 let matches = extra.leafName.match(/(.+)\.extra$/);
648 let pathComponents = PathUtils.split(extra.path);
649 pathComponents[pathComponents.length - 1] = matches[1];
650 let path = PathUtils.join(...pathComponents);
652 toDelete.push(extra.path, `${path}.dmp`, `${path}.memory.json.gz`);
657 toDelete.map(path => {
658 return IOUtils.remove(path, { ignoreAbsent: true });
664 // List of currently active submit objects
665 _activeSubmissions: [],