You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
922 lines
30 KiB
922 lines
30 KiB
/* 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, "AppConstants", |
|
"resource://gre/modules/AppConstants.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: function () { |
|
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: function (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(); |
|
} |
|
} |
|
|
|
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: function(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); |
|
}, |
|
|
|
/** |
|
* Submits a crash report from about:tabcrashed, if the crash |
|
* reporter is enabled and a crash report can be found. |
|
*/ |
|
maybeSendCrashReport(message) { |
|
/*** STUB ***/ |
|
return; |
|
}, |
|
|
|
removeSubmitCheckboxesForSameCrash: function(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: function (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); |
|
} |
|
}, |
|
|
|
/** |
|
* For some <xul:browser>, return a crash report dump ID for that browser |
|
* if we have been informed of one. Otherwise, return null. |
|
*/ |
|
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: function() { |
|
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.autoSubmit2"); |
|
}, |
|
|
|
set autoSubmit(val) { |
|
Services.prefs.setBoolPref("browser.crashReports.unsubmittedCheck.autoSubmit2", |
|
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", false); |
|
Services.obs.removeObserver(this, "gmp-plugin-crash", false); |
|
Services.obs.removeObserver(this, "profile-after-change", false); |
|
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 Gecko 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); |
|
}, |
|
};
|
|
|