bug 1269998 - Prompt users with pending crash reports to submit them r=mconley ui-r=shorlander
authorBrad Lassey <blassey@mozilla.com>
Tue, 10 May 2016 23:50:55 -0700
changeset 341025 7217724754491b5d40002d35012b73f670f76498
parent 341024 c4053b6d8c7704251776f52cdeb98a2c8d45fff4
child 341026 9020ac239e2a971a5fb5a93df50b89879b713912
push id1183
push userraliiev@mozilla.com
push dateMon, 05 Sep 2016 20:01:49 +0000
treeherdermozilla-release@3148731bed45 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmconley, shorlander
bugs1269998
milestone49.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
bug 1269998 - Prompt users with pending crash reports to submit them r=mconley ui-r=shorlander
browser/components/nsBrowserGlue.js
browser/locales/en-US/chrome/browser/browser.properties
toolkit/crashreporter/CrashSubmit.jsm
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -117,16 +117,21 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "ContentSearch",
                                   "resource:///modules/ContentSearch.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "TabCrashHandler",
                                   "resource:///modules/ContentCrashHandlers.jsm");
 if (AppConstants.MOZ_CRASHREPORTER) {
   XPCOMUtils.defineLazyModuleGetter(this, "PluginCrashReporter",
                                     "resource:///modules/ContentCrashHandlers.jsm");
+  XPCOMUtils.defineLazyModuleGetter(this, "CrashSubmit",
+                                    "resource://gre/modules/CrashSubmit.jsm");
+  XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
+                                    "resource://gre/modules/PluralForm.jsm");
+
 }
 
 XPCOMUtils.defineLazyGetter(this, "gBrandBundle", function() {
   return Services.strings.createBundle('chrome://branding/locale/brand.properties');
 });
 
 XPCOMUtils.defineLazyGetter(this, "gBrowserBundle", function() {
   return Services.strings.createBundle('chrome://browser/locale/browser.properties');
@@ -821,16 +826,70 @@ BrowserGlue.prototype = {
       let acceptableAge = Services.prefs.getIntPref("app.update.checkInstallTime.days") * millisecondsIn24Hours;
 
       if (buildDate + acceptableAge < today) {
         Cc["@mozilla.org/updates/update-service;1"].getService(Ci.nsIApplicationUpdateService).checkForBackgroundUpdates();
       }
     }
   },
 
+  checkForPendingCrashReports: function() {
+    // We don't process crash reports older than 28 days, so don't bother submitting them
+    const PENDING_CRASH_REPORT_DAYS = 28;
+    if (AppConstants.MOZ_CRASHREPORTER) {
+      let dateLimit = new Date();
+      dateLimit.setDate(dateLimit.getDate() - PENDING_CRASH_REPORT_DAYS);
+      CrashSubmit.pendingIDsAsync(dateLimit).then(
+        function onSuccess(ids) {
+          let count = ids.length;
+          if (count) {
+            let win = RecentWindow.getMostRecentBrowserWindow();
+            let nb =  win.document.getElementById("global-notificationbox");
+            let notification = nb.getNotificationWithValue("pending-crash-reports");
+            if (notification) {
+              return;
+            }
+            let buttons = [
+              {
+                label: win.gNavigatorBundle.getString("pendingCrashReports.submitAll"),
+                callback: function() {
+                  ids.forEach(function(id) {
+                    CrashSubmit.submit(id, {extraExtraKeyVals: {"SubmittedFromInfobar": true}});
+                  });
+                }
+              },
+              {
+                label: win.gNavigatorBundle.getString("pendingCrashReports.ignoreAll"),
+                callback: function() {
+                  ids.forEach(function(id) {
+                    CrashSubmit.ignore(id);
+                  });
+                }
+              },
+              {
+                label: win.gNavigatorBundle.getString("pendingCrashReports.viewAll"),
+                callback: function() {
+                  win.openUILinkIn("about:crashes", "tab");
+                }
+              }
+            ];
+            nb.appendNotification(PluralForm.get(count,
+                                                 win.gNavigatorBundle.getString("pendingCrashReports.label")).replace("#1", count),
+                                  "pending-crash-reports",
+                                  "chrome://browser/skin/tab-crashed.svg",
+                                  nb.PRIORITY_INFO_HIGH, buttons);
+          }
+        },
+        function onError(err) {
+          Cu.reportError(err);
+        }
+      );
+    }
+  },
+
   _onSafeModeRestart: function BG_onSafeModeRestart() {
     // prompt the user to confirm
     let strings = gBrowserBundle;
     let promptTitle = strings.GetStringFromName("safeModeRestartPromptTitle");
     let promptMessage = strings.GetStringFromName("safeModeRestartPromptMessage");
     let restartText = strings.GetStringFromName("safeModeRestartButton");
     let buttonFlags = (Services.prompt.BUTTON_POS_0 *
                        Services.prompt.BUTTON_TITLE_IS_STRING) +
@@ -1071,16 +1130,20 @@ BrowserGlue.prototype = {
         if (removalSuccessful && uninstalledValue == "True") {
           this._resetProfileNotification("uninstall");
         }
       }
     }
 
     this._checkForOldBuildUpdates();
 
+    if ("release" != AppConstants.MOZ_UPDATE_CHANNEL) {
+      this.checkForPendingCrashReports();
+    }
+
     this._firstWindowTelemetry(aWindow);
     this._firstWindowLoaded();
   },
 
   /**
    * Application shutdown handler.
    */
   _onQuitApplicationGranted: function () {
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -744,16 +744,24 @@ certErrorDetailsHSTS.label = HTTP Strict
 certErrorDetailsKeyPinning.label = HTTP Public Key Pinning: %S
 certErrorDetailsCertChain.label = Certificate chain:
 
 # LOCALIZATION NOTE (tabgroups.migration.anonGroup):
 # %S is the group number/ID
 tabgroups.migration.anonGroup = Group %S
 tabgroups.migration.tabGroupBookmarkFolderName = Bookmarked Tab Groups
 
+# LOCALIZATION NOTE (pendingCrashReports.label): Semi-colon list of plural forms
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 is the number of pending crash reports
+pendingCrashReports.label = You have an unsubmitted crash report;You have #1 unsubmitted crash reports
+pendingCrashReports.viewAll = View
+pendingCrashReports.submitAll = Submit
+pendingCrashReports.ignoreAll = Ignore
+
 decoder.noCodecs.button = Learn how
 decoder.noCodecs.accesskey = L
 decoder.noCodecs.message = To play video, you may need to install Microsoft’s Media Feature Pack.
 decoder.noCodecsVista.message = To play video, you may need to install Microsoft’s Platform Update Supplement for Windows Vista.
 decoder.noCodecsXP.message = To play video, you may need to enable Adobe’s Primetime Content Decryption Module.
 decoder.noCodecsLinux.message = To play video, you may need to install the required video codecs.
 decoder.noHWAcceleration.message = To improve video quality, you may need to install Microsoft’s Media Feature Pack.
 decoder.noHWAccelerationVista.message = To improve video quality, you may need to install Microsoft’s Platform Update Supplement for Windows Vista.
--- a/toolkit/crashreporter/CrashSubmit.jsm
+++ b/toolkit/crashreporter/CrashSubmit.jsm
@@ -6,31 +6,33 @@ const {classes: Cc, interfaces: Ci, util
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/KeyValueParser.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.importGlobalProperties(['File']);
 
 XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
                                   "resource://gre/modules/PromiseUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+                                  "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+                                  "resource://gre/modules/Task.jsm");
 
 this.EXPORTED_SYMBOLS = [
   "CrashSubmit"
 ];
 
 const STATE_START = Ci.nsIWebProgressListener.STATE_START;
 const STATE_STOP = Ci.nsIWebProgressListener.STATE_STOP;
 
 const SUCCESS = "success";
 const FAILED  = "failed";
 const SUBMITTING = "submitting";
 
-var reportURL = null;
-var strings = null;
-var myListener = null;
+const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
 
 function parseINIStrings(file) {
   var factory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"].
                 getService(Ci.nsIINIParserFactory);
   var parser = factory.createINIParser(file);
   var obj = {};
   var en = parser.getKeys("Strings");
   while (en.hasMore()) {
@@ -53,36 +55,39 @@ function getL10nStrings() {
     path = path.parent;
     path.append("MacOS");
     path.append("crashreporter.app");
     path.append("Contents");
     path.append("Resources");
     path.append("crashreporter.ini");
     if (!path.exists()) {
       // very bad, but I don't know how to recover
-      return;
+      return null;
     }
   }
   let crstrings = parseINIStrings(path);
-  strings = {
+  let strings = {
     'crashid': crstrings.CrashID,
     'reporturl': crstrings.CrashDetailsURL
   };
 
   path = dirSvc.get("XCurProcD", Ci.nsIFile);
   path.append("crashreporter-override.ini");
   if (path.exists()) {
     crstrings = parseINIStrings(path);
     if ('CrashID' in crstrings)
       strings['crashid'] = crstrings.CrashID;
     if ('CrashDetailsURL' in crstrings)
       strings['reporturl'] = crstrings.CrashDetailsURL;
   }
+  return strings;
 }
 
+XPCOMUtils.defineLazyGetter(this, "strings", getL10nStrings);
+
 function getDir(name) {
   let directoryService = Cc["@mozilla.org/file/directory_service;1"].
                          getService(Ci.nsIProperties);
   let dir = directoryService.get("UAppData", Ci.nsIFile);
   dir.append("Crash Reports");
   dir.append(name);
   return dir;
 }
@@ -471,30 +476,95 @@ this.CrashSubmit = {
     dump.remove(false);
     extra.remove(false);
     if (memory.exists()) {
       memory.remove(false);
     }
   },
 
   /**
+   * Add a .dmg.ignore file along side the .dmp file to indicate that the user
+   * shouldn't be prompted to submit this crash report again.
+   *
+   * @param id
+   *        Filename (minus .dmp extension) of the report to ignore
+   */
+
+  ignore: function CrashSubmit_ignore(id) {
+    let [dump, extra, mem] = getPendingMinidump(id);
+    return OS.File.open(dump.path + ".ignore", {create: true},
+                        {unixFlags: OS.Constants.libc.O_CREAT})
+      .then((file) => {file.close(); });
+  },
+
+  /**
    * Get the list of pending crash IDs.
    *
    * @return an array of string, each being an ID as
    *         expected to be passed to submit()
    */
   pendingIDs: function CrashSubmit_pendingIDs() {
     return getAllPendingMinidumpsIDs();
   },
 
   /**
+   * Get the list of pending crash IDs, excluding those marked to be ignored
+   * @param maxFileDate
+   *     A Date object. Any files last modified before that date will be ignored
+   *
+   * @return a Promise that is fulfilled with an array of string, each
+   * being an ID as expected to be passed to submit() or ignore()
+   */
+  pendingIDsAsync: Task.async(function* CrashSubmit_pendingIDsAsync(maxFileDate) {
+    let ids = [];
+    let info = null;
+    try {
+      info = yield OS.File.stat(getDir("pending").path)
+    } catch (ex) {
+      /* pending dir doesn't exist, ignore */
+      return ids;
+    }
+
+    if (info.isDir) {
+      let iterator = new OS.File.DirectoryIterator(getDir("pending").path);
+      try {
+        yield iterator.forEach(
+          function onEntry(file) {
+            if (file.name.endsWith(".dmp")) {
+              return OS.File.exists(file.path + ".ignore")
+                .then(ignoreExists => {
+                  if (!ignoreExists) {
+                    let id = file.name.slice(0, -4);
+                    if (UUID_REGEX.test(id)) {
+                      return OS.File.stat(file.path)
+                        .then(info => {
+                          if (info.lastAccessDate.valueOf() >
+                              maxFileDate.valueOf()) {
+                            ids.push(id);
+                          }
+                        });
+                    }
+                  }
+                  return null;
+                });
+            }
+            return null;
+          }
+        );
+      } catch(ex) {
+        Cu.reportError(ex);
+      } finally {
+        iterator.close();
+      }
+    }
+    return ids;
+  }),
+
+  /**
    * Prune the saved dumps.
    */
   pruneSavedDumps: function CrashSubmit_pruneSavedDumps() {
     pruneSavedDumps();
   },
 
   // List of currently active submit objects
   _activeSubmissions: []
 };
-
-// Run this when first loaded
-getL10nStrings();