Bug 1887774 convert from MediaEnginePrefs to AudioProcessing config in AudioInputProc...
[gecko.git] / toolkit / crashreporter / CrashSubmit.sys.mjs
blob72ff5dc30f13d5859321b3d2833a5deb932fa0ce
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";
9 const UUID_REGEX =
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}`);
31   });
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", {
40     id: crashID,
41   });
42   if (viewURL) {
43     data +=
44       "\n" +
45       (await l10n.formatValue("crashreporter-crash-details", { url: viewURL }));
46   }
48   await writeFileAsync("submitted", `${crashID}.txt`, data);
51 // the Submitter class represents an individual submission.
52 function Submitter(id, recordSubmission, noThrottle, extraExtraKeyVals) {
53   this.id = id;
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;
62   });
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);
70     try {
71       let toDelete = [this.dump, this.extra];
73       if (this.memory) {
74         toDelete.push(this.memory);
75       }
77       for (let entry of this.additionalDumps) {
78         toDelete.push(entry.dump);
79       }
81       await Promise.all(
82         toDelete.map(path => {
83           return IOUtils.remove(path, { ignoreAbsent: true });
84         })
85       );
86     } catch (ex) {
87       console.error(ex);
88     }
90     this.notifyStatus(SUCCESS, ret);
91     this.cleanup();
92   },
94   cleanup: function Submitter_cleanup() {
95     // drop some references just to be nice
96     this.iframe = null;
97     this.dump = null;
98     this.extra = null;
99     this.memory = null;
100     this.additionalDumps = null;
101     // remove this object from the list of active submissions
102     let idx = CrashSubmit._activeSubmissions.indexOf(this);
103     if (idx != -1) {
104       CrashSubmit._activeSubmissions.splice(idx, 1);
105     }
106   },
108   parseResponse: function Submitter_parseResponse(response) {
109     let parsedResponse = {};
111     for (let line of response.split("\n")) {
112       let data = line.split("=");
114       if (
115         (data.length == 2 &&
116           data[0] == "CrashID" &&
117           SUBMISSION_REGEX.test(data[1])) ||
118         data[0] == "ViewURL"
119       ) {
120         parsedResponse[data[0]] = data[1];
121       }
122     }
124     return parsedResponse;
125   },
127   submitForm: function Submitter_submitForm() {
128     if (!("ServerURL" in this.extraKeyVals)) {
129       return false;
130     }
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;
138     }
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";
148     // add the data
149     let payload = Object.assign({}, this.extraKeyVals);
150     let json = new Blob([JSON.stringify(payload)], {
151       type: "application/json",
152     });
153     formData.append("extra", json);
155     // add the minidumps
156     let promises = [
157       File.createFromFileName(this.dump, {
158         type: "application/octet-stream",
159       }).then(file => {
160         formData.append("upload_file_minidump", file);
161       }),
162     ];
164     if (this.memory) {
165       promises.push(
166         File.createFromFileName(this.memory, {
167           type: "application/gzip",
168         }).then(file => {
169           formData.append("memory_report", file);
170         })
171       );
172     }
174     if (this.additionalDumps.length) {
175       let names = [];
176       for (let i of this.additionalDumps) {
177         names.push(i.name);
178         promises.push(
179           File.createFromFileName(i.dump, {
180             type: "application/octet-stream",
181           }).then(file => {
182             formData.append("upload_file_minidump_" + i.name, file);
183           })
184         );
185       }
186     }
188     let manager = Services.crashmanager;
189     let submissionID = manager.generateSubmissionID();
191     xhr.addEventListener("readystatechange", () => {
192       if (xhr.readyState == 4) {
193         let ret =
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(
203             this.id,
204             submissionID,
205             new Date(),
206             result
207           );
208           if (submitted) {
209             manager.setRemoteCrashID(this.id, ret.CrashID);
210           }
211         }
213         p.then(() => {
214           if (submitted) {
215             this.submitSuccess(ret);
216           } else {
217             this.notifyStatus(FAILED);
218             this.cleanup();
219           }
220         });
221       }
222     });
224     let p = Promise.all(promises);
225     let id = this.id;
227     if (this.recordSubmission) {
228       p = p.then(() => {
229         return manager.addSubmissionAttempt(id, submissionID, new Date());
230       });
231     }
232     p.then(() => {
233       xhr.send(formData);
234     });
235     return true;
236   },
238   notifyStatus: function Submitter_notify(status, ret) {
239     let propBag = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
240       Ci.nsIWritablePropertyBag2
241     );
242     propBag.setPropertyAsAString("minidumpID", this.id);
243     if (status == SUCCESS) {
244       propBag.setPropertyAsAString("serverCrashID", ret.CrashID);
245     }
247     let extraKeyValsBag = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
248       Ci.nsIWritablePropertyBag2
249     );
250     for (let key in this.extraKeyVals) {
251       extraKeyValsBag.setPropertyAsAString(key, this.extraKeyVals[key]);
252     }
253     propBag.setPropertyAsInterface("extra", extraKeyValsBag);
255     Services.obs.notifyObservers(propBag, "crash-report-status", status);
257     switch (status) {
258       case SUCCESS:
259         this.resolveSubmitStatusPromise(ret.CrashID);
260         break;
261       case FAILED:
262         this.rejectSubmitStatusPromise(FAILED);
263         break;
264       default:
265       // no callbacks invoked.
266     }
267   },
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 = [
273       "StackTraces",
274       "TelemetryClientId",
275       "TelemetrySessionId",
276       "TelemetryServerURL",
277     ];
278     let extraKeyVals = await IOUtils.readJSON(extra);
280     this.extraKeyVals = { ...extraKeyVals, ...this.extraKeyVals };
281     strippedAnnotations.forEach(key => delete this.extraKeyVals[key]);
282   },
284   submit: async function Submitter_submit() {
285     if (this.recordSubmission) {
286       await Services.crashmanager.ensureCrashIsPresent(this.id);
287     }
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),
294     ]);
296     if (!dumpExists || !extraExists) {
297       this.notifyStatus(FAILED);
298       this.cleanup();
299       return this.submitStatusPromise;
300     }
302     this.dump = dump;
303     this.extra = extra;
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(
315           this.id + "-" + name
316         );
318         dumpsExistsPromises.push(IOUtils.exists(dump));
319         additionalDumps.push({ name, dump });
320       }
322       let dumpsExist = await Promise.all(dumpsExistsPromises);
323       let allDumpsExist = dumpsExist.every(exists => exists);
325       if (!allDumpsExist) {
326         this.notifyStatus(FAILED);
327         this.cleanup();
328         return this.submitStatusPromise;
329       }
330     }
332     this.notifyStatus(SUBMITTING);
333     this.additionalDumps = additionalDumps;
335     if (!(await this.submitForm())) {
336       this.notifyStatus(FAILED);
337       this.cleanup();
338     }
340     return this.submitStatusPromise;
341   },
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",
353   /**
354    * Submit the crash report named id.dmp from the "pending" directory.
355    *
356    * @param id
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.
361    * @param params
362    *        An object containing any of the following optional parameters:
363    *        - recordSubmission
364    *          If true, a submission event is recorded in CrashManager.
365    *        - noThrottle
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
375    *          .extra file.
376    *
377    *  @return a Promise that is fulfilled with the server crash ID when the
378    *          submission succeeds and rejected otherwise.
379    */
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;
388     }
390     if ("noThrottle" in params) {
391       noThrottle = params.noThrottle;
392     }
394     if ("extraExtraKeyVals" in params) {
395       extraExtraKeyVals = params.extraExtraKeyVals;
396     }
398     extraExtraKeyVals.SubmittedFrom = submittedFrom;
400     let submitter = new Submitter(
401       id,
402       recordSubmission,
403       noThrottle,
404       extraExtraKeyVals
405     );
406     CrashSubmit._activeSubmissions.push(submitter);
407     return submitter.submit();
408   },
410   /**
411    * Delete the minidup from the "pending" directory.
412    *
413    * @param id
414    *        Filename (minus .dmp extension) of the minidump to delete.
415    *
416    * @return a Promise that is fulfilled when the minidump is deleted and
417    *         rejected otherwise
418    */
419   delete: async function CrashSubmit_delete(id) {
420     await Promise.all(
421       getPendingMinidump(id).map(path => {
422         return IOUtils.remove(path);
423       })
424     );
425   },
427   /**
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.
430    *
431    * @param id
432    *        Filename (minus .dmp extension) of the report to ignore
433    *
434    * @return a Promise that is fulfilled when (if) the .dmg.ignore is created
435    *         and rejected otherwise.
436    */
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" });
441   },
443   /**
444    * Get the list of pending crash IDs, excluding those marked to be ignored
445    * @param minFileDate
446    *     A Date object. Any files last modified before that date will be ignored
447    *
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()
450    */
451   pendingIDs: async function CrashSubmit_pendingIDs(minFileDate) {
452     let ids = [];
453     let pendingDir = getDir("pending");
455     if (!(await IOUtils.exists(pendingDir))) {
456       return ids;
457     }
459     let children;
460     try {
461       children = await IOUtils.getChildren(pendingDir);
462     } catch (ex) {
463       console.error(ex);
464       throw ex;
465     }
467     try {
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$/);
477           if (matches) {
478             const id = matches[1];
480             if (UUID_REGEX.test(id)) {
481               entries[id] = info;
482             }
483           } else {
484             // maybe it's a .ignore file
485             const matchesIgnore = name.match(/(.+)\.dmp.ignore$/);
486             if (matchesIgnore) {
487               const id = matchesIgnore[1];
489               if (UUID_REGEX.test(id)) {
490                 ignored[id] = true;
491               }
492             }
493           }
494         }
495       }
497       for (const [id, info] of Object.entries(entries)) {
498         const accessDate = new Date(info.lastAccessed);
499         if (!(id in ignored) && accessDate > minFileDate) {
500           ids.push(id);
501         }
502       }
503     } catch (ex) {
504       console.error(ex);
505       throw ex;
506     }
508     return ids;
509   },
511   /**
512    * Prune the saved dumps.
513    *
514    * @return a Promise that is fulfilled when the daved dumps are deleted and
515    *         rejected otherwise
516    */
517   pruneSavedDumps: async function CrashSubmit_pruneSavedDumps() {
518     const KEEP = 10;
520     let dirEntries = [];
521     let pendingDir = getDir("pending");
523     let children;
524     try {
525       children = await IOUtils.getChildren(pendingDir);
526     } catch (ex) {
527       if (DOMException.isInstance(ex) && ex.name === "NotFoundError") {
528         return [];
529       }
531       throw ex;
532     }
534     for (const path of children) {
535       let infoPromise;
536       try {
537         infoPromise = IOUtils.stat(path);
538       } catch (ex) {
539         console.error(ex);
540         throw ex;
541       }
543       const name = PathUtils.filename(path);
545       if (name.match(/(.+)\.extra$/)) {
546         dirEntries.push({
547           name,
548           path,
549           infoPromise,
550         });
551       }
552     }
554     dirEntries.sort(async (a, b) => {
555       let dateA = (await a.infoPromise).lastModified;
556       let dateB = (await b.infoPromise).lastModified;
558       if (dateA < dateB) {
559         return -1;
560       }
562       if (dateB < dateA) {
563         return 1;
564       }
566       return 0;
567     });
569     if (dirEntries.length > KEEP) {
570       let toDelete = [];
572       for (let i = 0; i < dirEntries.length - KEEP; ++i) {
573         let extra = dirEntries[i];
574         let matches = extra.leafName.match(/(.+)\.extra$/);
576         if (matches) {
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`);
582         }
583       }
585       await Promise.all(
586         toDelete.map(path => {
587           return IOUtils.remove(path, { ignoreAbsent: true });
588         })
589       );
590     }
591   },
593   // List of currently active submit objects
594   _activeSubmissions: [],