fune/browser/modules/ContentCrashHandlers.sys.mjs
Norisz Fay 0d4fc02631 Backed out 7 changesets (bug 1845150) for causing failures on browser_datachoices_notification.js CLOSED TREE
Backed out changeset a07c835a4bf7 (bug 1845150)
Backed out changeset 0016d7f4e556 (bug 1845150)
Backed out changeset 46cfe5fe6e6a (bug 1845150)
Backed out changeset 83190b8a08d0 (bug 1845150)
Backed out changeset ada948351267 (bug 1845150)
Backed out changeset 7aa4340bfd96 (bug 1845150)
Backed out changeset 683745289588 (bug 1845150)
2024-01-05 18:52:01 +02:00

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);
}
},
};