DXR is a code search and navigation tool aimed at making sense of large projects. It supports full-text and regex searches as well as structural queries.

Mercurial (b6d82b1a6b02)

VCS Links

Line Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226
/* -*- indent-tabs-mode: nil; js-indent-level: 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.
 */

var EXPORTED_SYMBOLS = ["CrashMonitor"];

const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
const { PromiseUtils } = ChromeUtils.import(
  "resource://gre/modules/PromiseUtils.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",
];

var 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: PromiseUtils.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() {
    this.previousCheckpoints = (async function() {
      let data;
      try {
        data = await OS.File.read(CrashMonitorInternal.path, {
          encoding: "utf-8",
        });
      } catch (ex) {
        if (!(ex instanceof OS.File.Error)) {
          throw ex;
        }
        if (!ex.becauseNoSuchFile) {
          Cu.reportError(
            "Error while loading crash monitor data: " + ex.toString()
          );
        }

        return null;
      }

      let notifications;
      try {
        notifications = JSON.parse(data);
      } catch (ex) {
        Cu.reportError("Error while parsing crash monitor data: " + ex);
        return null;
      }

      // If `notifications` isn't an object, then the monitor data isn't valid.
      if (Object(notifications) !== notifications) {
        Cu.reportError(
          "Error while parsing crash monitor data: invalid monitor data"
        );
        return null;
      }

      return Object.freeze(notifications);
    })();

    return this.previousCheckpoints;
  },
};

var 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() {
    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);
    }, this);

    // Add shutdown blocker for profile-before-change
    OS.File.profileBeforeChange.addBlocker(
      "CrashMonitor: Writing notifications to file after receiving profile-before-change",
      CrashMonitorInternal.profileBeforeChangeDeferred.promise,
      () => this.checkpoints
    );

    CrashMonitorInternal.initialized = true;
    return promise;
  },

  /**
   * Handle registered notifications.
   *
   * Update checkpoint file for every new notification received.
   */
  observe(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;
      (async 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. */
          await 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);