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";
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",
18 // We don't process crash reports older than 28 days, so don't bother
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";
38 * BrowserWeakMap is exactly like a WeakMap, but expects <xul:browser>
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.
45 class BrowserWeakMap extends WeakMap {
47 if (browser.permanentKey) {
48 return super.get(browser.permanentKey);
50 return super.get(browser);
54 if (browser.permanentKey) {
55 return super.set(browser.permanentKey, value);
57 return super.set(browser, value);
61 if (browser.permanentKey) {
62 return super.delete(browser.permanentKey);
64 return super.delete(browser);
68 export var TabCrashHandler = {
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,
82 return (this.prefs = Services.prefs.getBranch(
83 "browser.tabs.crashReporting."
88 if (this.initialized) {
91 this.initialized = true;
93 Services.obs.addObserver(this, "ipc:content-shutdown");
94 Services.obs.addObserver(this, "oop-frameloader-crashed");
97 observe(aSubject, aTopic) {
99 case "ipc:content-shutdown": {
100 aSubject.QueryInterface(Ci.nsIPropertyBag2);
102 if (!aSubject.get("abnormal")) {
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);
114 .getHistogramById("FX_CONTENT_CRASH_DUMP_UNAVAILABLE")
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) {
123 ChromeUtils.nondeterministicGetWeakMapKeys(subframeCrashItem) ||
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);
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
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.
151 this.unseenCrashedChildIDs.length > MAX_UNSEEN_CRASHED_CHILD_IDS
153 this.unseenCrashedChildIDs.shift();
157 // check for environment affecting crash reporting
158 let shutdown = Services.env.exists("MOZ_CRASHREPORTER_SHUTDOWN");
162 "A content process crashed and MOZ_CRASHREPORTER_SHUTDOWN is " +
163 "set, shutting down\n"
165 Services.startup.quit(
166 Ci.nsIAppStartup.eForceQuit,
167 EXIT_CODE_CONTENT_CRASHED
173 case "oop-frameloader-crashed": {
174 let browser = aSubject.ownerElement;
179 this.browserMap.set(browser, aSubject.childID);
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.
191 * @param childID (int)
192 * The childID of the content process that just crashed.
194 * True if one or more browsers were sent to the tab crashed
197 flushCrashedBrowserQueue(childID) {
198 let browserQueue = this.crashedBrowserQueues.get(childID);
203 this.crashedBrowserQueues.delete(childID);
205 let sentBrowser = false;
206 for (let weakBrowser of browserQueue) {
207 let browser = weakBrowser.get();
210 this.restartRequiredBrowsers.has(browser) ||
211 this.testBuildIDMismatch
213 this.sendToRestartRequiredPage(browser);
215 this.sendToTabCrashedPage(browser);
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.
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.
234 onSelectedBrowserCrash(browser, restartRequired) {
235 if (!browser.isRemoteBrowser) {
236 console.error("Selected crashed browser is not remote.");
239 if (!browser.frameLoader) {
240 console.error("Selected crashed browser has no frameloader.");
244 let childID = browser.frameLoader.childID;
246 let browserQueue = this.crashedBrowserQueues.get(childID);
249 this.crashedBrowserQueues.set(childID, browserQueue);
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
257 browserQueue.push(Cu.getWeakReference(browser));
259 if (restartRequired) {
260 this.restartRequiredBrowsers.add(browser);
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.
268 this.flushCrashedBrowserQueue(0);
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.
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.
283 onBackgroundBrowserCrash(browser, restartRequired) {
284 if (restartRequired) {
285 this.restartRequiredBrowsers.add(browser);
288 let gBrowser = browser.getTabBrowser();
289 let tab = gBrowser.getTabForBrowser(browser);
291 gBrowser.updateBrowserRemoteness(browser, {
292 remoteType: lazy.E10SUtils.NOT_REMOTE,
295 lazy.SessionStore.reviveCrashedTab(tab);
299 * Called when a subframe crashes. If the dump is available, shows a subframe
300 * crashed notification, otherwise waits for one to be available.
302 * @param browser (<xul:browser>)
303 * The browser containing the frame that just crashed.
305 * The id of the process that just crashed.
307 async onSubFrameCrash(browser, childID) {
308 if (!AppConstants.MOZ_CRASHREPORTER) {
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);
317 this.showSubFrameNotification(browser, childID, dumpID);
319 let item = this.pendingSubFrameCrashes.get(childID);
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.
328 this.pendingSubFrameCrashesIDs.length >=
329 MAX_UNSEEN_CRASHED_SUBFRAME_IDS
331 let idToDelete = this.pendingSubFrameCrashesIDs.shift();
332 this.pendingSubFrameCrashes.delete(idToDelete);
334 this.pendingSubFrameCrashesIDs.push(childID);
336 item.set(browser, browser);
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.
345 * @param childID number
346 * childID of the content that crashed.
347 * @returns subframe crash info added by previous call to onSubFrameCrash.
349 getAndRemoveSubframeCrash(childID) {
350 let item = this.pendingSubFrameCrashes.get(childID);
352 this.pendingSubFrameCrashes.delete(childID);
353 let idx = this.pendingSubFrameCrashesIDs.indexOf(childID);
355 this.pendingSubFrameCrashesIDs.splice(idx, 1);
363 * Called to indicate that a subframe within a browser has crashed. A notification
366 * @param browser (<xul:browser>)
367 * The browser containing the frame that just crashed.
369 * The id of the process that just crashed.
371 * Minidump id of the crash.
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);
380 // Don't show multiple notifications for a browser.
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);
389 for (let notif of existingItem.slice()) {
395 gBrowser.ownerGlobal.MozXULElement.insertFTLIfNeeded(
396 "browser/contentCrash.ftl"
401 "l10n-id": "crashed-subframe-learnmore-link",
403 link: SUBFRAMECRASH_LEARNMORE_URI,
406 "l10n-id": "crashed-subframe-submit",
408 callback: async () => {
410 UnsubmittedCrashHandler.submitReports(
412 lazy.CrashSubmit.SUBMITTED_FROM_CRASH_TAB
415 closeAllNotifications();
420 notification = await notificationBox.appendNotification(
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);
430 let idx = existingItem.indexOf(notification);
432 existingItem.splice(idx, 1);
435 if (!existingItem.length) {
436 this.notificationsMap.delete(childID);
439 } else if (eventName == "dismissed") {
441 lazy.CrashSubmit.ignore(dumpID);
442 this.childMap.delete(childID);
445 closeAllNotifications();
452 let existingItem = this.notificationsMap.get(childID);
454 existingItem.push(notification);
456 this.notificationsMap.set(childID, [notification]);
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.
468 * @param browser (<xul:browser>)
469 * A browser from a browser tab that the user has just selected
470 * to restore on demand.
472 * True if TabCrashHandler will send the user to the tab crash
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
481 // 3) The user is not configured to automatically submit backlogged
482 // crash reports. If they are, we'll send the crash report
484 if (childID && this.unseenCrashedChildIDs.includes(childID)) {
485 if (UnsubmittedCrashHandler.autoSubmit) {
486 let dumpID = this.childMap.get(childID);
488 UnsubmittedCrashHandler.submitReports(
490 lazy.CrashSubmit.SUBMITTED_FROM_AUTO
494 this.sendToTabCrashedPage(browser);
497 } else if (childID === 0) {
498 if (this.restartRequiredBrowsers.has(browser)) {
499 this.sendToRestartRequiredPage(browser);
501 this.sendToTabCrashedPage(browser);
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,
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);
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.
534 * @param browser (<xul:browser>)
535 * The browser that has recently crashed.
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,
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);
555 * Submits a crash report from about:tabcrashed, if the crash
556 * reporter is enabled and a crash report can be found.
559 * The <xul:browser> that the report was sent from.
561 * Message data with the following properties:
564 * Whether to include the URL that the user was on
565 * in the crashed tab before the crash occurred.
567 * The URL that the user was on in the crashed tab
568 * before the crash occurred.
570 * Any additional comments from the user.
572 * Note that it is expected that all properties are set,
573 * even if they are empty.
575 maybeSendCrashReport(browser, message) {
576 if (!AppConstants.MOZ_CRASHREPORTER) {
580 if (!message.data.hasReport) {
581 // There was no report, so nothing to do.
585 if (message.data.autoSubmit) {
586 // The user has opted in to autosubmitted backlogged
587 // crash reports in the future.
588 UnsubmittedCrashHandler.autoSubmit = true;
591 let childID = this.browserMap.get(browser);
592 let dumpID = this.childMap.get(childID);
597 if (!message.data.sendReport) {
599 .getHistogramById("FX_CONTENT_CRASH_NOT_SUBMITTED")
601 this.prefs.setBoolPref("sendReport", false);
605 let { includeURL, comments, URL } = message.data;
607 let extraExtraKeyVals = {
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();
617 delete extraExtraKeyVals[key];
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.
625 extraExtraKeyVals.URL = "";
628 lazy.CrashSubmit.submit(dumpID, lazy.CrashSubmit.SUBMITTED_FROM_CRASH_TAB, {
629 recordSubmission: true,
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);
640 removeSubmitCheckboxesForSameCrash(childID) {
641 for (let window of Services.wm.getEnumerator("navigator:browser")) {
642 if (!window.gMultiProcessBrowser) {
646 for (let browser of window.gBrowser.browsers) {
647 if (browser.isRemoteBrowser) {
651 let doc = browser.contentDocument;
652 if (!doc.documentURI.startsWith("about:tabcrashed")) {
656 if (this.browserMap.get(browser) == childID) {
657 this.browserMap.delete(browser);
658 browser.sendMessageToActor("CrashReportSent", {}, "AboutTabCrashed");
665 * Process a crashed tab loaded into a browser.
668 * The <xul:browser> containing the page that crashed.
669 * @returns crash data
670 * Message data containing information about the crash.
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);
683 this.unseenCrashedChildIDs.splice(index, 1);
686 let dumpID = this.getDumpID(browser);
693 let requestAutoSubmit = !UnsubmittedCrashHandler.autoSubmit;
694 let sendReport = this.prefs.getBoolPref("sendReport");
695 let includeURL = this.prefs.getBoolPref("includeURL");
707 onAboutTabCrashedUnload(browser) {
708 if (!this._crashedTabCount) {
709 console.error("Can not decrement crashed tab count to below 0");
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) {
720 .getHistogramById("FX_CONTENT_CRASH_NOT_SUBMITTED")
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.
729 * @param browser (<xul:browser)
730 * The browser to try to get the dump ID for
731 * @returns dumpID (String)
734 if (!AppConstants.MOZ_CRASHREPORTER) {
738 return this.childMap.get(this.browserMap.get(browser));
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.
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
752 get queuedCrashedBrowsers() {
753 return this.crashedBrowserQueues.size;
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.
764 export var UnsubmittedCrashHandler = {
767 return (this.prefs = Services.prefs.getBranch(
768 "browser.crashReports.unsubmittedCheck."
773 return this.prefs.getBoolPref("enabled");
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().
790 if (this.initialized) {
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.
800 // UnsubmittedCrashHandler, when initialized but not enabled, is inert.
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;
810 // We're done suppressing, so we don't need this pref anymore.
811 this.prefs.clearUserPref("suppressUntilDate");
814 Services.obs.addObserver(this, "profile-before-change");
819 if (!this.initialized) {
823 this.initialized = false;
825 if (this._checkTimeout) {
826 lazy.clearTimeout(this._checkTimeout);
827 this._checkTimeout = null;
834 if (this.suppressed) {
835 this.suppressed = false;
836 // No need to do any more clean-up, since we were suppressed.
840 if (this.showingNotification) {
841 this.prefs.setBoolPref("shutdownWhileShowing", true);
842 this.showingNotification = false;
845 Services.obs.removeObserver(this, "profile-before-change");
848 observe(subject, topic) {
850 case "profile-before-change": {
857 scheduleCheckForUnsubmittedCrashReports() {
858 this._checkTimeout = lazy.setTimeout(() => {
859 Services.tm.idleDispatchToMainThread(() => {
860 this.checkForUnsubmittedCrashReports();
862 }, CHECK_FOR_UNSUBMITTED_CRASH_REPORTS_DELAY_MS);
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.
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.
876 async checkForUnsubmittedCrashReports() {
877 if (!this.enabled || this.suppressed) {
881 let dateLimit = new Date();
882 dateLimit.setDate(dateLimit.getDate() - PENDING_CRASH_REPORT_DAYS);
886 reportIDs = await lazy.CrashSubmit.pendingIDs(dateLimit);
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);
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
912 shouldShowPendingSubmissionsNotification() {
913 if (!this.prefs.prefHasUserValue("shutdownWhileShowing")) {
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.
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");
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)
942 this.prefs.setCharPref("suppressUntilDate", suppressUntil);
945 this.prefs.setIntPref("chancesUntilSuppress", chances);
952 * Given an array of unsubmitted crash report IDs, try to open
953 * up a notification asking the user to submit them.
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.
959 async showPendingSubmissionsNotification(reportIDs) {
960 if (!reportIDs.length) {
964 let notification = await this.show({
965 notificationID: "pending-crash-reports",
968 this.showingNotification = false;
973 this.showingNotification = true;
974 this.prefs.setCharPref("lastShownDate", this.dateString());
981 * Returns a string representation of a Date in the format
984 * @param someDate (Date, optional)
985 * The Date to convert to the string. If not provided,
986 * defaults to today's date.
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;
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.
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).
1008 * An Object with the following properties:
1010 * notificationID (string)
1011 * The ID for the notification to be opened.
1013 * reportIDs (Array<string>)
1014 * The array of report IDs to offer to the user.
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).
1021 * @returns The <xul:notification> if one is shown. null otherwise.
1023 show({ notificationID, reportIDs, onAction }) {
1024 let chromeWin = lazy.BrowserWindowTracker.getTopWindow();
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.
1033 chromeWin.gNotificationBox.getNotificationWithValue(notificationID);
1038 chromeWin.MozXULElement.insertFTLIfNeeded("browser/contentCrash.ftl");
1042 "l10n-id": "pending-crash-reports-send",
1046 lazy.CrashSubmit.SUBMITTED_FROM_INFOBAR
1054 "l10n-id": "pending-crash-reports-always-send",
1056 this.autoSubmit = true;
1059 lazy.CrashSubmit.SUBMITTED_FROM_INFOBAR
1067 "l10n-id": "pending-crash-reports-view-all",
1069 chromeWin.openTrustedLinkIn("about:crashes", "tab");
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);
1090 return chromeWin.gNotificationBox.appendNotification(
1094 "l10n-id": "pending-crash-reports-message",
1095 "l10n-args": { reportCount: reportIDs.length },
1097 image: TABCRASHED_ICON_URI,
1098 priority: chromeWin.gNotificationBox.PRIORITY_INFO_HIGH,
1106 return Services.prefs.getBoolPref(
1107 "browser.crashReports.unsubmittedCheck.autoSubmit2"
1111 set autoSubmit(val) {
1112 Services.prefs.setBoolPref(
1113 "browser.crashReports.unsubmittedCheck.autoSubmit2",
1119 * Attempt to submit reports to the crash report server.
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.
1127 submitReports(reportIDs, submittedFrom) {
1128 for (let reportID of reportIDs) {
1129 lazy.CrashSubmit.submit(reportID, submittedFrom).catch(console.error);