bug 1269998 - Prompt users with pending crash reports to submit them r=mconley ui-r=shorlander
☠☠ backed out by d4103c64c55c ☠ ☠
authorBrad Lassey <blassey@mozilla.com>
Tue, 10 May 2016 23:50:55 -0700
changeset 338419 53ab4450aff98742ad34122c7d81b1678dc4d6a4
parent 338418 0d2fe6baefe3f8c5a7e6b9aaf5e89dc6e124d50a
child 338420 bade5f18dc94268ddef4d156729d90bc66cf9fa0
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');
@@ -814,16 +819,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) +
@@ -1064,16 +1123,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
@@ -739,16 +739,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();