1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/components/crashmonitor/CrashMonitor.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,221 @@ 1.4 +/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 1.5 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +/** 1.10 + * Crash Monitor 1.11 + * 1.12 + * Monitors execution of a program to detect possible crashes. After 1.13 + * program termination, the monitor can be queried during the next run 1.14 + * to determine whether the last run exited cleanly or not. 1.15 + * 1.16 + * The monitoring is done by registering and listening for special 1.17 + * notifications, or checkpoints, known to be sent by the monitored 1.18 + * program as different stages in the execution are reached. As they 1.19 + * are observed, these notifications are written asynchronously to a 1.20 + * checkpoint file. 1.21 + * 1.22 + * During next program startup the crash monitor reads the checkpoint 1.23 + * file from the last session. If notifications are missing, a crash 1.24 + * has likely happened. By inspecting the notifications present, it is 1.25 + * possible to determine what stages were reached in the program 1.26 + * before the crash. 1.27 + * 1.28 + * Note that since the file is written asynchronously it is possible 1.29 + * that a received notification is lost if the program crashes right 1.30 + * after a checkpoint, but before crash monitor has been able to write 1.31 + * it to disk. Thus, while the presence of a notification in the 1.32 + * checkpoint file tells us that the corresponding stage was reached 1.33 + * during the last run, the absence of a notification after a crash 1.34 + * does not necessarily tell us that the checkpoint wasn't reached. 1.35 + */ 1.36 + 1.37 +this.EXPORTED_SYMBOLS = [ "CrashMonitor" ]; 1.38 + 1.39 +const Cu = Components.utils; 1.40 +const Cr = Components.results; 1.41 + 1.42 +Cu.import("resource://gre/modules/Services.jsm"); 1.43 +Cu.import("resource://gre/modules/osfile.jsm"); 1.44 +Cu.import("resource://gre/modules/Promise.jsm"); 1.45 +Cu.import("resource://gre/modules/Task.jsm"); 1.46 +Cu.import("resource://gre/modules/AsyncShutdown.jsm"); 1.47 + 1.48 +const NOTIFICATIONS = [ 1.49 + "final-ui-startup", 1.50 + "sessionstore-windows-restored", 1.51 + "quit-application-granted", 1.52 + "quit-application", 1.53 + "profile-change-net-teardown", 1.54 + "profile-change-teardown", 1.55 + "profile-before-change", 1.56 + "sessionstore-final-state-write-complete" 1.57 +]; 1.58 + 1.59 +let CrashMonitorInternal = { 1.60 + 1.61 + /** 1.62 + * Notifications received during the current session. 1.63 + * 1.64 + * Object where a property with a value of |true| means that the 1.65 + * notification of the same name has been received at least once by 1.66 + * the CrashMonitor during this session. Notifications that have not 1.67 + * yet been received are not present as properties. |NOTIFICATIONS| 1.68 + * lists the notifications tracked by the CrashMonitor. 1.69 + */ 1.70 + checkpoints: {}, 1.71 + 1.72 + /** 1.73 + * Notifications received during previous session. 1.74 + * 1.75 + * Available after |loadPreviousCheckpoints|. Promise which resolves 1.76 + * to an object containing a set of properties, where a property 1.77 + * with a value of |true| means that the notification with the same 1.78 + * name as the property name was received at least once last 1.79 + * session. 1.80 + */ 1.81 + previousCheckpoints: null, 1.82 + 1.83 + /* Deferred for AsyncShutdown blocker */ 1.84 + profileBeforeChangeDeferred: Promise.defer(), 1.85 + 1.86 + /** 1.87 + * Path to checkpoint file. 1.88 + * 1.89 + * Each time a new notification is received, this file is written to 1.90 + * disc to reflect the information in |checkpoints|. Although Firefox for 1.91 + * Desktop and Metro share the same profile, they need to keep record of 1.92 + * crashes separately. 1.93 + */ 1.94 + path: (Services.metro && Services.metro.immersive) ? 1.95 + OS.Path.join(OS.Constants.Path.profileDir, "metro", "sessionCheckpoints.json"): 1.96 + OS.Path.join(OS.Constants.Path.profileDir, "sessionCheckpoints.json"), 1.97 + 1.98 + /** 1.99 + * Load checkpoints from previous session asynchronously. 1.100 + * 1.101 + * @return {Promise} A promise that resolves/rejects once loading is complete 1.102 + */ 1.103 + loadPreviousCheckpoints: function () { 1.104 + this.previousCheckpoints = Task.spawn(function*() { 1.105 + let data; 1.106 + try { 1.107 + data = yield OS.File.read(CrashMonitorInternal.path, { encoding: "utf-8" }); 1.108 + } catch (ex if ex instanceof OS.File.Error) { 1.109 + if (!ex.becauseNoSuchFile) { 1.110 + Cu.reportError("Error while loading crash monitor data: " + ex.toString()); 1.111 + } 1.112 + 1.113 + return null; 1.114 + } 1.115 + 1.116 + let notifications; 1.117 + try { 1.118 + notifications = JSON.parse(data); 1.119 + } catch (ex) { 1.120 + Cu.reportError("Error while parsing crash monitor data: " + ex); 1.121 + return null; 1.122 + } 1.123 + 1.124 + return Object.freeze(notifications); 1.125 + }); 1.126 + 1.127 + return this.previousCheckpoints; 1.128 + } 1.129 +}; 1.130 + 1.131 +this.CrashMonitor = { 1.132 + 1.133 + /** 1.134 + * Notifications received during previous session. 1.135 + * 1.136 + * Return object containing the set of notifications received last 1.137 + * session as keys with values set to |true|. 1.138 + * 1.139 + * @return {Promise} A promise resolving to previous checkpoints 1.140 + */ 1.141 + get previousCheckpoints() { 1.142 + if (!CrashMonitorInternal.initialized) { 1.143 + throw new Error("CrashMonitor must be initialized before getting previous checkpoints"); 1.144 + } 1.145 + 1.146 + return CrashMonitorInternal.previousCheckpoints 1.147 + }, 1.148 + 1.149 + /** 1.150 + * Initialize CrashMonitor. 1.151 + * 1.152 + * Should only be called from the CrashMonitor XPCOM component. 1.153 + * 1.154 + * @return {Promise} 1.155 + */ 1.156 + init: function () { 1.157 + if (CrashMonitorInternal.initialized) { 1.158 + throw new Error("CrashMonitor.init() must only be called once!"); 1.159 + } 1.160 + 1.161 + let promise = CrashMonitorInternal.loadPreviousCheckpoints(); 1.162 + // Add "profile-after-change" to checkpoint as this method is 1.163 + // called after receiving it 1.164 + CrashMonitorInternal.checkpoints["profile-after-change"] = true; 1.165 + 1.166 + NOTIFICATIONS.forEach(function (aTopic) { 1.167 + Services.obs.addObserver(this, aTopic, false); 1.168 + }, this); 1.169 + 1.170 + // Add shutdown blocker for profile-before-change 1.171 + AsyncShutdown.profileBeforeChange.addBlocker( 1.172 + "CrashMonitor: Writing notifications to file after receiving profile-before-change", 1.173 + CrashMonitorInternal.profileBeforeChangeDeferred.promise 1.174 + ); 1.175 + 1.176 + CrashMonitorInternal.initialized = true; 1.177 + if (Services.metro && Services.metro.immersive) { 1.178 + OS.File.makeDir(OS.Path.join(OS.Constants.Path.profileDir, "metro")); 1.179 + } 1.180 + return promise; 1.181 + }, 1.182 + 1.183 + /** 1.184 + * Handle registered notifications. 1.185 + * 1.186 + * Update checkpoint file for every new notification received. 1.187 + */ 1.188 + observe: function (aSubject, aTopic, aData) { 1.189 + if (!(aTopic in CrashMonitorInternal.checkpoints)) { 1.190 + // If this is the first time this notification is received, 1.191 + // remember it and write it to file 1.192 + CrashMonitorInternal.checkpoints[aTopic] = true; 1.193 + Task.spawn(function() { 1.194 + try { 1.195 + let data = JSON.stringify(CrashMonitorInternal.checkpoints); 1.196 + 1.197 + /* Write to the checkpoint file asynchronously, off the main 1.198 + * thread, for performance reasons. Note that this means 1.199 + * that there's not a 100% guarantee that the file will be 1.200 + * written by the time the notification completes. The 1.201 + * exception is profile-before-change which has a shutdown 1.202 + * blocker. */ 1.203 + yield OS.File.writeAtomic( 1.204 + CrashMonitorInternal.path, 1.205 + data, {tmpPath: CrashMonitorInternal.path + ".tmp"}); 1.206 + 1.207 + } finally { 1.208 + // Resolve promise for blocker 1.209 + if (aTopic == "profile-before-change") { 1.210 + CrashMonitorInternal.profileBeforeChangeDeferred.resolve(); 1.211 + } 1.212 + } 1.213 + }); 1.214 + } 1.215 + 1.216 + if (NOTIFICATIONS.every(elem => elem in CrashMonitorInternal.checkpoints)) { 1.217 + // All notifications received, unregister observers 1.218 + NOTIFICATIONS.forEach(function (aTopic) { 1.219 + Services.obs.removeObserver(this, aTopic); 1.220 + }, this); 1.221 + } 1.222 + } 1.223 +}; 1.224 +Object.freeze(this.CrashMonitor);