Bug 1890689 Don't pretend to pre-buffer with DynamicResampler r=pehrsons
[gecko.git] / browser / modules / ContentCrashHandlers.sys.mjs
blobcd8dfc04c60b37f37091f4a258b2d80926e66dc1
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 { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
7 const lazy = {};
9 ChromeUtils.defineESModuleGetters(lazy, {
10   BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
11   CrashSubmit: "resource://gre/modules/CrashSubmit.sys.mjs",
12   E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
13   SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
14   clearTimeout: "resource://gre/modules/Timer.sys.mjs",
15   setTimeout: "resource://gre/modules/Timer.sys.mjs",
16 });
18 // We don't process crash reports older than 28 days, so don't bother
19 // submitting them
20 const PENDING_CRASH_REPORT_DAYS = 28;
21 const DAY = 24 * 60 * 60 * 1000; // milliseconds
22 const DAYS_TO_SUPPRESS = 30;
23 const MAX_UNSEEN_CRASHED_CHILD_IDS = 20;
24 const MAX_UNSEEN_CRASHED_SUBFRAME_IDS = 10;
26 // Time after which we will begin scanning for unsubmitted crash reports
27 const CHECK_FOR_UNSUBMITTED_CRASH_REPORTS_DELAY_MS = 60 * 10000; // 10 minutes
29 // This is SIGUSR1 and indicates a user-invoked crash
30 const EXIT_CODE_CONTENT_CRASHED = 245;
32 const TABCRASHED_ICON_URI = "chrome://browser/skin/tab-crashed.svg";
34 const SUBFRAMECRASH_LEARNMORE_URI =
35   "https://support.mozilla.org/kb/firefox-crashes-troubleshoot-prevent-and-get-help";
37 /**
38  * BrowserWeakMap is exactly like a WeakMap, but expects <xul:browser>
39  * objects only.
40  *
41  * Under the hood, BrowserWeakMap keys the map off of the <xul:browser>
42  * permanentKey. If, however, the browser has never gotten a permanentKey,
43  * it falls back to keying on the <xul:browser> element itself.
44  */
45 class BrowserWeakMap extends WeakMap {
46   get(browser) {
47     if (browser.permanentKey) {
48       return super.get(browser.permanentKey);
49     }
50     return super.get(browser);
51   }
53   set(browser, value) {
54     if (browser.permanentKey) {
55       return super.set(browser.permanentKey, value);
56     }
57     return super.set(browser, value);
58   }
60   delete(browser) {
61     if (browser.permanentKey) {
62       return super.delete(browser.permanentKey);
63     }
64     return super.delete(browser);
65   }
68 export var TabCrashHandler = {
69   _crashedTabCount: 0,
70   childMap: new Map(),
71   browserMap: new BrowserWeakMap(),
72   notificationsMap: new Map(),
73   unseenCrashedChildIDs: [],
74   pendingSubFrameCrashes: new Map(),
75   pendingSubFrameCrashesIDs: [],
76   crashedBrowserQueues: new Map(),
77   restartRequiredBrowsers: new WeakSet(),
78   testBuildIDMismatch: false,
80   get prefs() {
81     delete this.prefs;
82     return (this.prefs = Services.prefs.getBranch(
83       "browser.tabs.crashReporting."
84     ));
85   },
87   init() {
88     if (this.initialized) {
89       return;
90     }
91     this.initialized = true;
93     Services.obs.addObserver(this, "ipc:content-shutdown");
94     Services.obs.addObserver(this, "oop-frameloader-crashed");
95   },
97   observe(aSubject, aTopic) {
98     switch (aTopic) {
99       case "ipc:content-shutdown": {
100         aSubject.QueryInterface(Ci.nsIPropertyBag2);
102         if (!aSubject.get("abnormal")) {
103           return;
104         }
106         let childID = aSubject.get("childID");
107         let dumpID = aSubject.get("dumpID");
109         // Get and remove the subframe crash info first.
110         let subframeCrashItem = this.getAndRemoveSubframeCrash(childID);
112         if (!dumpID) {
113           Services.telemetry
114             .getHistogramById("FX_CONTENT_CRASH_DUMP_UNAVAILABLE")
115             .add(1);
116         } else if (AppConstants.MOZ_CRASHREPORTER) {
117           this.childMap.set(childID, dumpID);
119           // If this is a subframe crash, show the crash notification. Only
120           // show subframe notifications when there is a minidump available.
121           if (subframeCrashItem) {
122             let browsers =
123               ChromeUtils.nondeterministicGetWeakMapKeys(subframeCrashItem) ||
124               [];
125             for (let browserItem of browsers) {
126               let browser = subframeCrashItem.get(browserItem);
127               if (browser.isConnected && !browser.ownerGlobal.closed) {
128                 this.showSubFrameNotification(browser, childID, dumpID);
129               }
130             }
131           }
132         }
134         if (!this.flushCrashedBrowserQueue(childID)) {
135           this.unseenCrashedChildIDs.push(childID);
136           // The elements in unseenCrashedChildIDs will only be removed if
137           // the tab crash page is shown. However, ipc:content-shutdown might
138           // be fired for processes for which we'll never show the tab crash
139           // page - for example, the thumbnailing process. Another case to
140           // consider is if the user is configured to submit backlogged crash
141           // reports automatically, and a background tab crashes. In that case,
142           // we will never show the tab crash page, and never remove the element
143           // from the list.
144           //
145           // Instead of trying to account for all of those cases, we prevent
146           // this list from getting too large by putting a reasonable upper
147           // limit on how many childIDs we track. It's unlikely that this
148           // array would ever get so large as to be unwieldy (that'd be a lot
149           // or crashes!), but a leak is a leak.
150           if (
151             this.unseenCrashedChildIDs.length > MAX_UNSEEN_CRASHED_CHILD_IDS
152           ) {
153             this.unseenCrashedChildIDs.shift();
154           }
155         }
157         // check for environment affecting crash reporting
158         let shutdown = Services.env.exists("MOZ_CRASHREPORTER_SHUTDOWN");
160         if (shutdown) {
161           dump(
162             "A content process crashed and MOZ_CRASHREPORTER_SHUTDOWN is " +
163               "set, shutting down\n"
164           );
165           Services.startup.quit(
166             Ci.nsIAppStartup.eForceQuit,
167             EXIT_CODE_CONTENT_CRASHED
168           );
169         }
171         break;
172       }
173       case "oop-frameloader-crashed": {
174         let browser = aSubject.ownerElement;
175         if (!browser) {
176           return;
177         }
179         this.browserMap.set(browser, aSubject.childID);
180         break;
181       }
182     }
183   },
185   /**
186    * This should be called once a content process has finished
187    * shutting down abnormally. Any tabbrowser browsers that were
188    * selected at the time of the crash will then be sent to
189    * the crashed tab page.
190    *
191    * @param childID (int)
192    *        The childID of the content process that just crashed.
193    * @returns boolean
194    *        True if one or more browsers were sent to the tab crashed
195    *        page.
196    */
197   flushCrashedBrowserQueue(childID) {
198     let browserQueue = this.crashedBrowserQueues.get(childID);
199     if (!browserQueue) {
200       return false;
201     }
203     this.crashedBrowserQueues.delete(childID);
205     let sentBrowser = false;
206     for (let weakBrowser of browserQueue) {
207       let browser = weakBrowser.get();
208       if (browser) {
209         if (
210           this.restartRequiredBrowsers.has(browser) ||
211           this.testBuildIDMismatch
212         ) {
213           this.sendToRestartRequiredPage(browser);
214         } else {
215           this.sendToTabCrashedPage(browser);
216         }
217         sentBrowser = true;
218       }
219     }
221     return sentBrowser;
222   },
224   /**
225    * Called by a tabbrowser when it notices that its selected browser
226    * has crashed. This will queue the browser to show the tab crash
227    * page once the content process has finished tearing down.
228    *
229    * @param browser (<xul:browser>)
230    *        The selected browser that just crashed.
231    * @param restartRequired (bool)
232    *        Whether or not a browser restart is required to recover.
233    */
234   onSelectedBrowserCrash(browser, restartRequired) {
235     if (!browser.isRemoteBrowser) {
236       console.error("Selected crashed browser is not remote.");
237       return;
238     }
239     if (!browser.frameLoader) {
240       console.error("Selected crashed browser has no frameloader.");
241       return;
242     }
244     let childID = browser.frameLoader.childID;
246     let browserQueue = this.crashedBrowserQueues.get(childID);
247     if (!browserQueue) {
248       browserQueue = [];
249       this.crashedBrowserQueues.set(childID, browserQueue);
250     }
251     // It's probably unnecessary to store this browser as a
252     // weak reference, since the content process should complete
253     // its teardown in the same tick of the event loop, and then
254     // this queue will be flushed. The weak reference is to avoid
255     // leaking browsers in case anything goes wrong during this
256     // teardown process.
257     browserQueue.push(Cu.getWeakReference(browser));
259     if (restartRequired) {
260       this.restartRequiredBrowsers.add(browser);
261     }
263     // In the event that the content process failed to launch, then
264     // the childID will be 0. In that case, we will never receive
265     // a dumpID nor an ipc:content-shutdown observer notification,
266     // so we should flush the queue for childID 0 immediately.
267     if (childID == 0) {
268       this.flushCrashedBrowserQueue(0);
269     }
270   },
272   /**
273    * Called by a tabbrowser when it notices that a background browser
274    * has crashed. This will flip its remoteness to non-remote, and attempt
275    * to revive the crashed tab so that upon selection the tab either shows
276    * an error page, or automatically restores.
277    *
278    * @param browser (<xul:browser>)
279    *        The background browser that just crashed.
280    * @param restartRequired (bool)
281    *        Whether or not a browser restart is required to recover.
282    */
283   onBackgroundBrowserCrash(browser, restartRequired) {
284     if (restartRequired) {
285       this.restartRequiredBrowsers.add(browser);
286     }
288     let gBrowser = browser.getTabBrowser();
289     let tab = gBrowser.getTabForBrowser(browser);
291     gBrowser.updateBrowserRemoteness(browser, {
292       remoteType: lazy.E10SUtils.NOT_REMOTE,
293     });
295     lazy.SessionStore.reviveCrashedTab(tab);
296   },
298   /**
299    * Called when a subframe crashes. If the dump is available, shows a subframe
300    * crashed notification, otherwise waits for one to be available.
301    *
302    * @param browser (<xul:browser>)
303    *        The browser containing the frame that just crashed.
304    * @param childId
305    *        The id of the process that just crashed.
306    */
307   async onSubFrameCrash(browser, childID) {
308     if (!AppConstants.MOZ_CRASHREPORTER) {
309       return;
310     }
312     // If a crash dump is available, use it. Otherwise, add the child id to the pending
313     // subframe crashes list, and wait for the crash "ipc:content-shutdown" notification
314     // to get the minidump. If it never arrives, don't show the notification.
315     let dumpID = this.childMap.get(childID);
316     if (dumpID) {
317       this.showSubFrameNotification(browser, childID, dumpID);
318     } else {
319       let item = this.pendingSubFrameCrashes.get(childID);
320       if (!item) {
321         item = new BrowserWeakMap();
322         this.pendingSubFrameCrashes.set(childID, item);
324         // Add the childID to an array that only has room for MAX_UNSEEN_CRASHED_SUBFRAME_IDS
325         // items. If there is no more room, pop the oldest off and remove it. This technique
326         // is used instead of a timeout.
327         if (
328           this.pendingSubFrameCrashesIDs.length >=
329           MAX_UNSEEN_CRASHED_SUBFRAME_IDS
330         ) {
331           let idToDelete = this.pendingSubFrameCrashesIDs.shift();
332           this.pendingSubFrameCrashes.delete(idToDelete);
333         }
334         this.pendingSubFrameCrashesIDs.push(childID);
335       }
336       item.set(browser, browser);
337     }
338   },
340   /**
341    * Given a childID, retrieve the subframe crash info for it
342    * from the pendingSubFrameCrashes map. The data is removed
343    * from the map and returned.
344    *
345    * @param childID number
346    *        childID of the content that crashed.
347    * @returns subframe crash info added by previous call to onSubFrameCrash.
348    */
349   getAndRemoveSubframeCrash(childID) {
350     let item = this.pendingSubFrameCrashes.get(childID);
351     if (item) {
352       this.pendingSubFrameCrashes.delete(childID);
353       let idx = this.pendingSubFrameCrashesIDs.indexOf(childID);
354       if (idx >= 0) {
355         this.pendingSubFrameCrashesIDs.splice(idx, 1);
356       }
357     }
359     return item;
360   },
362   /**
363    * Called to indicate that a subframe within a browser has crashed. A notification
364    * bar will be shown.
365    *
366    * @param browser (<xul:browser>)
367    *        The browser containing the frame that just crashed.
368    * @param childId
369    *        The id of the process that just crashed.
370    * @param dumpID
371    *        Minidump id of the crash.
372    */
373   async showSubFrameNotification(browser, childID, dumpID) {
374     let gBrowser = browser.getTabBrowser();
375     let notificationBox = gBrowser.getNotificationBox(browser);
377     const value = "subframe-crashed";
378     let notification = notificationBox.getNotificationWithValue(value);
379     if (notification) {
380       // Don't show multiple notifications for a browser.
381       return;
382     }
384     let closeAllNotifications = () => {
385       // Close all other notifications on other tabs that might
386       // be open for the same crashed process.
387       let existingItem = this.notificationsMap.get(childID);
388       if (existingItem) {
389         for (let notif of existingItem.slice()) {
390           notif.close();
391         }
392       }
393     };
395     gBrowser.ownerGlobal.MozXULElement.insertFTLIfNeeded(
396       "browser/contentCrash.ftl"
397     );
399     let buttons = [
400       {
401         "l10n-id": "crashed-subframe-learnmore-link",
402         popup: null,
403         link: SUBFRAMECRASH_LEARNMORE_URI,
404       },
405       {
406         "l10n-id": "crashed-subframe-submit",
407         popup: null,
408         callback: async () => {
409           if (dumpID) {
410             UnsubmittedCrashHandler.submitReports(
411               [dumpID],
412               lazy.CrashSubmit.SUBMITTED_FROM_CRASH_TAB
413             );
414           }
415           closeAllNotifications();
416         },
417       },
418     ];
420     notification = await notificationBox.appendNotification(
421       value,
422       {
423         label: { "l10n-id": "crashed-subframe-message" },
424         image: TABCRASHED_ICON_URI,
425         priority: notificationBox.PRIORITY_INFO_MEDIUM,
426         eventCallback: eventName => {
427           if (eventName == "disconnected") {
428             let existingItem = this.notificationsMap.get(childID);
429             if (existingItem) {
430               let idx = existingItem.indexOf(notification);
431               if (idx >= 0) {
432                 existingItem.splice(idx, 1);
433               }
435               if (!existingItem.length) {
436                 this.notificationsMap.delete(childID);
437               }
438             }
439           } else if (eventName == "dismissed") {
440             if (dumpID) {
441               lazy.CrashSubmit.ignore(dumpID);
442               this.childMap.delete(childID);
443             }
445             closeAllNotifications();
446           }
447         },
448       },
449       buttons
450     );
452     let existingItem = this.notificationsMap.get(childID);
453     if (existingItem) {
454       existingItem.push(notification);
455     } else {
456       this.notificationsMap.set(childID, [notification]);
457     }
458   },
460   /**
461    * This method is exposed for SessionStore to call if the user selects
462    * a tab which will restore on demand. It's possible that the tab
463    * is in this state because it recently crashed. If that's the case, then
464    * it's also possible that the user has not seen the tab crash page for
465    * that particular crash, in which case, we might show it to them instead
466    * of restoring the tab.
467    *
468    * @param browser (<xul:browser>)
469    *        A browser from a browser tab that the user has just selected
470    *        to restore on demand.
471    * @returns (boolean)
472    *        True if TabCrashHandler will send the user to the tab crash
473    *        page instead.
474    */
475   willShowCrashedTab(browser) {
476     let childID = this.browserMap.get(browser);
477     // We will only show the tab crash page if:
478     // 1) We are aware that this browser crashed
479     // 2) We know we've never shown the tab crash page for the
480     //    crash yet
481     // 3) The user is not configured to automatically submit backlogged
482     //    crash reports. If they are, we'll send the crash report
483     //    immediately.
484     if (childID && this.unseenCrashedChildIDs.includes(childID)) {
485       if (UnsubmittedCrashHandler.autoSubmit) {
486         let dumpID = this.childMap.get(childID);
487         if (dumpID) {
488           UnsubmittedCrashHandler.submitReports(
489             [dumpID],
490             lazy.CrashSubmit.SUBMITTED_FROM_AUTO
491           );
492         }
493       } else {
494         this.sendToTabCrashedPage(browser);
495         return true;
496       }
497     } else if (childID === 0) {
498       if (this.restartRequiredBrowsers.has(browser)) {
499         this.sendToRestartRequiredPage(browser);
500       } else {
501         this.sendToTabCrashedPage(browser);
502       }
503       return true;
504     }
506     return false;
507   },
509   sendToRestartRequiredPage(browser) {
510     let uri = browser.currentURI;
511     let gBrowser = browser.getTabBrowser();
512     let tab = gBrowser.getTabForBrowser(browser);
513     // The restart required page is non-remote by default.
514     gBrowser.updateBrowserRemoteness(browser, {
515       remoteType: lazy.E10SUtils.NOT_REMOTE,
516     });
518     browser.docShell.displayLoadError(Cr.NS_ERROR_BUILDID_MISMATCH, uri, null);
519     tab.setAttribute("crashed", true);
520     gBrowser.tabContainer.updateTabIndicatorAttr(tab);
522     // Make sure to only count once even if there are multiple windows
523     // that will all show about:restartrequired.
524     if (this._crashedTabCount == 1) {
525       Services.telemetry.scalarAdd("dom.contentprocess.buildID_mismatch", 1);
526     }
527   },
529   /**
530    * We show a special page to users when a normal browser tab has crashed.
531    * This method should be called to send a browser to that page once the
532    * process has completely closed.
533    *
534    * @param browser (<xul:browser>)
535    *        The browser that has recently crashed.
536    */
537   sendToTabCrashedPage(browser) {
538     let title = browser.contentTitle;
539     let uri = browser.currentURI;
540     let gBrowser = browser.getTabBrowser();
541     let tab = gBrowser.getTabForBrowser(browser);
542     // The tab crashed page is non-remote by default.
543     gBrowser.updateBrowserRemoteness(browser, {
544       remoteType: lazy.E10SUtils.NOT_REMOTE,
545     });
547     browser.setAttribute("crashedPageTitle", title);
548     browser.docShell.displayLoadError(Cr.NS_ERROR_CONTENT_CRASHED, uri, null);
549     browser.removeAttribute("crashedPageTitle");
550     tab.setAttribute("crashed", true);
551     gBrowser.tabContainer.updateTabIndicatorAttr(tab);
552   },
554   /**
555    * Submits a crash report from about:tabcrashed, if the crash
556    * reporter is enabled and a crash report can be found.
557    *
558    * @param browser
559    *        The <xul:browser> that the report was sent from.
560    * @param message
561    *        Message data with the following properties:
562    *
563    *        includeURL (bool):
564    *          Whether to include the URL that the user was on
565    *          in the crashed tab before the crash occurred.
566    *        URL (String)
567    *          The URL that the user was on in the crashed tab
568    *          before the crash occurred.
569    *        comments (String):
570    *          Any additional comments from the user.
571    *
572    *        Note that it is expected that all properties are set,
573    *        even if they are empty.
574    */
575   maybeSendCrashReport(browser, message) {
576     if (!AppConstants.MOZ_CRASHREPORTER) {
577       return;
578     }
580     if (!message.data.hasReport) {
581       // There was no report, so nothing to do.
582       return;
583     }
585     if (message.data.autoSubmit) {
586       // The user has opted in to autosubmitted backlogged
587       // crash reports in the future.
588       UnsubmittedCrashHandler.autoSubmit = true;
589     }
591     let childID = this.browserMap.get(browser);
592     let dumpID = this.childMap.get(childID);
593     if (!dumpID) {
594       return;
595     }
597     if (!message.data.sendReport) {
598       Services.telemetry
599         .getHistogramById("FX_CONTENT_CRASH_NOT_SUBMITTED")
600         .add(1);
601       this.prefs.setBoolPref("sendReport", false);
602       return;
603     }
605     let { includeURL, comments, URL } = message.data;
607     let extraExtraKeyVals = {
608       Comments: comments,
609       URL,
610     };
612     // For the entries in extraExtraKeyVals, we only want to submit the
613     // extra data values where they are not the empty string.
614     for (let key in extraExtraKeyVals) {
615       let val = extraExtraKeyVals[key].trim();
616       if (!val) {
617         delete extraExtraKeyVals[key];
618       }
619     }
621     // URL is special, since it's already been written to extra data by
622     // default. In order to make sure we don't send it, we overwrite it
623     // with the empty string.
624     if (!includeURL) {
625       extraExtraKeyVals.URL = "";
626     }
628     lazy.CrashSubmit.submit(dumpID, lazy.CrashSubmit.SUBMITTED_FROM_CRASH_TAB, {
629       recordSubmission: true,
630       extraExtraKeyVals,
631     }).catch(console.error);
633     this.prefs.setBoolPref("sendReport", true);
634     this.prefs.setBoolPref("includeURL", includeURL);
636     this.childMap.set(childID, null); // Avoid resubmission.
637     this.removeSubmitCheckboxesForSameCrash(childID);
638   },
640   removeSubmitCheckboxesForSameCrash(childID) {
641     for (let window of Services.wm.getEnumerator("navigator:browser")) {
642       if (!window.gMultiProcessBrowser) {
643         continue;
644       }
646       for (let browser of window.gBrowser.browsers) {
647         if (browser.isRemoteBrowser) {
648           continue;
649         }
651         let doc = browser.contentDocument;
652         if (!doc.documentURI.startsWith("about:tabcrashed")) {
653           continue;
654         }
656         if (this.browserMap.get(browser) == childID) {
657           this.browserMap.delete(browser);
658           browser.sendMessageToActor("CrashReportSent", {}, "AboutTabCrashed");
659         }
660       }
661     }
662   },
664   /**
665    * Process a crashed tab loaded into a browser.
666    *
667    * @param browser
668    *        The <xul:browser> containing the page that crashed.
669    * @returns crash data
670    *        Message data containing information about the crash.
671    */
672   onAboutTabCrashedLoad(browser) {
673     this._crashedTabCount++;
675     let window = browser.ownerGlobal;
677     // Reset the zoom for the tabcrashed page.
678     window.ZoomManager.setZoomForBrowser(browser, 1);
680     let childID = this.browserMap.get(browser);
681     let index = this.unseenCrashedChildIDs.indexOf(childID);
682     if (index != -1) {
683       this.unseenCrashedChildIDs.splice(index, 1);
684     }
686     let dumpID = this.getDumpID(browser);
687     if (!dumpID) {
688       return {
689         hasReport: false,
690       };
691     }
693     let requestAutoSubmit = !UnsubmittedCrashHandler.autoSubmit;
694     let sendReport = this.prefs.getBoolPref("sendReport");
695     let includeURL = this.prefs.getBoolPref("includeURL");
697     let data = {
698       hasReport: true,
699       sendReport,
700       includeURL,
701       requestAutoSubmit,
702     };
704     return data;
705   },
707   onAboutTabCrashedUnload(browser) {
708     if (!this._crashedTabCount) {
709       console.error("Can not decrement crashed tab count to below 0");
710       return;
711     }
712     this._crashedTabCount--;
714     let childID = this.browserMap.get(browser);
716     // Make sure to only count once even if there are multiple windows
717     // that will all show about:tabcrashed.
718     if (this._crashedTabCount == 0 && childID) {
719       Services.telemetry
720         .getHistogramById("FX_CONTENT_CRASH_NOT_SUBMITTED")
721         .add(1);
722     }
723   },
725   /**
726    * For some <xul:browser>, return a crash report dump ID for that browser
727    * if we have been informed of one. Otherwise, return null.
728    *
729    * @param browser (<xul:browser)
730    *        The browser to try to get the dump ID for
731    * @returns dumpID (String)
732    */
733   getDumpID(browser) {
734     if (!AppConstants.MOZ_CRASHREPORTER) {
735       return null;
736     }
738     return this.childMap.get(this.browserMap.get(browser));
739   },
741   /**
742    * This is intended for TESTING ONLY. It returns the amount of
743    * content processes that have crashed such that we're still waiting
744    * for dump IDs for their crash reports.
745    *
746    * For our automated tests, accessing the crashed content process
747    * count helps us test the behaviour when content processes crash due
748    * to launch failure, since in those cases we should not increase the
749    * crashed browser queue (since we never receive dump IDs for launch
750    * failures).
751    */
752   get queuedCrashedBrowsers() {
753     return this.crashedBrowserQueues.size;
754   },
758  * This component is responsible for scanning the pending
759  * crash report directory for reports, and (if enabled), to
760  * prompt the user to submit those reports. It might also
761  * submit those reports automatically without prompting if
762  * the user has opted in.
763  */
764 export var UnsubmittedCrashHandler = {
765   get prefs() {
766     delete this.prefs;
767     return (this.prefs = Services.prefs.getBranch(
768       "browser.crashReports.unsubmittedCheck."
769     ));
770   },
772   get enabled() {
773     return this.prefs.getBoolPref("enabled");
774   },
776   // showingNotification is set to true once a notification
777   // is successfully shown, and then set back to false if
778   // the notification is dismissed by an action by the user.
779   showingNotification: false,
780   // suppressed is true if we've determined that we've shown
781   // the notification too many times across too many days without
782   // user interaction, so we're suppressing the notification for
783   // some number of days. See the documentation for
784   // shouldShowPendingSubmissionsNotification().
785   suppressed: false,
787   _checkTimeout: null,
789   init() {
790     if (this.initialized) {
791       return;
792     }
794     this.initialized = true;
796     // UnsubmittedCrashHandler can be initialized but still be disabled.
797     // This is intentional, as this makes simulating UnsubmittedCrashHandler's
798     // reactions to browser startup and shutdown easier in test automation.
799     //
800     // UnsubmittedCrashHandler, when initialized but not enabled, is inert.
801     if (this.enabled) {
802       if (this.prefs.prefHasUserValue("suppressUntilDate")) {
803         if (this.prefs.getCharPref("suppressUntilDate") > this.dateString()) {
804           // We'll be suppressing any notifications until after suppressedDate,
805           // so there's no need to do anything more.
806           this.suppressed = true;
807           return;
808         }
810         // We're done suppressing, so we don't need this pref anymore.
811         this.prefs.clearUserPref("suppressUntilDate");
812       }
814       Services.obs.addObserver(this, "profile-before-change");
815     }
816   },
818   uninit() {
819     if (!this.initialized) {
820       return;
821     }
823     this.initialized = false;
825     if (this._checkTimeout) {
826       lazy.clearTimeout(this._checkTimeout);
827       this._checkTimeout = null;
828     }
830     if (!this.enabled) {
831       return;
832     }
834     if (this.suppressed) {
835       this.suppressed = false;
836       // No need to do any more clean-up, since we were suppressed.
837       return;
838     }
840     if (this.showingNotification) {
841       this.prefs.setBoolPref("shutdownWhileShowing", true);
842       this.showingNotification = false;
843     }
845     Services.obs.removeObserver(this, "profile-before-change");
846   },
848   observe(subject, topic) {
849     switch (topic) {
850       case "profile-before-change": {
851         this.uninit();
852         break;
853       }
854     }
855   },
857   scheduleCheckForUnsubmittedCrashReports() {
858     this._checkTimeout = lazy.setTimeout(() => {
859       Services.tm.idleDispatchToMainThread(() => {
860         this.checkForUnsubmittedCrashReports();
861       });
862     }, CHECK_FOR_UNSUBMITTED_CRASH_REPORTS_DELAY_MS);
863   },
865   /**
866    * Scans the profile directory for unsubmitted crash reports
867    * within the past PENDING_CRASH_REPORT_DAYS days. If it
868    * finds any, it will, if necessary, attempt to open a notification
869    * bar to prompt the user to submit them.
870    *
871    * @returns Promise
872    *          Resolves with the <xul:notification> after it tries to
873    *          show a notification on the most recent browser window.
874    *          If a notification cannot be shown, will resolve with null.
875    */
876   async checkForUnsubmittedCrashReports() {
877     if (!this.enabled || this.suppressed) {
878       return null;
879     }
881     let dateLimit = new Date();
882     dateLimit.setDate(dateLimit.getDate() - PENDING_CRASH_REPORT_DAYS);
884     let reportIDs = [];
885     try {
886       reportIDs = await lazy.CrashSubmit.pendingIDs(dateLimit);
887     } catch (e) {
888       console.error(e);
889       return null;
890     }
892     if (reportIDs.length) {
893       if (this.autoSubmit) {
894         this.submitReports(reportIDs, lazy.CrashSubmit.SUBMITTED_FROM_AUTO);
895       } else if (this.shouldShowPendingSubmissionsNotification()) {
896         return this.showPendingSubmissionsNotification(reportIDs);
897       }
898     }
899     return null;
900   },
902   /**
903    * Returns true if the notification should be shown.
904    * shouldShowPendingSubmissionsNotification makes this decision
905    * by looking at whether or not the user has seen the notification
906    * over several days without ever interacting with it. If this occurs
907    * too many times, we suppress the notification for DAYS_TO_SUPPRESS
908    * days.
909    *
910    * @returns bool
911    */
912   shouldShowPendingSubmissionsNotification() {
913     if (!this.prefs.prefHasUserValue("shutdownWhileShowing")) {
914       return true;
915     }
917     let shutdownWhileShowing = this.prefs.getBoolPref("shutdownWhileShowing");
918     this.prefs.clearUserPref("shutdownWhileShowing");
920     if (!this.prefs.prefHasUserValue("lastShownDate")) {
921       // This isn't expected, but we're being defensive here. We'll
922       // opt for showing the notification in this case.
923       return true;
924     }
926     let lastShownDate = this.prefs.getCharPref("lastShownDate");
927     if (this.dateString() > lastShownDate && shutdownWhileShowing) {
928       // We're on a newer day then when we last showed the
929       // notification without closing it. We don't want to do
930       // this too many times, so we'll decrement a counter for
931       // this situation. Too many of these, and we'll assume the
932       // user doesn't know or care about unsubmitted notifications,
933       // and we'll suppress the notification for a while.
934       let chances = this.prefs.getIntPref("chancesUntilSuppress");
935       if (--chances < 0) {
936         // We're out of chances!
937         this.prefs.clearUserPref("chancesUntilSuppress");
938         // We'll suppress for DAYS_TO_SUPPRESS days.
939         let suppressUntil = this.dateString(
940           new Date(Date.now() + DAY * DAYS_TO_SUPPRESS)
941         );
942         this.prefs.setCharPref("suppressUntilDate", suppressUntil);
943         return false;
944       }
945       this.prefs.setIntPref("chancesUntilSuppress", chances);
946     }
948     return true;
949   },
951   /**
952    * Given an array of unsubmitted crash report IDs, try to open
953    * up a notification asking the user to submit them.
954    *
955    * @param reportIDs (Array<string>)
956    *        The Array of report IDs to offer the user to send.
957    * @returns The <xul:notification> if one is shown. null otherwise.
958    */
959   async showPendingSubmissionsNotification(reportIDs) {
960     if (!reportIDs.length) {
961       return null;
962     }
964     let notification = await this.show({
965       notificationID: "pending-crash-reports",
966       reportIDs,
967       onAction: () => {
968         this.showingNotification = false;
969       },
970     });
972     if (notification) {
973       this.showingNotification = true;
974       this.prefs.setCharPref("lastShownDate", this.dateString());
975     }
977     return notification;
978   },
980   /**
981    * Returns a string representation of a Date in the format
982    * YYYYMMDD.
983    *
984    * @param someDate (Date, optional)
985    *        The Date to convert to the string. If not provided,
986    *        defaults to today's date.
987    * @returns String
988    */
989   dateString(someDate = new Date()) {
990     let year = String(someDate.getFullYear()).padStart(4, "0");
991     let month = String(someDate.getMonth() + 1).padStart(2, "0");
992     let day = String(someDate.getDate()).padStart(2, "0");
993     return year + month + day;
994   },
996   /**
997    * Attempts to show a notification bar to the user in the most
998    * recent browser window asking them to submit some crash report
999    * IDs. If a notification cannot be shown (for example, there
1000    * is no browser window), this method exits silently.
1001    *
1002    * The notification will allow the user to submit their crash
1003    * reports. If the user dismissed the notification, the crash
1004    * reports will be marked to be ignored (though they can
1005    * still be manually submitted via about:crashes).
1006    *
1007    * @param JS Object
1008    *        An Object with the following properties:
1009    *
1010    *        notificationID (string)
1011    *          The ID for the notification to be opened.
1012    *
1013    *        reportIDs (Array<string>)
1014    *          The array of report IDs to offer to the user.
1015    *
1016    *        onAction (function, optional)
1017    *          A callback to fire once the user performs an
1018    *          action on the notification bar (this includes
1019    *          dismissing the notification).
1020    *
1021    * @returns The <xul:notification> if one is shown. null otherwise.
1022    */
1023   show({ notificationID, reportIDs, onAction }) {
1024     let chromeWin = lazy.BrowserWindowTracker.getTopWindow();
1025     if (!chromeWin) {
1026       // Can't show a notification in this case. We'll hopefully
1027       // get another opportunity to have the user submit their
1028       // crash reports later.
1029       return null;
1030     }
1032     let notification =
1033       chromeWin.gNotificationBox.getNotificationWithValue(notificationID);
1034     if (notification) {
1035       return null;
1036     }
1038     chromeWin.MozXULElement.insertFTLIfNeeded("browser/contentCrash.ftl");
1040     let buttons = [
1041       {
1042         "l10n-id": "pending-crash-reports-send",
1043         callback: () => {
1044           this.submitReports(
1045             reportIDs,
1046             lazy.CrashSubmit.SUBMITTED_FROM_INFOBAR
1047           );
1048           if (onAction) {
1049             onAction();
1050           }
1051         },
1052       },
1053       {
1054         "l10n-id": "pending-crash-reports-always-send",
1055         callback: () => {
1056           this.autoSubmit = true;
1057           this.submitReports(
1058             reportIDs,
1059             lazy.CrashSubmit.SUBMITTED_FROM_INFOBAR
1060           );
1061           if (onAction) {
1062             onAction();
1063           }
1064         },
1065       },
1066       {
1067         "l10n-id": "pending-crash-reports-view-all",
1068         callback() {
1069           chromeWin.openTrustedLinkIn("about:crashes", "tab");
1070           return true;
1071         },
1072       },
1073     ];
1075     let eventCallback = eventType => {
1076       if (eventType == "dismissed") {
1077         // The user intentionally dismissed the notification,
1078         // which we interpret as meaning that they don't care
1079         // to submit the reports. We'll ignore these particular
1080         // reports going forward.
1081         reportIDs.forEach(function (reportID) {
1082           lazy.CrashSubmit.ignore(reportID);
1083         });
1084         if (onAction) {
1085           onAction();
1086         }
1087       }
1088     };
1090     return chromeWin.gNotificationBox.appendNotification(
1091       notificationID,
1092       {
1093         label: {
1094           "l10n-id": "pending-crash-reports-message",
1095           "l10n-args": { reportCount: reportIDs.length },
1096         },
1097         image: TABCRASHED_ICON_URI,
1098         priority: chromeWin.gNotificationBox.PRIORITY_INFO_HIGH,
1099         eventCallback,
1100       },
1101       buttons
1102     );
1103   },
1105   get autoSubmit() {
1106     return Services.prefs.getBoolPref(
1107       "browser.crashReports.unsubmittedCheck.autoSubmit2"
1108     );
1109   },
1111   set autoSubmit(val) {
1112     Services.prefs.setBoolPref(
1113       "browser.crashReports.unsubmittedCheck.autoSubmit2",
1114       val
1115     );
1116   },
1118   /**
1119    * Attempt to submit reports to the crash report server.
1120    *
1121    * @param reportIDs (Array<string>)
1122    *        The array of reportIDs to submit.
1123    * @param submittedFrom (string)
1124    *        One of the CrashSubmit.SUBMITTED_FROM_* constants representing
1125    *        how this crash was submitted.
1126    */
1127   submitReports(reportIDs, submittedFrom) {
1128     for (let reportID of reportIDs) {
1129       lazy.CrashSubmit.submit(reportID, submittedFrom).catch(console.error);
1130     }
1131   },