Bug 888373: Simple API for detecting startup/shutdown crashes;r=yoric,gps
authorMichael Brennan <brennan.brisad@gmail.com>
Fri, 15 Nov 2013 16:46:44 +0100
changeset 161065 dfdfcd1c37ffde7eed163662f0b2d47bbb1e32fa
parent 161064 a97cb4ad5ba1c79d1153a15e0175849b562586fc
child 161066 ebfa7ef811a6baaaf303d4875be9bb89ad8c3f9c
push id4063
push userttaubert@mozilla.com
push dateWed, 18 Dec 2013 21:04:07 +0000
treeherderfx-team@dfdfcd1c37ff [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersyoric, gps
bugs888373
milestone29.0a1
Bug 888373: Simple API for detecting startup/shutdown crashes;r=yoric,gps
browser/installer/package-manifest.in
toolkit/components/crashmonitor/CrashMonitor.jsm
toolkit/components/crashmonitor/crashmonitor.manifest
toolkit/components/crashmonitor/moz.build
toolkit/components/crashmonitor/nsCrashMonitor.js
toolkit/components/moz.build
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -374,16 +374,18 @@
 @BINPATH@/browser/components/DownloadsStartup.js
 @BINPATH@/browser/components/DownloadsUI.js
 @BINPATH@/browser/components/BrowserPlaces.manifest
 @BINPATH@/browser/components/devtools-clhandler.manifest
 @BINPATH@/browser/components/devtools-clhandler.js
 @BINPATH@/components/Downloads.manifest
 @BINPATH@/components/DownloadLegacy.js
 @BINPATH@/components/BrowserPageThumbs.manifest
+@BINPATH@/components/crashmonitor.manifest
+@BINPATH@/components/nsCrashMonitor.js
 @BINPATH@/components/SiteSpecificUserAgent.js
 @BINPATH@/components/SiteSpecificUserAgent.manifest
 @BINPATH@/components/toolkitsearch.manifest
 @BINPATH@/components/nsSearchService.js
 @BINPATH@/components/nsSearchSuggestions.js
 @BINPATH@/components/passwordmgr.manifest
 @BINPATH@/components/nsLoginInfo.js
 @BINPATH@/components/nsLoginManager.js
new file mode 100644
--- /dev/null
+++ b/toolkit/components/crashmonitor/CrashMonitor.jsm
@@ -0,0 +1,208 @@
+/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/**
+ * Crash Monitor
+ *
+ * Monitors execution of a program to detect possible crashes. After
+ * program termination, the monitor can be queried during the next run
+ * to determine whether the last run exited cleanly or not.
+ *
+ * The monitoring is done by registering and listening for special
+ * notifications, or checkpoints, known to be sent by the monitored
+ * program as different stages in the execution are reached. As they
+ * are observed, these notifications are written asynchronously to a
+ * checkpoint file.
+ *
+ * During next program startup the crash monitor reads the checkpoint
+ * file from the last session. If notifications are missing, a crash
+ * has likely happened. By inspecting the notifications present, it is
+ * possible to determine what stages were reached in the program
+ * before the crash.
+ *
+ * Note that since the file is written asynchronously it is possible
+ * that a received notification is lost if the program crashes right
+ * after a checkpoint, but before crash monitor has been able to write
+ * it to disk. Thus, while the presence of a notification in the
+ * checkpoint file tells us that the corresponding stage was reached
+ * during the last run, the absence of a notification after a crash
+ * does not necessarily tell us that the checkpoint wasn't reached.
+ */
+
+this.EXPORTED_SYMBOLS = [ "CrashMonitor" ];
+
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/AsyncShutdown.jsm");
+
+const NOTIFICATIONS = [
+  "final-ui-startup",
+  "sessionstore-windows-restored",
+  "quit-application-granted",
+  "quit-application",
+  "profile-change-net-teardown",
+  "profile-change-teardown",
+  "profile-before-change",
+  "sessionstore-final-state-write-complete"
+];
+
+let CrashMonitorInternal = {
+
+  /**
+   * Notifications received during the current session.
+   *
+   * Object where a property with a value of |true| means that the
+   * notification of the same name has been received at least once by
+   * the CrashMonitor during this session. Notifications that have not
+   * yet been received are not present as properties. |NOTIFICATIONS|
+   * lists the notifications tracked by the CrashMonitor.
+   */
+  checkpoints: {},
+
+  /**
+   * Notifications received during previous session.
+   *
+   * Available after |loadPreviousCheckpoints|. Promise which resolves
+   * to an object containing a set of properties, where a property
+   * with a value of |true| means that the notification with the same
+   * name as the property name was received at least once last
+   * session.
+   */
+  previousCheckpoints: null,
+
+  /* Deferred for AsyncShutdown blocker */
+  profileBeforeChangeDeferred: Promise.defer(),
+
+  /**
+   * Path to checkpoint file.
+   *
+   * Each time a new notification is received, this file is written to
+   * disc to reflect the information in |checkpoints|.
+   */
+  path: OS.Path.join(OS.Constants.Path.profileDir, "sessionCheckpoints.json"),
+
+  /**
+   * Load checkpoints from previous session asynchronously.
+   *
+   * @return {Promise} A promise that resolves/rejects once loading is complete
+   */
+  loadPreviousCheckpoints: function () {
+    let promise = Task.spawn(function () {
+      let notifications;
+      try {
+        let decoder = new TextDecoder();
+        let data = yield OS.File.read(CrashMonitorInternal.path);
+        let contents = decoder.decode(data);
+        notifications = JSON.parse(contents);
+      } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
+        // If checkpoint file cannot be read
+        throw new Task.Result(null);
+      } catch (ex) {
+        Cu.reportError("Error while loading crash monitor data: " + ex);
+        throw new Task.Result(null);
+      }
+      throw new Task.Result(Object.freeze(notifications));
+    });
+
+    CrashMonitorInternal.previousCheckpoints = promise;
+    return promise;
+  }
+};
+
+this.CrashMonitor = {
+
+  /**
+   * Notifications received during previous session.
+   *
+   * Return object containing the set of notifications received last
+   * session as keys with values set to |true|.
+   *
+   * @return {Promise} A promise resolving to previous checkpoints
+   */
+  get previousCheckpoints() {
+    if (!CrashMonitorInternal.initialized) {
+      throw new Error("CrashMonitor must be initialized before getting previous checkpoints");
+    }
+
+    return CrashMonitorInternal.previousCheckpoints
+  },
+
+  /**
+   * Initialize CrashMonitor.
+   *
+   * Should only be called from the CrashMonitor XPCOM component.
+   *
+   * @return {Promise}
+   */
+  init: function () {
+    if (CrashMonitorInternal.initialized) {
+      throw new Error("CrashMonitor.init() must only be called once!");
+    }
+
+    let promise = CrashMonitorInternal.loadPreviousCheckpoints();
+    // Add "profile-after-change" to checkpoint as this method is
+    // called after receiving it
+    CrashMonitorInternal.checkpoints["profile-after-change"] = true;
+
+    NOTIFICATIONS.forEach(function (aTopic) {
+      Services.obs.addObserver(this, aTopic, false);
+    }, this);
+
+    // Add shutdown blocker for profile-before-change
+    AsyncShutdown.profileBeforeChange.addBlocker(
+      "CrashMonitor: Writing notifications to file after receiving profile-before-change",
+      CrashMonitorInternal.profileBeforeChangeDeferred.promise
+    );
+
+    CrashMonitorInternal.initialized = true;
+    return promise;
+  },
+
+  /**
+   * Handle registered notifications.
+   *
+   * Update checkpoint file for every new notification received.
+   */
+  observe: function (aSubject, aTopic, aData) {
+    if (!(aTopic in CrashMonitorInternal.checkpoints)) {
+      // If this is the first time this notification is received,
+      // remember it and write it to file
+      CrashMonitorInternal.checkpoints[aTopic] = true;
+      Task.spawn(function() {
+        try {
+          let data = JSON.stringify(CrashMonitorInternal.checkpoints);
+
+          /* Write to the checkpoint file asynchronously, off the main
+           * thread, for performance reasons. Note that this means
+           * that there's not a 100% guarantee that the file will be
+           * written by the time the notification completes. The
+           * exception is profile-before-change which has a shutdown
+           * blocker. */
+          yield OS.File.writeAtomic(
+            CrashMonitorInternal.path,
+            data, {tmpPath: CrashMonitorInternal.path + ".tmp"});
+
+        } finally {
+          // Resolve promise for blocker
+          if (aTopic == "profile-before-change") {
+            CrashMonitorInternal.profileBeforeChangeDeferred.resolve();
+          }
+        }
+      });
+    }
+
+    if (NOTIFICATIONS.every(elem => elem in CrashMonitorInternal.checkpoints)) {
+      // All notifications received, unregister observers
+      NOTIFICATIONS.forEach(function (aTopic) {
+        Services.obs.removeObserver(this, aTopic);
+      }, this);
+    }
+  }
+};
+Object.freeze(this.CrashMonitor);
new file mode 100644
--- /dev/null
+++ b/toolkit/components/crashmonitor/crashmonitor.manifest
@@ -0,0 +1,3 @@
+component {d9d75e86-8f17-4c57-993e-f738f0d86d42} nsCrashMonitor.js
+contract @mozilla.org/toolkit/crashmonitor;1 {d9d75e86-8f17-4c57-993e-f738f0d86d42}
+category profile-after-change CrashMonitor @mozilla.org/toolkit/crashmonitor;1
new file mode 100644
--- /dev/null
+++ b/toolkit/components/crashmonitor/moz.build
@@ -0,0 +1,17 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+EXTRA_JS_MODULES = [
+    'CrashMonitor.jsm',
+]
+
+EXTRA_COMPONENTS += [
+    'crashmonitor.manifest',
+]
+
+EXTRA_PP_COMPONENTS += [
+    'nsCrashMonitor.js',
+]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/crashmonitor/nsCrashMonitor.js
@@ -0,0 +1,29 @@
+/* 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/. */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+let Scope = {}
+Components.utils.import("resource://gre/modules/CrashMonitor.jsm", Scope);
+let MonitorAPI = Scope.CrashMonitor;
+
+function CrashMonitor() {};
+
+CrashMonitor.prototype = {
+
+  classID: Components.ID("{d9d75e86-8f17-4c57-993e-f738f0d86d42}"),
+  contractID: "@mozilla.org/toolkit/crashmonitor;1",
+
+  QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIObserver]),
+
+  observe: function (aSubject, aTopic, aData) {
+    switch (aTopic) {
+    case "profile-after-change":
+      MonitorAPI.init();
+    }
+  },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([CrashMonitor]);
--- a/toolkit/components/moz.build
+++ b/toolkit/components/moz.build
@@ -11,16 +11,17 @@ if CONFIG['MOZ_ENABLE_XREMOTE']:
 PARALLEL_DIRS += [
     'aboutmemory',
     'alerts',
     'apppicker',
     'commandlines',
     'console',
     'contentprefs',
     'cookie',
+    'crashmonitor',
     'diskspacewatcher',
     'downloads',
     'exthelper',
     'filepicker',
     'finalizationwitness',
     'find',
     'intl',
     'jsdownloads',