diff options
author | Matt A. Tobin <email@mattatobin.com> | 2018-02-02 03:32:58 -0500 |
---|---|---|
committer | Matt A. Tobin <email@mattatobin.com> | 2018-02-02 03:32:58 -0500 |
commit | e72ef92b5bdc43cd2584198e2e54e951b70299e8 (patch) | |
tree | 01ceb4a897c33eca9e7ccf2bc3aefbe530169fe5 /application/basilisk/modules/ContentCrashHandlers.jsm | |
parent | 0d19b77d3eaa5b8d837bf52c19759e68e42a1c4c (diff) | |
download | UXP-e72ef92b5bdc43cd2584198e2e54e951b70299e8.tar UXP-e72ef92b5bdc43cd2584198e2e54e951b70299e8.tar.gz UXP-e72ef92b5bdc43cd2584198e2e54e951b70299e8.tar.lz UXP-e72ef92b5bdc43cd2584198e2e54e951b70299e8.tar.xz UXP-e72ef92b5bdc43cd2584198e2e54e951b70299e8.zip |
Add Basilisk
Diffstat (limited to 'application/basilisk/modules/ContentCrashHandlers.jsm')
-rw-r--r-- | application/basilisk/modules/ContentCrashHandlers.jsm | 921 |
1 files changed, 921 insertions, 0 deletions
diff --git a/application/basilisk/modules/ContentCrashHandlers.jsm b/application/basilisk/modules/ContentCrashHandlers.jsm new file mode 100644 index 000000000..5e96a3134 --- /dev/null +++ b/application/basilisk/modules/ContentCrashHandlers.jsm @@ -0,0 +1,921 @@ +/* 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/. */ + +"use strict"; + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; +var Cr = Components.results; + +this.EXPORTED_SYMBOLS = [ "TabCrashHandler", + "PluginCrashReporter", + "UnsubmittedCrashHandler" ]; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "CrashSubmit", + "resource://gre/modules/CrashSubmit.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "RemotePages", + "resource://gre/modules/RemotePageManager.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "SessionStore", + "resource:///modules/sessionstore/SessionStore.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow", + "resource:///modules/RecentWindow.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", + "resource://gre/modules/PluralForm.jsm"); + +XPCOMUtils.defineLazyGetter(this, "gNavigatorBundle", function() { + const url = "chrome://browser/locale/browser.properties"; + return Services.strings.createBundle(url); +}); + +// 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; + +this.TabCrashHandler = { + _crashedTabCount: 0, + childMap: new Map(), + browserMap: new WeakMap(), + unseenCrashedChildIDs: [], + crashedBrowserQueues: new Map(), + + 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", false); + Services.obs.addObserver(this, "oop-frameloader-crashed", false); + + this.pageListener = new RemotePages("about:tabcrashed"); + // LOAD_BACKGROUND pages don't fire load events, so the about:tabcrashed + // content will fire up its own message when its initial scripts have + // finished running. + this.pageListener.addMessageListener("Load", this.receiveMessage.bind(this)); + this.pageListener.addMessageListener("RemotePage:Unload", this.receiveMessage.bind(this)); + this.pageListener.addMessageListener("closeTab", this.receiveMessage.bind(this)); + this.pageListener.addMessageListener("restoreTab", this.receiveMessage.bind(this)); + this.pageListener.addMessageListener("restoreAll", this.receiveMessage.bind(this)); + }, + + 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"); + + if (!dumpID) { + Services.telemetry + .getHistogramById("FX_CONTENT_CRASH_DUMP_UNAVAILABLE") + .add(1); + } + + 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 env = Cc["@mozilla.org/process/environment;1"] + .getService(Ci.nsIEnvironment); + let shutdown = env.exists("MOZ_CRASHREPORTER_SHUTDOWN"); + + if (shutdown) { + Services.startup.quit(Ci.nsIAppStartup.eForceQuit); + } + + break; + } + case "oop-frameloader-crashed": { + aSubject.QueryInterface(Ci.nsIFrameLoader); + + let browser = aSubject.ownerElement; + if (!browser) { + return; + } + + this.browserMap.set(browser.permanentKey, aSubject.childID); + break; + } + } + }, + + receiveMessage(message) { + let browser = message.target.browser; + let gBrowser = browser.ownerGlobal.gBrowser; + let tab = gBrowser.getTabForBrowser(browser); + + switch (message.name) { + case "Load": { + this.onAboutTabCrashedLoad(message); + break; + } + + case "RemotePage:Unload": { + this.onAboutTabCrashedUnload(message); + break; + } + + case "closeTab": { + this.maybeSendCrashReport(message); + gBrowser.removeTab(tab, { animate: true }); + break; + } + + case "restoreTab": { + this.maybeSendCrashReport(message); + SessionStore.reviveCrashedTab(tab); + break; + } + + case "restoreAll": { + this.maybeSendCrashReport(message); + SessionStore.reviveAllCrashedTabs(); + 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) { + 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. + */ + onSelectedBrowserCrash(browser) { + if (!browser.isRemoteBrowser) { + Cu.reportError("Selected crashed browser is not remote.") + return; + } + if (!browser.frameLoader) { + Cu.reportError("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)); + }, + + /** + * 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.permanentKey); + // 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.indexOf(childID) != -1) { + if (UnsubmittedCrashHandler.autoSubmit) { + let dumpID = this.childMap.get(childID); + if (dumpID) { + UnsubmittedCrashHandler.submitReports([dumpID]); + } + } else { + this.sendToTabCrashedPage(browser); + return true; + } + } + + return false; + }, + + /** + * 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.ownerGlobal.gBrowser; + let tab = gBrowser.getTabForBrowser(browser); + // The tab crashed page is non-remote by default. + gBrowser.updateBrowserRemoteness(browser, false); + + browser.setAttribute("crashedPageTitle", title); + browser.docShell.displayLoadError(Cr.NS_ERROR_CONTENT_CRASHED, uri, null); + browser.removeAttribute("crashedPageTitle"); + tab.setAttribute("crashed", true); + }, + + maybeSendCrashReport(message) { + // **STUB** + return; + }, + + removeSubmitCheckboxesForSameCrash(childID) { + let enumerator = Services.wm.getEnumerator("navigator:browser"); + while (enumerator.hasMoreElements()) { + let window = enumerator.getNext(); + 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.permanentKey) == childID) { + this.browserMap.delete(browser.permanentKey); + let ports = this.pageListener.portsForBrowser(browser); + if (ports.length) { + // For about:tabcrashed, we don't expect subframes. We can + // assume sending to the first port is sufficient. + ports[0].sendAsyncMessage("CrashReportSent"); + } + } + } + } + }, + + onAboutTabCrashedLoad(message) { + this._crashedTabCount++; + + // Broadcast to all about:tabcrashed pages a count of + // how many about:tabcrashed pages exist, so that they + // can decide whether or not to display the "Restore All + // Crashed Tabs" button. + this.pageListener.sendAsyncMessage("UpdateCount", { + count: this._crashedTabCount, + }); + + let browser = message.target.browser; + + let childID = this.browserMap.get(browser.permanentKey); + let index = this.unseenCrashedChildIDs.indexOf(childID); + if (index != -1) { + this.unseenCrashedChildIDs.splice(index, 1); + } + + let dumpID = this.getDumpID(browser); + if (!dumpID) { + message.target.sendAsyncMessage("SetCrashReportAvailable", { + hasReport: false, + }); + return; + } + + let requestAutoSubmit = !UnsubmittedCrashHandler.autoSubmit; + let requestEmail = this.prefs.getBoolPref("requestEmail"); + let sendReport = this.prefs.getBoolPref("sendReport"); + let includeURL = this.prefs.getBoolPref("includeURL"); + let emailMe = this.prefs.getBoolPref("emailMe"); + + let data = { + hasReport: true, + sendReport, + includeURL, + emailMe, + requestAutoSubmit, + requestEmail, + }; + + if (emailMe) { + data.email = this.prefs.getCharPref("email"); + } + + // Make sure to only count once even if there are multiple windows + // that will all show about:tabcrashed. + if (this._crashedTabCount == 1) { + Services.telemetry.getHistogramById("FX_CONTENT_CRASH_PRESENTED").add(1); + } + + message.target.sendAsyncMessage("SetCrashReportAvailable", data); + }, + + onAboutTabCrashedUnload(message) { + if (!this._crashedTabCount) { + Cu.reportError("Can not decrement crashed tab count to below 0"); + return; + } + this._crashedTabCount--; + + // Broadcast to all about:tabcrashed pages a count of + // how many about:tabcrashed pages exist, so that they + // can decide whether or not to display the "Restore All + // Crashed Tabs" button. + this.pageListener.sendAsyncMessage("UpdateCount", { + count: this._crashedTabCount, + }); + + let browser = message.target.browser; + let childID = this.browserMap.get(browser.permanentKey); + + // 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); + } + }, + + getDumpID(browser) { + // **STUB** + return null; + }, +} + +/** + * 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. + */ +this.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, + + 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, "browser-delayed-startup-finished", + false); + Services.obs.addObserver(this, "profile-before-change", + false); + } + }, + + uninit() { + if (!this.initialized) { + return; + } + + this.initialized = false; + + 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; + } + + try { + Services.obs.removeObserver(this, "browser-delayed-startup-finished"); + } catch (e) { + // The browser-delayed-startup-finished observer might have already + // fired and removed itself, so if this fails, it's okay. + if (e.result != Cr.NS_ERROR_FAILURE) { + throw e; + } + } + + Services.obs.removeObserver(this, "profile-before-change"); + }, + + observe(subject, topic, data) { + switch (topic) { + case "browser-delayed-startup-finished": { + Services.obs.removeObserver(this, topic); + this.checkForUnsubmittedCrashReports(); + break; + } + case "profile-before-change": { + this.uninit(); + break; + } + } + }, + + /** + * 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. + */ + checkForUnsubmittedCrashReports: Task.async(function*() { + let dateLimit = new Date(); + dateLimit.setDate(dateLimit.getDate() - PENDING_CRASH_REPORT_DAYS); + + let reportIDs = []; + try { + reportIDs = yield CrashSubmit.pendingIDsAsync(dateLimit); + } catch (e) { + Cu.reportError(e); + return null; + } + + if (reportIDs.length) { + if (this.autoSubmit) { + this.submitReports(reportIDs); + } 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) { + let count = reportIDs.length; + if (!count) { + return null; + } + + let messageTemplate = + gNavigatorBundle.GetStringFromName("pendingCrashReports2.label"); + + let message = PluralForm.get(count, messageTemplate).replace("#1", count); + + let notification = this.show({ + notificationID: "pending-crash-reports", + message, + 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. + * + * message (string) + * The message to be displayed in the notification. + * + * 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, message, reportIDs, onAction }) { + let chromeWin = RecentWindow.getMostRecentBrowserWindow(); + 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 nb = chromeWin.document.getElementById("global-notificationbox"); + let notification = nb.getNotificationWithValue(notificationID); + if (notification) { + return null; + } + + let buttons = [{ + label: gNavigatorBundle.GetStringFromName("pendingCrashReports.send"), + callback: () => { + this.submitReports(reportIDs); + if (onAction) { + onAction(); + } + }, + }, + { + label: gNavigatorBundle.GetStringFromName("pendingCrashReports.alwaysSend"), + callback: () => { + this.autoSubmit = true; + this.submitReports(reportIDs); + if (onAction) { + onAction(); + } + }, + }, + { + label: gNavigatorBundle.GetStringFromName("pendingCrashReports.viewAll"), + callback() { + chromeWin.openUILinkIn("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) { + CrashSubmit.ignore(reportID); + }); + if (onAction) { + onAction(); + } + } + }; + + return nb.appendNotification(message, notificationID, + "chrome://browser/skin/tab-crashed.svg", + nb.PRIORITY_INFO_HIGH, buttons, + eventCallback); + }, + + get autoSubmit() { + return Services.prefs + .getBoolPref("browser.crashReports.unsubmittedCheck.autoSubmit"); + }, + + set autoSubmit(val) { + Services.prefs.setBoolPref("browser.crashReports.unsubmittedCheck.autoSubmit", + val); + }, + + /** + * Attempt to submit reports to the crash report server. Each + * report will have the "SubmittedFromInfobar" extra key set + * to true. + * + * @param reportIDs (Array<string>) + * The array of reportIDs to submit. + */ + submitReports(reportIDs) { + for (let reportID of reportIDs) { + CrashSubmit.submit(reportID, { + extraExtraKeyVals: { + "SubmittedFromInfobar": true, + }, + }); + } + }, +}; + +this.PluginCrashReporter = { + /** + * Makes the PluginCrashReporter ready to hear about and + * submit crash reports. + */ + init() { + if (this.initialized) { + return; + } + + this.initialized = true; + this.crashReports = new Map(); + + Services.obs.addObserver(this, "plugin-crashed", false); + Services.obs.addObserver(this, "gmp-plugin-crash", false); + Services.obs.addObserver(this, "profile-after-change", false); + }, + + uninit() { + Services.obs.removeObserver(this, "plugin-crashed"); + Services.obs.removeObserver(this, "gmp-plugin-crash"); + Services.obs.removeObserver(this, "profile-after-change"); + this.initialized = false; + }, + + observe(subject, topic, data) { + switch (topic) { + case "plugin-crashed": { + let propertyBag = subject; + if (!(propertyBag instanceof Ci.nsIPropertyBag2) || + !(propertyBag instanceof Ci.nsIWritablePropertyBag2) || + !propertyBag.hasKey("runID") || + !propertyBag.hasKey("pluginDumpID")) { + Cu.reportError("PluginCrashReporter can not read plugin information."); + return; + } + + let runID = propertyBag.getPropertyAsUint32("runID"); + let pluginDumpID = propertyBag.getPropertyAsAString("pluginDumpID"); + let browserDumpID = propertyBag.getPropertyAsAString("browserDumpID"); + if (pluginDumpID) { + this.crashReports.set(runID, { pluginDumpID, browserDumpID }); + } + break; + } + case "gmp-plugin-crash": { + let propertyBag = subject; + if (!(propertyBag instanceof Ci.nsIWritablePropertyBag2) || + !propertyBag.hasKey("pluginID") || + !propertyBag.hasKey("pluginDumpID") || + !propertyBag.hasKey("pluginName")) { + Cu.reportError("PluginCrashReporter can not read plugin information."); + return; + } + + let pluginID = propertyBag.getPropertyAsUint32("pluginID"); + let pluginDumpID = propertyBag.getPropertyAsAString("pluginDumpID"); + if (pluginDumpID) { + this.crashReports.set(pluginID, { pluginDumpID }); + } + + // Only the parent process gets the gmp-plugin-crash observer + // notification, so we need to inform any content processes that + // the GMP has crashed. + if (Cc["@mozilla.org/parentprocessmessagemanager;1"]) { + let pluginName = propertyBag.getPropertyAsAString("pluginName"); + let mm = Cc["@mozilla.org/parentprocessmessagemanager;1"] + .getService(Ci.nsIMessageListenerManager); + mm.broadcastAsyncMessage("gmp-plugin-crash", + { pluginName, pluginID }); + } + break; + } + case "profile-after-change": + this.uninit(); + break; + } + }, + + /** + * Submit a crash report for a crashed NPAPI plugin. + * + * @param runID + * The runID of the plugin that crashed. A run ID is a unique + * identifier for a particular run of a plugin process - and is + * analogous to a process ID (though it is managed by Goanna instead + * of the operating system). + * @param keyVals + * An object whose key-value pairs will be merged + * with the ".extra" file submitted with the report. + * The properties of htis object will override properties + * of the same name in the .extra file. + */ + submitCrashReport(runID, keyVals) { + if (!this.crashReports.has(runID)) { + Cu.reportError(`Could not find plugin dump IDs for run ID ${runID}.` + + `It is possible that a report was already submitted.`); + return; + } + + keyVals = keyVals || {}; + let { pluginDumpID, browserDumpID } = this.crashReports.get(runID); + + let submissionPromise = CrashSubmit.submit(pluginDumpID, { + recordSubmission: true, + extraExtraKeyVals: keyVals, + }); + + if (browserDumpID) + CrashSubmit.submit(browserDumpID); + + this.broadcastState(runID, "submitting"); + + submissionPromise.then(() => { + this.broadcastState(runID, "success"); + }, () => { + this.broadcastState(runID, "failed"); + }); + + this.crashReports.delete(runID); + }, + + broadcastState(runID, state) { + let enumerator = Services.wm.getEnumerator("navigator:browser"); + while (enumerator.hasMoreElements()) { + let window = enumerator.getNext(); + let mm = window.messageManager; + mm.broadcastAsyncMessage("BrowserPlugins:CrashReportSubmitted", + { runID, state }); + } + }, + + hasCrashReport(runID) { + return this.crashReports.has(runID); + }, +}; |