summaryrefslogtreecommitdiffstats
path: root/browser/modules/ContentCrashHandlers.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'browser/modules/ContentCrashHandlers.sys.mjs')
-rw-r--r--browser/modules/ContentCrashHandlers.sys.mjs1132
1 files changed, 1132 insertions, 0 deletions
diff --git a/browser/modules/ContentCrashHandlers.sys.mjs b/browser/modules/ContentCrashHandlers.sys.mjs
new file mode 100644
index 0000000000..4ab6f600cd
--- /dev/null
+++ b/browser/modules/ContentCrashHandlers.sys.mjs
@@ -0,0 +1,1132 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
+ CrashSubmit: "resource://gre/modules/CrashSubmit.sys.mjs",
+ E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
+ SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
+ clearTimeout: "resource://gre/modules/Timer.sys.mjs",
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+});
+
+// We don't process crash reports older than 28 days, so don't bother
+// submitting them
+const PENDING_CRASH_REPORT_DAYS = 28;
+const DAY = 24 * 60 * 60 * 1000; // milliseconds
+const DAYS_TO_SUPPRESS = 30;
+const MAX_UNSEEN_CRASHED_CHILD_IDS = 20;
+const MAX_UNSEEN_CRASHED_SUBFRAME_IDS = 10;
+
+// Time after which we will begin scanning for unsubmitted crash reports
+const CHECK_FOR_UNSUBMITTED_CRASH_REPORTS_DELAY_MS = 60 * 10000; // 10 minutes
+
+// This is SIGUSR1 and indicates a user-invoked crash
+const EXIT_CODE_CONTENT_CRASHED = 245;
+
+const TABCRASHED_ICON_URI = "chrome://browser/skin/tab-crashed.svg";
+
+const SUBFRAMECRASH_LEARNMORE_URI =
+ "https://support.mozilla.org/kb/firefox-crashes-troubleshoot-prevent-and-get-help";
+
+/**
+ * BrowserWeakMap is exactly like a WeakMap, but expects <xul:browser>
+ * objects only.
+ *
+ * Under the hood, BrowserWeakMap keys the map off of the <xul:browser>
+ * permanentKey. If, however, the browser has never gotten a permanentKey,
+ * it falls back to keying on the <xul:browser> element itself.
+ */
+class BrowserWeakMap extends WeakMap {
+ get(browser) {
+ if (browser.permanentKey) {
+ return super.get(browser.permanentKey);
+ }
+ return super.get(browser);
+ }
+
+ set(browser, value) {
+ if (browser.permanentKey) {
+ return super.set(browser.permanentKey, value);
+ }
+ return super.set(browser, value);
+ }
+
+ delete(browser) {
+ if (browser.permanentKey) {
+ return super.delete(browser.permanentKey);
+ }
+ return super.delete(browser);
+ }
+}
+
+export var TabCrashHandler = {
+ _crashedTabCount: 0,
+ childMap: new Map(),
+ browserMap: new BrowserWeakMap(),
+ notificationsMap: new Map(),
+ unseenCrashedChildIDs: [],
+ pendingSubFrameCrashes: new Map(),
+ pendingSubFrameCrashesIDs: [],
+ crashedBrowserQueues: new Map(),
+ restartRequiredBrowsers: new WeakSet(),
+ testBuildIDMismatch: false,
+
+ get prefs() {
+ delete this.prefs;
+ return (this.prefs = Services.prefs.getBranch(
+ "browser.tabs.crashReporting."
+ ));
+ },
+
+ init() {
+ if (this.initialized) {
+ return;
+ }
+ this.initialized = true;
+
+ Services.obs.addObserver(this, "ipc:content-shutdown");
+ Services.obs.addObserver(this, "oop-frameloader-crashed");
+ },
+
+ observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "ipc:content-shutdown": {
+ aSubject.QueryInterface(Ci.nsIPropertyBag2);
+
+ if (!aSubject.get("abnormal")) {
+ return;
+ }
+
+ let childID = aSubject.get("childID");
+ let dumpID = aSubject.get("dumpID");
+
+ // Get and remove the subframe crash info first.
+ let subframeCrashItem = this.getAndRemoveSubframeCrash(childID);
+
+ if (!dumpID) {
+ Services.telemetry
+ .getHistogramById("FX_CONTENT_CRASH_DUMP_UNAVAILABLE")
+ .add(1);
+ } else if (AppConstants.MOZ_CRASHREPORTER) {
+ this.childMap.set(childID, dumpID);
+
+ // If this is a subframe crash, show the crash notification. Only
+ // show subframe notifications when there is a minidump available.
+ if (subframeCrashItem) {
+ let browsers =
+ ChromeUtils.nondeterministicGetWeakMapKeys(subframeCrashItem) ||
+ [];
+ for (let browserItem of browsers) {
+ let browser = subframeCrashItem.get(browserItem);
+ if (browser.isConnected && !browser.ownerGlobal.closed) {
+ this.showSubFrameNotification(browser, childID, dumpID);
+ }
+ }
+ }
+ }
+
+ if (!this.flushCrashedBrowserQueue(childID)) {
+ this.unseenCrashedChildIDs.push(childID);
+ // The elements in unseenCrashedChildIDs will only be removed if
+ // the tab crash page is shown. However, ipc:content-shutdown might
+ // be fired for processes for which we'll never show the tab crash
+ // page - for example, the thumbnailing process. Another case to
+ // consider is if the user is configured to submit backlogged crash
+ // reports automatically, and a background tab crashes. In that case,
+ // we will never show the tab crash page, and never remove the element
+ // from the list.
+ //
+ // Instead of trying to account for all of those cases, we prevent
+ // this list from getting too large by putting a reasonable upper
+ // limit on how many childIDs we track. It's unlikely that this
+ // array would ever get so large as to be unwieldy (that'd be a lot
+ // or crashes!), but a leak is a leak.
+ if (
+ this.unseenCrashedChildIDs.length > MAX_UNSEEN_CRASHED_CHILD_IDS
+ ) {
+ this.unseenCrashedChildIDs.shift();
+ }
+ }
+
+ // check for environment affecting crash reporting
+ let shutdown = Services.env.exists("MOZ_CRASHREPORTER_SHUTDOWN");
+
+ if (shutdown) {
+ dump(
+ "A content process crashed and MOZ_CRASHREPORTER_SHUTDOWN is " +
+ "set, shutting down\n"
+ );
+ Services.startup.quit(
+ Ci.nsIAppStartup.eForceQuit,
+ EXIT_CODE_CONTENT_CRASHED
+ );
+ }
+
+ break;
+ }
+ case "oop-frameloader-crashed": {
+ let browser = aSubject.ownerElement;
+ if (!browser) {
+ return;
+ }
+
+ this.browserMap.set(browser, aSubject.childID);
+ break;
+ }
+ }
+ },
+
+ /**
+ * This should be called once a content process has finished
+ * shutting down abnormally. Any tabbrowser browsers that were
+ * selected at the time of the crash will then be sent to
+ * the crashed tab page.
+ *
+ * @param childID (int)
+ * The childID of the content process that just crashed.
+ * @returns boolean
+ * True if one or more browsers were sent to the tab crashed
+ * page.
+ */
+ flushCrashedBrowserQueue(childID) {
+ let browserQueue = this.crashedBrowserQueues.get(childID);
+ if (!browserQueue) {
+ return false;
+ }
+
+ this.crashedBrowserQueues.delete(childID);
+
+ let sentBrowser = false;
+ for (let weakBrowser of browserQueue) {
+ let browser = weakBrowser.get();
+ if (browser) {
+ if (
+ this.restartRequiredBrowsers.has(browser) ||
+ this.testBuildIDMismatch
+ ) {
+ this.sendToRestartRequiredPage(browser);
+ } else {
+ this.sendToTabCrashedPage(browser);
+ }
+ sentBrowser = true;
+ }
+ }
+
+ return sentBrowser;
+ },
+
+ /**
+ * Called by a tabbrowser when it notices that its selected browser
+ * has crashed. This will queue the browser to show the tab crash
+ * page once the content process has finished tearing down.
+ *
+ * @param browser (<xul:browser>)
+ * The selected browser that just crashed.
+ * @param restartRequired (bool)
+ * Whether or not a browser restart is required to recover.
+ */
+ onSelectedBrowserCrash(browser, restartRequired) {
+ if (!browser.isRemoteBrowser) {
+ console.error("Selected crashed browser is not remote.");
+ return;
+ }
+ if (!browser.frameLoader) {
+ console.error("Selected crashed browser has no frameloader.");
+ return;
+ }
+
+ let childID = browser.frameLoader.childID;
+
+ let browserQueue = this.crashedBrowserQueues.get(childID);
+ if (!browserQueue) {
+ browserQueue = [];
+ this.crashedBrowserQueues.set(childID, browserQueue);
+ }
+ // It's probably unnecessary to store this browser as a
+ // weak reference, since the content process should complete
+ // its teardown in the same tick of the event loop, and then
+ // this queue will be flushed. The weak reference is to avoid
+ // leaking browsers in case anything goes wrong during this
+ // teardown process.
+ browserQueue.push(Cu.getWeakReference(browser));
+
+ if (restartRequired) {
+ this.restartRequiredBrowsers.add(browser);
+ }
+
+ // In the event that the content process failed to launch, then
+ // the childID will be 0. In that case, we will never receive
+ // a dumpID nor an ipc:content-shutdown observer notification,
+ // so we should flush the queue for childID 0 immediately.
+ if (childID == 0) {
+ this.flushCrashedBrowserQueue(0);
+ }
+ },
+
+ /**
+ * Called by a tabbrowser when it notices that a background browser
+ * has crashed. This will flip its remoteness to non-remote, and attempt
+ * to revive the crashed tab so that upon selection the tab either shows
+ * an error page, or automatically restores.
+ *
+ * @param browser (<xul:browser>)
+ * The background browser that just crashed.
+ * @param restartRequired (bool)
+ * Whether or not a browser restart is required to recover.
+ */
+ onBackgroundBrowserCrash(browser, restartRequired) {
+ if (restartRequired) {
+ this.restartRequiredBrowsers.add(browser);
+ }
+
+ let gBrowser = browser.getTabBrowser();
+ let tab = gBrowser.getTabForBrowser(browser);
+
+ gBrowser.updateBrowserRemoteness(browser, {
+ remoteType: lazy.E10SUtils.NOT_REMOTE,
+ });
+
+ lazy.SessionStore.reviveCrashedTab(tab);
+ },
+
+ /**
+ * Called when a subframe crashes. If the dump is available, shows a subframe
+ * crashed notification, otherwise waits for one to be available.
+ *
+ * @param browser (<xul:browser>)
+ * The browser containing the frame that just crashed.
+ * @param childId
+ * The id of the process that just crashed.
+ */
+ async onSubFrameCrash(browser, childID) {
+ if (!AppConstants.MOZ_CRASHREPORTER) {
+ return;
+ }
+
+ // If a crash dump is available, use it. Otherwise, add the child id to the pending
+ // subframe crashes list, and wait for the crash "ipc:content-shutdown" notification
+ // to get the minidump. If it never arrives, don't show the notification.
+ let dumpID = this.childMap.get(childID);
+ if (dumpID) {
+ this.showSubFrameNotification(browser, childID, dumpID);
+ } else {
+ let item = this.pendingSubFrameCrashes.get(childID);
+ if (!item) {
+ item = new BrowserWeakMap();
+ this.pendingSubFrameCrashes.set(childID, item);
+
+ // Add the childID to an array that only has room for MAX_UNSEEN_CRASHED_SUBFRAME_IDS
+ // items. If there is no more room, pop the oldest off and remove it. This technique
+ // is used instead of a timeout.
+ if (
+ this.pendingSubFrameCrashesIDs.length >=
+ MAX_UNSEEN_CRASHED_SUBFRAME_IDS
+ ) {
+ let idToDelete = this.pendingSubFrameCrashesIDs.shift();
+ this.pendingSubFrameCrashes.delete(idToDelete);
+ }
+ this.pendingSubFrameCrashesIDs.push(childID);
+ }
+ item.set(browser, browser);
+ }
+ },
+
+ /**
+ * Given a childID, retrieve the subframe crash info for it
+ * from the pendingSubFrameCrashes map. The data is removed
+ * from the map and returned.
+ *
+ * @param childID number
+ * childID of the content that crashed.
+ * @returns subframe crash info added by previous call to onSubFrameCrash.
+ */
+ getAndRemoveSubframeCrash(childID) {
+ let item = this.pendingSubFrameCrashes.get(childID);
+ if (item) {
+ this.pendingSubFrameCrashes.delete(childID);
+ let idx = this.pendingSubFrameCrashesIDs.indexOf(childID);
+ if (idx >= 0) {
+ this.pendingSubFrameCrashesIDs.splice(idx, 1);
+ }
+ }
+
+ return item;
+ },
+
+ /**
+ * Called to indicate that a subframe within a browser has crashed. A notification
+ * bar will be shown.
+ *
+ * @param browser (<xul:browser>)
+ * The browser containing the frame that just crashed.
+ * @param childId
+ * The id of the process that just crashed.
+ * @param dumpID
+ * Minidump id of the crash.
+ */
+ async showSubFrameNotification(browser, childID, dumpID) {
+ let gBrowser = browser.getTabBrowser();
+ let notificationBox = gBrowser.getNotificationBox(browser);
+
+ const value = "subframe-crashed";
+ let notification = notificationBox.getNotificationWithValue(value);
+ if (notification) {
+ // Don't show multiple notifications for a browser.
+ return;
+ }
+
+ let closeAllNotifications = () => {
+ // Close all other notifications on other tabs that might
+ // be open for the same crashed process.
+ let existingItem = this.notificationsMap.get(childID);
+ if (existingItem) {
+ for (let notif of existingItem.slice()) {
+ notif.close();
+ }
+ }
+ };
+
+ gBrowser.ownerGlobal.MozXULElement.insertFTLIfNeeded(
+ "browser/contentCrash.ftl"
+ );
+
+ let buttons = [
+ {
+ "l10n-id": "crashed-subframe-learnmore-link",
+ popup: null,
+ link: SUBFRAMECRASH_LEARNMORE_URI,
+ },
+ {
+ "l10n-id": "crashed-subframe-submit",
+ popup: null,
+ callback: async () => {
+ if (dumpID) {
+ UnsubmittedCrashHandler.submitReports(
+ [dumpID],
+ lazy.CrashSubmit.SUBMITTED_FROM_CRASH_TAB
+ );
+ }
+ closeAllNotifications();
+ },
+ },
+ ];
+
+ notification = await notificationBox.appendNotification(
+ value,
+ {
+ label: { "l10n-id": "crashed-subframe-message" },
+ image: TABCRASHED_ICON_URI,
+ priority: notificationBox.PRIORITY_INFO_MEDIUM,
+ eventCallback: eventName => {
+ if (eventName == "disconnected") {
+ let existingItem = this.notificationsMap.get(childID);
+ if (existingItem) {
+ let idx = existingItem.indexOf(notification);
+ if (idx >= 0) {
+ existingItem.splice(idx, 1);
+ }
+
+ if (!existingItem.length) {
+ this.notificationsMap.delete(childID);
+ }
+ }
+ } else if (eventName == "dismissed") {
+ if (dumpID) {
+ lazy.CrashSubmit.ignore(dumpID);
+ this.childMap.delete(childID);
+ }
+
+ closeAllNotifications();
+ }
+ },
+ },
+ buttons
+ );
+
+ let existingItem = this.notificationsMap.get(childID);
+ if (existingItem) {
+ existingItem.push(notification);
+ } else {
+ this.notificationsMap.set(childID, [notification]);
+ }
+ },
+
+ /**
+ * This method is exposed for SessionStore to call if the user selects
+ * a tab which will restore on demand. It's possible that the tab
+ * is in this state because it recently crashed. If that's the case, then
+ * it's also possible that the user has not seen the tab crash page for
+ * that particular crash, in which case, we might show it to them instead
+ * of restoring the tab.
+ *
+ * @param browser (<xul:browser>)
+ * A browser from a browser tab that the user has just selected
+ * to restore on demand.
+ * @returns (boolean)
+ * True if TabCrashHandler will send the user to the tab crash
+ * page instead.
+ */
+ willShowCrashedTab(browser) {
+ let childID = this.browserMap.get(browser);
+ // We will only show the tab crash page if:
+ // 1) We are aware that this browser crashed
+ // 2) We know we've never shown the tab crash page for the
+ // crash yet
+ // 3) The user is not configured to automatically submit backlogged
+ // crash reports. If they are, we'll send the crash report
+ // immediately.
+ if (childID && this.unseenCrashedChildIDs.includes(childID)) {
+ if (UnsubmittedCrashHandler.autoSubmit) {
+ let dumpID = this.childMap.get(childID);
+ if (dumpID) {
+ UnsubmittedCrashHandler.submitReports(
+ [dumpID],
+ lazy.CrashSubmit.SUBMITTED_FROM_AUTO
+ );
+ }
+ } else {
+ this.sendToTabCrashedPage(browser);
+ return true;
+ }
+ } else if (childID === 0) {
+ if (this.restartRequiredBrowsers.has(browser)) {
+ this.sendToRestartRequiredPage(browser);
+ } else {
+ this.sendToTabCrashedPage(browser);
+ }
+ return true;
+ }
+
+ return false;
+ },
+
+ sendToRestartRequiredPage(browser) {
+ let uri = browser.currentURI;
+ let gBrowser = browser.getTabBrowser();
+ let tab = gBrowser.getTabForBrowser(browser);
+ // The restart required page is non-remote by default.
+ gBrowser.updateBrowserRemoteness(browser, {
+ remoteType: lazy.E10SUtils.NOT_REMOTE,
+ });
+
+ browser.docShell.displayLoadError(Cr.NS_ERROR_BUILDID_MISMATCH, uri, null);
+ tab.setAttribute("crashed", true);
+ gBrowser.tabContainer.updateTabIndicatorAttr(tab);
+
+ // Make sure to only count once even if there are multiple windows
+ // that will all show about:restartrequired.
+ if (this._crashedTabCount == 1) {
+ Services.telemetry.scalarAdd("dom.contentprocess.buildID_mismatch", 1);
+ }
+ },
+
+ /**
+ * We show a special page to users when a normal browser tab has crashed.
+ * This method should be called to send a browser to that page once the
+ * process has completely closed.
+ *
+ * @param browser (<xul:browser>)
+ * The browser that has recently crashed.
+ */
+ sendToTabCrashedPage(browser) {
+ let title = browser.contentTitle;
+ let uri = browser.currentURI;
+ let gBrowser = browser.getTabBrowser();
+ let tab = gBrowser.getTabForBrowser(browser);
+ // The tab crashed page is non-remote by default.
+ gBrowser.updateBrowserRemoteness(browser, {
+ remoteType: lazy.E10SUtils.NOT_REMOTE,
+ });
+
+ browser.setAttribute("crashedPageTitle", title);
+ browser.docShell.displayLoadError(Cr.NS_ERROR_CONTENT_CRASHED, uri, null);
+ browser.removeAttribute("crashedPageTitle");
+ tab.setAttribute("crashed", true);
+ gBrowser.tabContainer.updateTabIndicatorAttr(tab);
+ },
+
+ /**
+ * Submits a crash report from about:tabcrashed, if the crash
+ * reporter is enabled and a crash report can be found.
+ *
+ * @param browser
+ * The <xul:browser> that the report was sent from.
+ * @param message
+ * Message data with the following properties:
+ *
+ * includeURL (bool):
+ * Whether to include the URL that the user was on
+ * in the crashed tab before the crash occurred.
+ * URL (String)
+ * The URL that the user was on in the crashed tab
+ * before the crash occurred.
+ * comments (String):
+ * Any additional comments from the user.
+ *
+ * Note that it is expected that all properties are set,
+ * even if they are empty.
+ */
+ maybeSendCrashReport(browser, message) {
+ if (!AppConstants.MOZ_CRASHREPORTER) {
+ return;
+ }
+
+ if (!message.data.hasReport) {
+ // There was no report, so nothing to do.
+ return;
+ }
+
+ if (message.data.autoSubmit) {
+ // The user has opted in to autosubmitted backlogged
+ // crash reports in the future.
+ UnsubmittedCrashHandler.autoSubmit = true;
+ }
+
+ let childID = this.browserMap.get(browser);
+ let dumpID = this.childMap.get(childID);
+ if (!dumpID) {
+ return;
+ }
+
+ if (!message.data.sendReport) {
+ Services.telemetry
+ .getHistogramById("FX_CONTENT_CRASH_NOT_SUBMITTED")
+ .add(1);
+ this.prefs.setBoolPref("sendReport", false);
+ return;
+ }
+
+ let { includeURL, comments, URL } = message.data;
+
+ let extraExtraKeyVals = {
+ Comments: comments,
+ URL,
+ };
+
+ // For the entries in extraExtraKeyVals, we only want to submit the
+ // extra data values where they are not the empty string.
+ for (let key in extraExtraKeyVals) {
+ let val = extraExtraKeyVals[key].trim();
+ if (!val) {
+ delete extraExtraKeyVals[key];
+ }
+ }
+
+ // URL is special, since it's already been written to extra data by
+ // default. In order to make sure we don't send it, we overwrite it
+ // with the empty string.
+ if (!includeURL) {
+ extraExtraKeyVals.URL = "";
+ }
+
+ lazy.CrashSubmit.submit(dumpID, lazy.CrashSubmit.SUBMITTED_FROM_CRASH_TAB, {
+ recordSubmission: true,
+ extraExtraKeyVals,
+ }).catch(console.error);
+
+ this.prefs.setBoolPref("sendReport", true);
+ this.prefs.setBoolPref("includeURL", includeURL);
+
+ this.childMap.set(childID, null); // Avoid resubmission.
+ this.removeSubmitCheckboxesForSameCrash(childID);
+ },
+
+ removeSubmitCheckboxesForSameCrash(childID) {
+ for (let window of Services.wm.getEnumerator("navigator:browser")) {
+ if (!window.gMultiProcessBrowser) {
+ continue;
+ }
+
+ for (let browser of window.gBrowser.browsers) {
+ if (browser.isRemoteBrowser) {
+ continue;
+ }
+
+ let doc = browser.contentDocument;
+ if (!doc.documentURI.startsWith("about:tabcrashed")) {
+ continue;
+ }
+
+ if (this.browserMap.get(browser) == childID) {
+ this.browserMap.delete(browser);
+ browser.sendMessageToActor("CrashReportSent", {}, "AboutTabCrashed");
+ }
+ }
+ }
+ },
+
+ /**
+ * Process a crashed tab loaded into a browser.
+ *
+ * @param browser
+ * The <xul:browser> containing the page that crashed.
+ * @returns crash data
+ * Message data containing information about the crash.
+ */
+ onAboutTabCrashedLoad(browser) {
+ this._crashedTabCount++;
+
+ let window = browser.ownerGlobal;
+
+ // Reset the zoom for the tabcrashed page.
+ window.ZoomManager.setZoomForBrowser(browser, 1);
+
+ let childID = this.browserMap.get(browser);
+ let index = this.unseenCrashedChildIDs.indexOf(childID);
+ if (index != -1) {
+ this.unseenCrashedChildIDs.splice(index, 1);
+ }
+
+ let dumpID = this.getDumpID(browser);
+ if (!dumpID) {
+ return {
+ hasReport: false,
+ };
+ }
+
+ let requestAutoSubmit = !UnsubmittedCrashHandler.autoSubmit;
+ let sendReport = this.prefs.getBoolPref("sendReport");
+ let includeURL = this.prefs.getBoolPref("includeURL");
+
+ let data = {
+ hasReport: true,
+ sendReport,
+ includeURL,
+ requestAutoSubmit,
+ };
+
+ return data;
+ },
+
+ onAboutTabCrashedUnload(browser) {
+ if (!this._crashedTabCount) {
+ console.error("Can not decrement crashed tab count to below 0");
+ return;
+ }
+ this._crashedTabCount--;
+
+ let childID = this.browserMap.get(browser);
+
+ // Make sure to only count once even if there are multiple windows
+ // that will all show about:tabcrashed.
+ if (this._crashedTabCount == 0 && childID) {
+ Services.telemetry
+ .getHistogramById("FX_CONTENT_CRASH_NOT_SUBMITTED")
+ .add(1);
+ }
+ },
+
+ /**
+ * For some <xul:browser>, return a crash report dump ID for that browser
+ * if we have been informed of one. Otherwise, return null.
+ *
+ * @param browser (<xul:browser)
+ * The browser to try to get the dump ID for
+ * @returns dumpID (String)
+ */
+ getDumpID(browser) {
+ if (!AppConstants.MOZ_CRASHREPORTER) {
+ return null;
+ }
+
+ return this.childMap.get(this.browserMap.get(browser));
+ },
+
+ /**
+ * This is intended for TESTING ONLY. It returns the amount of
+ * content processes that have crashed such that we're still waiting
+ * for dump IDs for their crash reports.
+ *
+ * For our automated tests, accessing the crashed content process
+ * count helps us test the behaviour when content processes crash due
+ * to launch failure, since in those cases we should not increase the
+ * crashed browser queue (since we never receive dump IDs for launch
+ * failures).
+ */
+ get queuedCrashedBrowsers() {
+ return this.crashedBrowserQueues.size;
+ },
+};
+
+/**
+ * This component is responsible for scanning the pending
+ * crash report directory for reports, and (if enabled), to
+ * prompt the user to submit those reports. It might also
+ * submit those reports automatically without prompting if
+ * the user has opted in.
+ */
+export var UnsubmittedCrashHandler = {
+ get prefs() {
+ delete this.prefs;
+ return (this.prefs = Services.prefs.getBranch(
+ "browser.crashReports.unsubmittedCheck."
+ ));
+ },
+
+ get enabled() {
+ return this.prefs.getBoolPref("enabled");
+ },
+
+ // showingNotification is set to true once a notification
+ // is successfully shown, and then set back to false if
+ // the notification is dismissed by an action by the user.
+ showingNotification: false,
+ // suppressed is true if we've determined that we've shown
+ // the notification too many times across too many days without
+ // user interaction, so we're suppressing the notification for
+ // some number of days. See the documentation for
+ // shouldShowPendingSubmissionsNotification().
+ suppressed: false,
+
+ _checkTimeout: null,
+
+ init() {
+ if (this.initialized) {
+ return;
+ }
+
+ this.initialized = true;
+
+ // UnsubmittedCrashHandler can be initialized but still be disabled.
+ // This is intentional, as this makes simulating UnsubmittedCrashHandler's
+ // reactions to browser startup and shutdown easier in test automation.
+ //
+ // UnsubmittedCrashHandler, when initialized but not enabled, is inert.
+ if (this.enabled) {
+ if (this.prefs.prefHasUserValue("suppressUntilDate")) {
+ if (this.prefs.getCharPref("suppressUntilDate") > this.dateString()) {
+ // We'll be suppressing any notifications until after suppressedDate,
+ // so there's no need to do anything more.
+ this.suppressed = true;
+ return;
+ }
+
+ // We're done suppressing, so we don't need this pref anymore.
+ this.prefs.clearUserPref("suppressUntilDate");
+ }
+
+ Services.obs.addObserver(this, "profile-before-change");
+ }
+ },
+
+ uninit() {
+ if (!this.initialized) {
+ return;
+ }
+
+ this.initialized = false;
+
+ if (this._checkTimeout) {
+ lazy.clearTimeout(this._checkTimeout);
+ this._checkTimeout = null;
+ }
+
+ if (!this.enabled) {
+ return;
+ }
+
+ if (this.suppressed) {
+ this.suppressed = false;
+ // No need to do any more clean-up, since we were suppressed.
+ return;
+ }
+
+ if (this.showingNotification) {
+ this.prefs.setBoolPref("shutdownWhileShowing", true);
+ this.showingNotification = false;
+ }
+
+ Services.obs.removeObserver(this, "profile-before-change");
+ },
+
+ observe(subject, topic, data) {
+ switch (topic) {
+ case "profile-before-change": {
+ this.uninit();
+ break;
+ }
+ }
+ },
+
+ scheduleCheckForUnsubmittedCrashReports() {
+ this._checkTimeout = lazy.setTimeout(() => {
+ Services.tm.idleDispatchToMainThread(() => {
+ this.checkForUnsubmittedCrashReports();
+ });
+ }, CHECK_FOR_UNSUBMITTED_CRASH_REPORTS_DELAY_MS);
+ },
+
+ /**
+ * Scans the profile directory for unsubmitted crash reports
+ * within the past PENDING_CRASH_REPORT_DAYS days. If it
+ * finds any, it will, if necessary, attempt to open a notification
+ * bar to prompt the user to submit them.
+ *
+ * @returns Promise
+ * Resolves with the <xul:notification> after it tries to
+ * show a notification on the most recent browser window.
+ * If a notification cannot be shown, will resolve with null.
+ */
+ async checkForUnsubmittedCrashReports() {
+ if (!this.enabled || this.suppressed) {
+ return null;
+ }
+
+ let dateLimit = new Date();
+ dateLimit.setDate(dateLimit.getDate() - PENDING_CRASH_REPORT_DAYS);
+
+ let reportIDs = [];
+ try {
+ reportIDs = await lazy.CrashSubmit.pendingIDs(dateLimit);
+ } catch (e) {
+ console.error(e);
+ return null;
+ }
+
+ if (reportIDs.length) {
+ if (this.autoSubmit) {
+ this.submitReports(reportIDs, lazy.CrashSubmit.SUBMITTED_FROM_AUTO);
+ } else if (this.shouldShowPendingSubmissionsNotification()) {
+ return this.showPendingSubmissionsNotification(reportIDs);
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Returns true if the notification should be shown.
+ * shouldShowPendingSubmissionsNotification makes this decision
+ * by looking at whether or not the user has seen the notification
+ * over several days without ever interacting with it. If this occurs
+ * too many times, we suppress the notification for DAYS_TO_SUPPRESS
+ * days.
+ *
+ * @returns bool
+ */
+ shouldShowPendingSubmissionsNotification() {
+ if (!this.prefs.prefHasUserValue("shutdownWhileShowing")) {
+ return true;
+ }
+
+ let shutdownWhileShowing = this.prefs.getBoolPref("shutdownWhileShowing");
+ this.prefs.clearUserPref("shutdownWhileShowing");
+
+ if (!this.prefs.prefHasUserValue("lastShownDate")) {
+ // This isn't expected, but we're being defensive here. We'll
+ // opt for showing the notification in this case.
+ return true;
+ }
+
+ let lastShownDate = this.prefs.getCharPref("lastShownDate");
+ if (this.dateString() > lastShownDate && shutdownWhileShowing) {
+ // We're on a newer day then when we last showed the
+ // notification without closing it. We don't want to do
+ // this too many times, so we'll decrement a counter for
+ // this situation. Too many of these, and we'll assume the
+ // user doesn't know or care about unsubmitted notifications,
+ // and we'll suppress the notification for a while.
+ let chances = this.prefs.getIntPref("chancesUntilSuppress");
+ if (--chances < 0) {
+ // We're out of chances!
+ this.prefs.clearUserPref("chancesUntilSuppress");
+ // We'll suppress for DAYS_TO_SUPPRESS days.
+ let suppressUntil = this.dateString(
+ new Date(Date.now() + DAY * DAYS_TO_SUPPRESS)
+ );
+ this.prefs.setCharPref("suppressUntilDate", suppressUntil);
+ return false;
+ }
+ this.prefs.setIntPref("chancesUntilSuppress", chances);
+ }
+
+ return true;
+ },
+
+ /**
+ * Given an array of unsubmitted crash report IDs, try to open
+ * up a notification asking the user to submit them.
+ *
+ * @param reportIDs (Array<string>)
+ * The Array of report IDs to offer the user to send.
+ * @returns The <xul:notification> if one is shown. null otherwise.
+ */
+ async showPendingSubmissionsNotification(reportIDs) {
+ if (!reportIDs.length) {
+ return null;
+ }
+
+ let notification = await this.show({
+ notificationID: "pending-crash-reports",
+ reportIDs,
+ onAction: () => {
+ this.showingNotification = false;
+ },
+ });
+
+ if (notification) {
+ this.showingNotification = true;
+ this.prefs.setCharPref("lastShownDate", this.dateString());
+ }
+
+ return notification;
+ },
+
+ /**
+ * Returns a string representation of a Date in the format
+ * YYYYMMDD.
+ *
+ * @param someDate (Date, optional)
+ * The Date to convert to the string. If not provided,
+ * defaults to today's date.
+ * @returns String
+ */
+ dateString(someDate = new Date()) {
+ let year = String(someDate.getFullYear()).padStart(4, "0");
+ let month = String(someDate.getMonth() + 1).padStart(2, "0");
+ let day = String(someDate.getDate()).padStart(2, "0");
+ return year + month + day;
+ },
+
+ /**
+ * Attempts to show a notification bar to the user in the most
+ * recent browser window asking them to submit some crash report
+ * IDs. If a notification cannot be shown (for example, there
+ * is no browser window), this method exits silently.
+ *
+ * The notification will allow the user to submit their crash
+ * reports. If the user dismissed the notification, the crash
+ * reports will be marked to be ignored (though they can
+ * still be manually submitted via about:crashes).
+ *
+ * @param JS Object
+ * An Object with the following properties:
+ *
+ * notificationID (string)
+ * The ID for the notification to be opened.
+ *
+ * reportIDs (Array<string>)
+ * The array of report IDs to offer to the user.
+ *
+ * onAction (function, optional)
+ * A callback to fire once the user performs an
+ * action on the notification bar (this includes
+ * dismissing the notification).
+ *
+ * @returns The <xul:notification> if one is shown. null otherwise.
+ */
+ show({ notificationID, reportIDs, onAction }) {
+ let chromeWin = lazy.BrowserWindowTracker.getTopWindow();
+ if (!chromeWin) {
+ // Can't show a notification in this case. We'll hopefully
+ // get another opportunity to have the user submit their
+ // crash reports later.
+ return null;
+ }
+
+ let notification =
+ chromeWin.gNotificationBox.getNotificationWithValue(notificationID);
+ if (notification) {
+ return null;
+ }
+
+ chromeWin.MozXULElement.insertFTLIfNeeded("browser/contentCrash.ftl");
+
+ let buttons = [
+ {
+ "l10n-id": "pending-crash-reports-send",
+ callback: () => {
+ this.submitReports(
+ reportIDs,
+ lazy.CrashSubmit.SUBMITTED_FROM_INFOBAR
+ );
+ if (onAction) {
+ onAction();
+ }
+ },
+ },
+ {
+ "l10n-id": "pending-crash-reports-always-send",
+ callback: () => {
+ this.autoSubmit = true;
+ this.submitReports(
+ reportIDs,
+ lazy.CrashSubmit.SUBMITTED_FROM_INFOBAR
+ );
+ if (onAction) {
+ onAction();
+ }
+ },
+ },
+ {
+ "l10n-id": "pending-crash-reports-view-all",
+ callback() {
+ chromeWin.openTrustedLinkIn("about:crashes", "tab");
+ return true;
+ },
+ },
+ ];
+
+ let eventCallback = eventType => {
+ if (eventType == "dismissed") {
+ // The user intentionally dismissed the notification,
+ // which we interpret as meaning that they don't care
+ // to submit the reports. We'll ignore these particular
+ // reports going forward.
+ reportIDs.forEach(function (reportID) {
+ lazy.CrashSubmit.ignore(reportID);
+ });
+ if (onAction) {
+ onAction();
+ }
+ }
+ };
+
+ return chromeWin.gNotificationBox.appendNotification(
+ notificationID,
+ {
+ label: {
+ "l10n-id": "pending-crash-reports-message",
+ "l10n-args": { reportCount: reportIDs.length },
+ },
+ image: TABCRASHED_ICON_URI,
+ priority: chromeWin.gNotificationBox.PRIORITY_INFO_HIGH,
+ eventCallback,
+ },
+ buttons
+ );
+ },
+
+ get autoSubmit() {
+ return Services.prefs.getBoolPref(
+ "browser.crashReports.unsubmittedCheck.autoSubmit2"
+ );
+ },
+
+ set autoSubmit(val) {
+ Services.prefs.setBoolPref(
+ "browser.crashReports.unsubmittedCheck.autoSubmit2",
+ val
+ );
+ },
+
+ /**
+ * Attempt to submit reports to the crash report server.
+ *
+ * @param reportIDs (Array<string>)
+ * The array of reportIDs to submit.
+ * @param submittedFrom (string)
+ * One of the CrashSubmit.SUBMITTED_FROM_* constants representing
+ * how this crash was submitted.
+ */
+ submitReports(reportIDs, submittedFrom) {
+ for (let reportID of reportIDs) {
+ lazy.CrashSubmit.submit(reportID, submittedFrom).catch(console.error);
+ }
+ },
+};