gecko/toolkit/components/crashmonitor/CrashMonitor.jsm

234 lines
8.0 KiB
JavaScript

/* -*- 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;
const Cr = Components.results;
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");
Cu.import("resource://gre/modules/NetUtil.jsm");
Cu.import("resource://gre/modules/FileUtils.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|. Although Firefox for
* Desktop and Metro share the same profile, they need to keep record of
* crashes separately.
*/
path: (Services.metro && Services.metro.immersive) ?
OS.Path.join(OS.Constants.Path.profileDir, "metro", "sessionCheckpoints.json"):
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 deferred = Promise.defer();
CrashMonitorInternal.previousCheckpoints = deferred.promise;
let file = FileUtils.File(CrashMonitorInternal.path);
NetUtil.asyncFetch(file, function(inputStream, status) {
if (!Components.isSuccessCode(status)) {
if (status != Cr.NS_ERROR_FILE_NOT_FOUND) {
Cu.reportError("Error while loading crash monitor data: " + status);
}
deferred.resolve(null);
return;
}
let data = NetUtil.readInputStreamToString(inputStream,
inputStream.available(), { charset: "UTF-8" });
let notifications = null;
try {
notifications = JSON.parse(data);
} catch (ex) {
Cu.reportError("Error while parsing crash monitor data: " + ex);
deferred.resolve(null);
}
try {
deferred.resolve(Object.freeze(notifications));
} catch (ex) {
// The only exception we reject from is if notifications is not
// an object. This happens when the checkpoints file contained
// just a numeric string.
deferred.reject(ex);
}
});
return deferred.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;
OS.File.makeDir(OS.Path.join(OS.Constants.Path.profileDir, "metro"));
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);