michael@0: /* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: /** michael@0: * Crash Monitor michael@0: * michael@0: * Monitors execution of a program to detect possible crashes. After michael@0: * program termination, the monitor can be queried during the next run michael@0: * to determine whether the last run exited cleanly or not. michael@0: * michael@0: * The monitoring is done by registering and listening for special michael@0: * notifications, or checkpoints, known to be sent by the monitored michael@0: * program as different stages in the execution are reached. As they michael@0: * are observed, these notifications are written asynchronously to a michael@0: * checkpoint file. michael@0: * michael@0: * During next program startup the crash monitor reads the checkpoint michael@0: * file from the last session. If notifications are missing, a crash michael@0: * has likely happened. By inspecting the notifications present, it is michael@0: * possible to determine what stages were reached in the program michael@0: * before the crash. michael@0: * michael@0: * Note that since the file is written asynchronously it is possible michael@0: * that a received notification is lost if the program crashes right michael@0: * after a checkpoint, but before crash monitor has been able to write michael@0: * it to disk. Thus, while the presence of a notification in the michael@0: * checkpoint file tells us that the corresponding stage was reached michael@0: * during the last run, the absence of a notification after a crash michael@0: * does not necessarily tell us that the checkpoint wasn't reached. michael@0: */ michael@0: michael@0: this.EXPORTED_SYMBOLS = [ "CrashMonitor" ]; michael@0: michael@0: const Cu = Components.utils; michael@0: const Cr = Components.results; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/osfile.jsm"); michael@0: Cu.import("resource://gre/modules/Promise.jsm"); michael@0: Cu.import("resource://gre/modules/Task.jsm"); michael@0: Cu.import("resource://gre/modules/AsyncShutdown.jsm"); michael@0: michael@0: const NOTIFICATIONS = [ michael@0: "final-ui-startup", michael@0: "sessionstore-windows-restored", michael@0: "quit-application-granted", michael@0: "quit-application", michael@0: "profile-change-net-teardown", michael@0: "profile-change-teardown", michael@0: "profile-before-change", michael@0: "sessionstore-final-state-write-complete" michael@0: ]; michael@0: michael@0: let CrashMonitorInternal = { michael@0: michael@0: /** michael@0: * Notifications received during the current session. michael@0: * michael@0: * Object where a property with a value of |true| means that the michael@0: * notification of the same name has been received at least once by michael@0: * the CrashMonitor during this session. Notifications that have not michael@0: * yet been received are not present as properties. |NOTIFICATIONS| michael@0: * lists the notifications tracked by the CrashMonitor. michael@0: */ michael@0: checkpoints: {}, michael@0: michael@0: /** michael@0: * Notifications received during previous session. michael@0: * michael@0: * Available after |loadPreviousCheckpoints|. Promise which resolves michael@0: * to an object containing a set of properties, where a property michael@0: * with a value of |true| means that the notification with the same michael@0: * name as the property name was received at least once last michael@0: * session. michael@0: */ michael@0: previousCheckpoints: null, michael@0: michael@0: /* Deferred for AsyncShutdown blocker */ michael@0: profileBeforeChangeDeferred: Promise.defer(), michael@0: michael@0: /** michael@0: * Path to checkpoint file. michael@0: * michael@0: * Each time a new notification is received, this file is written to michael@0: * disc to reflect the information in |checkpoints|. Although Firefox for michael@0: * Desktop and Metro share the same profile, they need to keep record of michael@0: * crashes separately. michael@0: */ michael@0: path: (Services.metro && Services.metro.immersive) ? michael@0: OS.Path.join(OS.Constants.Path.profileDir, "metro", "sessionCheckpoints.json"): michael@0: OS.Path.join(OS.Constants.Path.profileDir, "sessionCheckpoints.json"), michael@0: michael@0: /** michael@0: * Load checkpoints from previous session asynchronously. michael@0: * michael@0: * @return {Promise} A promise that resolves/rejects once loading is complete michael@0: */ michael@0: loadPreviousCheckpoints: function () { michael@0: this.previousCheckpoints = Task.spawn(function*() { michael@0: let data; michael@0: try { michael@0: data = yield OS.File.read(CrashMonitorInternal.path, { encoding: "utf-8" }); michael@0: } catch (ex if ex instanceof OS.File.Error) { michael@0: if (!ex.becauseNoSuchFile) { michael@0: Cu.reportError("Error while loading crash monitor data: " + ex.toString()); michael@0: } michael@0: michael@0: return null; michael@0: } michael@0: michael@0: let notifications; michael@0: try { michael@0: notifications = JSON.parse(data); michael@0: } catch (ex) { michael@0: Cu.reportError("Error while parsing crash monitor data: " + ex); michael@0: return null; michael@0: } michael@0: michael@0: return Object.freeze(notifications); michael@0: }); michael@0: michael@0: return this.previousCheckpoints; michael@0: } michael@0: }; michael@0: michael@0: this.CrashMonitor = { michael@0: michael@0: /** michael@0: * Notifications received during previous session. michael@0: * michael@0: * Return object containing the set of notifications received last michael@0: * session as keys with values set to |true|. michael@0: * michael@0: * @return {Promise} A promise resolving to previous checkpoints michael@0: */ michael@0: get previousCheckpoints() { michael@0: if (!CrashMonitorInternal.initialized) { michael@0: throw new Error("CrashMonitor must be initialized before getting previous checkpoints"); michael@0: } michael@0: michael@0: return CrashMonitorInternal.previousCheckpoints michael@0: }, michael@0: michael@0: /** michael@0: * Initialize CrashMonitor. michael@0: * michael@0: * Should only be called from the CrashMonitor XPCOM component. michael@0: * michael@0: * @return {Promise} michael@0: */ michael@0: init: function () { michael@0: if (CrashMonitorInternal.initialized) { michael@0: throw new Error("CrashMonitor.init() must only be called once!"); michael@0: } michael@0: michael@0: let promise = CrashMonitorInternal.loadPreviousCheckpoints(); michael@0: // Add "profile-after-change" to checkpoint as this method is michael@0: // called after receiving it michael@0: CrashMonitorInternal.checkpoints["profile-after-change"] = true; michael@0: michael@0: NOTIFICATIONS.forEach(function (aTopic) { michael@0: Services.obs.addObserver(this, aTopic, false); michael@0: }, this); michael@0: michael@0: // Add shutdown blocker for profile-before-change michael@0: AsyncShutdown.profileBeforeChange.addBlocker( michael@0: "CrashMonitor: Writing notifications to file after receiving profile-before-change", michael@0: CrashMonitorInternal.profileBeforeChangeDeferred.promise michael@0: ); michael@0: michael@0: CrashMonitorInternal.initialized = true; michael@0: if (Services.metro && Services.metro.immersive) { michael@0: OS.File.makeDir(OS.Path.join(OS.Constants.Path.profileDir, "metro")); michael@0: } michael@0: return promise; michael@0: }, michael@0: michael@0: /** michael@0: * Handle registered notifications. michael@0: * michael@0: * Update checkpoint file for every new notification received. michael@0: */ michael@0: observe: function (aSubject, aTopic, aData) { michael@0: if (!(aTopic in CrashMonitorInternal.checkpoints)) { michael@0: // If this is the first time this notification is received, michael@0: // remember it and write it to file michael@0: CrashMonitorInternal.checkpoints[aTopic] = true; michael@0: Task.spawn(function() { michael@0: try { michael@0: let data = JSON.stringify(CrashMonitorInternal.checkpoints); michael@0: michael@0: /* Write to the checkpoint file asynchronously, off the main michael@0: * thread, for performance reasons. Note that this means michael@0: * that there's not a 100% guarantee that the file will be michael@0: * written by the time the notification completes. The michael@0: * exception is profile-before-change which has a shutdown michael@0: * blocker. */ michael@0: yield OS.File.writeAtomic( michael@0: CrashMonitorInternal.path, michael@0: data, {tmpPath: CrashMonitorInternal.path + ".tmp"}); michael@0: michael@0: } finally { michael@0: // Resolve promise for blocker michael@0: if (aTopic == "profile-before-change") { michael@0: CrashMonitorInternal.profileBeforeChangeDeferred.resolve(); michael@0: } michael@0: } michael@0: }); michael@0: } michael@0: michael@0: if (NOTIFICATIONS.every(elem => elem in CrashMonitorInternal.checkpoints)) { michael@0: // All notifications received, unregister observers michael@0: NOTIFICATIONS.forEach(function (aTopic) { michael@0: Services.obs.removeObserver(this, aTopic); michael@0: }, this); michael@0: } michael@0: } michael@0: }; michael@0: Object.freeze(this.CrashMonitor);