forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			1132 lines
		
	
	
	
		
			35 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1132 lines
		
	
	
	
		
			35 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* 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.
 | |
|    */
 | |
|   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 = 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.
 | |
|    */
 | |
|   showPendingSubmissionsNotification(reportIDs) {
 | |
|     if (!reportIDs.length) {
 | |
|       return null;
 | |
|     }
 | |
| 
 | |
|     let notification = 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);
 | |
|     }
 | |
|   },
 | |
| };
 | 
