Bug 1908842: update password generator prompt text r=android-reviewers,gl
[gecko.git] / browser / modules / ContentCrashHandlers.sys.mjs
blobabd1961483a8da8027cd80b78060155b2c42c3aa
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   log: null,
791   init() {
792     if (this.initialized) {
793       return;
794     }
796     this.initialized = true;
798     this.log = console.createInstance({
799       prefix: "UnsubmittedCrashHandler",
800       maxLogLevel: this.prefs.getStringPref("loglevel", "Error"),
801     });
803     // UnsubmittedCrashHandler can be initialized but still be disabled.
804     // This is intentional, as this makes simulating UnsubmittedCrashHandler's
805     // reactions to browser startup and shutdown easier in test automation.
806     //
807     // UnsubmittedCrashHandler, when initialized but not enabled, is inert.
808     if (this.enabled) {
809       if (this.prefs.prefHasUserValue("suppressUntilDate")) {
810         if (this.prefs.getCharPref("suppressUntilDate") > this.dateString()) {
811           // We'll be suppressing any notifications until after suppressedDate,
812           // so there's no need to do anything more.
813           this.suppressed = true;
814           this.log.debug("suppressing crash handler due to suppressUntilDate");
815           return;
816         }
818         // We're done suppressing, so we don't need this pref anymore.
819         this.prefs.clearUserPref("suppressUntilDate");
820       }
822       Services.obs.addObserver(this, "profile-before-change");
823     } else {
824       this.log.debug("not enabled");
825     }
826   },
828   uninit() {
829     if (!this.initialized) {
830       return;
831     }
833     this.initialized = false;
835     this.log = null;
837     if (this._checkTimeout) {
838       lazy.clearTimeout(this._checkTimeout);
839       this._checkTimeout = null;
840     }
842     if (!this.enabled) {
843       return;
844     }
846     if (this.suppressed) {
847       this.suppressed = false;
848       // No need to do any more clean-up, since we were suppressed.
849       return;
850     }
852     if (this.showingNotification) {
853       this.prefs.setBoolPref("shutdownWhileShowing", true);
854       this.showingNotification = false;
855     }
857     Services.obs.removeObserver(this, "profile-before-change");
858   },
860   observe(subject, topic) {
861     switch (topic) {
862       case "profile-before-change": {
863         this.uninit();
864         break;
865       }
866     }
867   },
869   scheduleCheckForUnsubmittedCrashReports() {
870     this._checkTimeout = lazy.setTimeout(() => {
871       Services.tm.idleDispatchToMainThread(() => {
872         this.checkForUnsubmittedCrashReports();
873       });
874     }, CHECK_FOR_UNSUBMITTED_CRASH_REPORTS_DELAY_MS);
875   },
877   /**
878    * Scans the profile directory for unsubmitted crash reports
879    * within the past PENDING_CRASH_REPORT_DAYS days. If it
880    * finds any, it will, if necessary, attempt to open a notification
881    * bar to prompt the user to submit them.
882    *
883    * @returns Promise
884    *          Resolves with the <xul:notification> after it tries to
885    *          show a notification on the most recent browser window.
886    *          If a notification cannot be shown, will resolve with null.
887    */
888   async checkForUnsubmittedCrashReports() {
889     if (!this.enabled || this.suppressed) {
890       return null;
891     }
893     this.log.debug("checking for unsubmitted crash reports");
895     let dateLimit = new Date();
896     dateLimit.setDate(dateLimit.getDate() - PENDING_CRASH_REPORT_DAYS);
898     let reportIDs = [];
899     try {
900       reportIDs = await lazy.CrashSubmit.pendingIDs(dateLimit);
901     } catch (e) {
902       this.log.error(e);
903       return null;
904     }
906     if (reportIDs.length) {
907       this.log.debug("found ", reportIDs.length, " unsubmitted crash reports");
908       Glean.crashSubmission.pending.add(reportIDs.length);
909       if (this.autoSubmit) {
910         this.log.debug("auto submitted crash reports");
911         this.submitReports(reportIDs, lazy.CrashSubmit.SUBMITTED_FROM_AUTO);
912       } else if (this.shouldShowPendingSubmissionsNotification()) {
913         return this.showPendingSubmissionsNotification(reportIDs);
914       }
915     }
916     return null;
917   },
919   /**
920    * Returns true if the notification should be shown.
921    * shouldShowPendingSubmissionsNotification makes this decision
922    * by looking at whether or not the user has seen the notification
923    * over several days without ever interacting with it. If this occurs
924    * too many times, we suppress the notification for DAYS_TO_SUPPRESS
925    * days.
926    *
927    * @returns bool
928    */
929   shouldShowPendingSubmissionsNotification() {
930     if (!this.prefs.prefHasUserValue("shutdownWhileShowing")) {
931       return true;
932     }
934     let shutdownWhileShowing = this.prefs.getBoolPref("shutdownWhileShowing");
935     this.prefs.clearUserPref("shutdownWhileShowing");
937     if (!this.prefs.prefHasUserValue("lastShownDate")) {
938       // This isn't expected, but we're being defensive here. We'll
939       // opt for showing the notification in this case.
940       return true;
941     }
943     let lastShownDate = this.prefs.getCharPref("lastShownDate");
944     if (this.dateString() > lastShownDate && shutdownWhileShowing) {
945       // We're on a newer day then when we last showed the
946       // notification without closing it. We don't want to do
947       // this too many times, so we'll decrement a counter for
948       // this situation. Too many of these, and we'll assume the
949       // user doesn't know or care about unsubmitted notifications,
950       // and we'll suppress the notification for a while.
951       let chances = this.prefs.getIntPref("chancesUntilSuppress");
952       if (--chances < 0) {
953         // We're out of chances!
954         this.prefs.clearUserPref("chancesUntilSuppress");
955         // We'll suppress for DAYS_TO_SUPPRESS days.
956         let suppressUntil = this.dateString(
957           new Date(Date.now() + DAY * DAYS_TO_SUPPRESS)
958         );
959         this.prefs.setCharPref("suppressUntilDate", suppressUntil);
960         return false;
961       }
962       this.prefs.setIntPref("chancesUntilSuppress", chances);
963     }
965     return true;
966   },
968   /**
969    * Given an array of unsubmitted crash report IDs, try to open
970    * up a notification asking the user to submit them.
971    *
972    * @param reportIDs (Array<string>)
973    *        The Array of report IDs to offer the user to send.
974    * @returns The <xul:notification> if one is shown. null otherwise.
975    */
976   async showPendingSubmissionsNotification(reportIDs) {
977     if (!reportIDs.length) {
978       return null;
979     }
981     this.log.debug("showing pending submissions notification");
983     let notification = await this.show({
984       notificationID: "pending-crash-reports",
985       reportIDs,
986       onAction: () => {
987         this.showingNotification = false;
988       },
989     });
991     if (notification) {
992       this.showingNotification = true;
993       this.prefs.setCharPref("lastShownDate", this.dateString());
994     }
996     return notification;
997   },
999   /**
1000    * Returns a string representation of a Date in the format
1001    * YYYYMMDD.
1002    *
1003    * @param someDate (Date, optional)
1004    *        The Date to convert to the string. If not provided,
1005    *        defaults to today's date.
1006    * @returns String
1007    */
1008   dateString(someDate = new Date()) {
1009     let year = String(someDate.getFullYear()).padStart(4, "0");
1010     let month = String(someDate.getMonth() + 1).padStart(2, "0");
1011     let day = String(someDate.getDate()).padStart(2, "0");
1012     return year + month + day;
1013   },
1015   /**
1016    * Attempts to show a notification bar to the user in the most
1017    * recent browser window asking them to submit some crash report
1018    * IDs. If a notification cannot be shown (for example, there
1019    * is no browser window), this method exits silently.
1020    *
1021    * The notification will allow the user to submit their crash
1022    * reports. If the user dismissed the notification, the crash
1023    * reports will be marked to be ignored (though they can
1024    * still be manually submitted via about:crashes).
1025    *
1026    * @param JS Object
1027    *        An Object with the following properties:
1028    *
1029    *        notificationID (string)
1030    *          The ID for the notification to be opened.
1031    *
1032    *        reportIDs (Array<string>)
1033    *          The array of report IDs to offer to the user.
1034    *
1035    *        onAction (function, optional)
1036    *          A callback to fire once the user performs an
1037    *          action on the notification bar (this includes
1038    *          dismissing the notification).
1039    *
1040    * @returns The <xul:notification> if one is shown. null otherwise.
1041    */
1042   show({ notificationID, reportIDs, onAction }) {
1043     let chromeWin = lazy.BrowserWindowTracker.getTopWindow();
1044     if (!chromeWin) {
1045       // Can't show a notification in this case. We'll hopefully
1046       // get another opportunity to have the user submit their
1047       // crash reports later.
1048       return null;
1049     }
1051     let notification =
1052       chromeWin.gNotificationBox.getNotificationWithValue(notificationID);
1053     if (notification) {
1054       return null;
1055     }
1057     chromeWin.MozXULElement.insertFTLIfNeeded("browser/contentCrash.ftl");
1059     let buttons = [
1060       {
1061         "l10n-id": "pending-crash-reports-send",
1062         callback: () => {
1063           this.submitReports(
1064             reportIDs,
1065             lazy.CrashSubmit.SUBMITTED_FROM_INFOBAR
1066           );
1067           if (onAction) {
1068             onAction();
1069           }
1070         },
1071       },
1072       {
1073         "l10n-id": "pending-crash-reports-always-send",
1074         callback: () => {
1075           this.autoSubmit = true;
1076           this.submitReports(
1077             reportIDs,
1078             lazy.CrashSubmit.SUBMITTED_FROM_INFOBAR
1079           );
1080           if (onAction) {
1081             onAction();
1082           }
1083         },
1084       },
1085       {
1086         "l10n-id": "pending-crash-reports-view-all",
1087         callback() {
1088           chromeWin.openTrustedLinkIn("about:crashes", "tab");
1089           return true;
1090         },
1091       },
1092     ];
1094     let eventCallback = eventType => {
1095       if (eventType == "dismissed") {
1096         // The user intentionally dismissed the notification,
1097         // which we interpret as meaning that they don't care
1098         // to submit the reports. We'll ignore these particular
1099         // reports going forward.
1100         reportIDs.forEach(function (reportID) {
1101           lazy.CrashSubmit.ignore(reportID);
1102         });
1103         if (onAction) {
1104           onAction();
1105         }
1106       }
1107     };
1109     return chromeWin.gNotificationBox.appendNotification(
1110       notificationID,
1111       {
1112         label: {
1113           "l10n-id": "pending-crash-reports-message",
1114           "l10n-args": { reportCount: reportIDs.length },
1115         },
1116         image: TABCRASHED_ICON_URI,
1117         priority: chromeWin.gNotificationBox.PRIORITY_INFO_HIGH,
1118         eventCallback,
1119       },
1120       buttons
1121     );
1122   },
1124   get autoSubmit() {
1125     return Services.prefs.getBoolPref(
1126       "browser.crashReports.unsubmittedCheck.autoSubmit2"
1127     );
1128   },
1130   set autoSubmit(val) {
1131     Services.prefs.setBoolPref(
1132       "browser.crashReports.unsubmittedCheck.autoSubmit2",
1133       val
1134     );
1135   },
1137   /**
1138    * Attempt to submit reports to the crash report server.
1139    *
1140    * @param reportIDs (Array<string>)
1141    *        The array of reportIDs to submit.
1142    * @param submittedFrom (string)
1143    *        One of the CrashSubmit.SUBMITTED_FROM_* constants representing
1144    *        how this crash was submitted.
1145    */
1146   submitReports(reportIDs, submittedFrom) {
1147     this.log.debug(
1148       "submitting ",
1149       reportIDs.length,
1150       " reports from ",
1151       submittedFrom
1152     );
1153     for (let reportID of reportIDs) {
1154       lazy.CrashSubmit.submit(reportID, submittedFrom).catch(
1155         this.log.error.bind(this.log)
1156       );
1157     }
1158   },