Fri, 16 Jan 2015 18:13:44 +0100
Integrate suggestion from review to improve consistency with existing code.
1 /* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 /**
7 * Crash Monitor
8 *
9 * Monitors execution of a program to detect possible crashes. After
10 * program termination, the monitor can be queried during the next run
11 * to determine whether the last run exited cleanly or not.
12 *
13 * The monitoring is done by registering and listening for special
14 * notifications, or checkpoints, known to be sent by the monitored
15 * program as different stages in the execution are reached. As they
16 * are observed, these notifications are written asynchronously to a
17 * checkpoint file.
18 *
19 * During next program startup the crash monitor reads the checkpoint
20 * file from the last session. If notifications are missing, a crash
21 * has likely happened. By inspecting the notifications present, it is
22 * possible to determine what stages were reached in the program
23 * before the crash.
24 *
25 * Note that since the file is written asynchronously it is possible
26 * that a received notification is lost if the program crashes right
27 * after a checkpoint, but before crash monitor has been able to write
28 * it to disk. Thus, while the presence of a notification in the
29 * checkpoint file tells us that the corresponding stage was reached
30 * during the last run, the absence of a notification after a crash
31 * does not necessarily tell us that the checkpoint wasn't reached.
32 */
34 this.EXPORTED_SYMBOLS = [ "CrashMonitor" ];
36 const Cu = Components.utils;
37 const Cr = Components.results;
39 Cu.import("resource://gre/modules/Services.jsm");
40 Cu.import("resource://gre/modules/osfile.jsm");
41 Cu.import("resource://gre/modules/Promise.jsm");
42 Cu.import("resource://gre/modules/Task.jsm");
43 Cu.import("resource://gre/modules/AsyncShutdown.jsm");
45 const NOTIFICATIONS = [
46 "final-ui-startup",
47 "sessionstore-windows-restored",
48 "quit-application-granted",
49 "quit-application",
50 "profile-change-net-teardown",
51 "profile-change-teardown",
52 "profile-before-change",
53 "sessionstore-final-state-write-complete"
54 ];
56 let CrashMonitorInternal = {
58 /**
59 * Notifications received during the current session.
60 *
61 * Object where a property with a value of |true| means that the
62 * notification of the same name has been received at least once by
63 * the CrashMonitor during this session. Notifications that have not
64 * yet been received are not present as properties. |NOTIFICATIONS|
65 * lists the notifications tracked by the CrashMonitor.
66 */
67 checkpoints: {},
69 /**
70 * Notifications received during previous session.
71 *
72 * Available after |loadPreviousCheckpoints|. Promise which resolves
73 * to an object containing a set of properties, where a property
74 * with a value of |true| means that the notification with the same
75 * name as the property name was received at least once last
76 * session.
77 */
78 previousCheckpoints: null,
80 /* Deferred for AsyncShutdown blocker */
81 profileBeforeChangeDeferred: Promise.defer(),
83 /**
84 * Path to checkpoint file.
85 *
86 * Each time a new notification is received, this file is written to
87 * disc to reflect the information in |checkpoints|. Although Firefox for
88 * Desktop and Metro share the same profile, they need to keep record of
89 * crashes separately.
90 */
91 path: (Services.metro && Services.metro.immersive) ?
92 OS.Path.join(OS.Constants.Path.profileDir, "metro", "sessionCheckpoints.json"):
93 OS.Path.join(OS.Constants.Path.profileDir, "sessionCheckpoints.json"),
95 /**
96 * Load checkpoints from previous session asynchronously.
97 *
98 * @return {Promise} A promise that resolves/rejects once loading is complete
99 */
100 loadPreviousCheckpoints: function () {
101 this.previousCheckpoints = Task.spawn(function*() {
102 let data;
103 try {
104 data = yield OS.File.read(CrashMonitorInternal.path, { encoding: "utf-8" });
105 } catch (ex if ex instanceof OS.File.Error) {
106 if (!ex.becauseNoSuchFile) {
107 Cu.reportError("Error while loading crash monitor data: " + ex.toString());
108 }
110 return null;
111 }
113 let notifications;
114 try {
115 notifications = JSON.parse(data);
116 } catch (ex) {
117 Cu.reportError("Error while parsing crash monitor data: " + ex);
118 return null;
119 }
121 return Object.freeze(notifications);
122 });
124 return this.previousCheckpoints;
125 }
126 };
128 this.CrashMonitor = {
130 /**
131 * Notifications received during previous session.
132 *
133 * Return object containing the set of notifications received last
134 * session as keys with values set to |true|.
135 *
136 * @return {Promise} A promise resolving to previous checkpoints
137 */
138 get previousCheckpoints() {
139 if (!CrashMonitorInternal.initialized) {
140 throw new Error("CrashMonitor must be initialized before getting previous checkpoints");
141 }
143 return CrashMonitorInternal.previousCheckpoints
144 },
146 /**
147 * Initialize CrashMonitor.
148 *
149 * Should only be called from the CrashMonitor XPCOM component.
150 *
151 * @return {Promise}
152 */
153 init: function () {
154 if (CrashMonitorInternal.initialized) {
155 throw new Error("CrashMonitor.init() must only be called once!");
156 }
158 let promise = CrashMonitorInternal.loadPreviousCheckpoints();
159 // Add "profile-after-change" to checkpoint as this method is
160 // called after receiving it
161 CrashMonitorInternal.checkpoints["profile-after-change"] = true;
163 NOTIFICATIONS.forEach(function (aTopic) {
164 Services.obs.addObserver(this, aTopic, false);
165 }, this);
167 // Add shutdown blocker for profile-before-change
168 AsyncShutdown.profileBeforeChange.addBlocker(
169 "CrashMonitor: Writing notifications to file after receiving profile-before-change",
170 CrashMonitorInternal.profileBeforeChangeDeferred.promise
171 );
173 CrashMonitorInternal.initialized = true;
174 if (Services.metro && Services.metro.immersive) {
175 OS.File.makeDir(OS.Path.join(OS.Constants.Path.profileDir, "metro"));
176 }
177 return promise;
178 },
180 /**
181 * Handle registered notifications.
182 *
183 * Update checkpoint file for every new notification received.
184 */
185 observe: function (aSubject, aTopic, aData) {
186 if (!(aTopic in CrashMonitorInternal.checkpoints)) {
187 // If this is the first time this notification is received,
188 // remember it and write it to file
189 CrashMonitorInternal.checkpoints[aTopic] = true;
190 Task.spawn(function() {
191 try {
192 let data = JSON.stringify(CrashMonitorInternal.checkpoints);
194 /* Write to the checkpoint file asynchronously, off the main
195 * thread, for performance reasons. Note that this means
196 * that there's not a 100% guarantee that the file will be
197 * written by the time the notification completes. The
198 * exception is profile-before-change which has a shutdown
199 * blocker. */
200 yield OS.File.writeAtomic(
201 CrashMonitorInternal.path,
202 data, {tmpPath: CrashMonitorInternal.path + ".tmp"});
204 } finally {
205 // Resolve promise for blocker
206 if (aTopic == "profile-before-change") {
207 CrashMonitorInternal.profileBeforeChangeDeferred.resolve();
208 }
209 }
210 });
211 }
213 if (NOTIFICATIONS.every(elem => elem in CrashMonitorInternal.checkpoints)) {
214 // All notifications received, unregister observers
215 NOTIFICATIONS.forEach(function (aTopic) {
216 Services.obs.removeObserver(this, aTopic);
217 }, this);
218 }
219 }
220 };
221 Object.freeze(this.CrashMonitor);