diff options
Diffstat (limited to 'toolkit/components/crashmonitor')
13 files changed, 426 insertions, 0 deletions
diff --git a/toolkit/components/crashmonitor/CrashMonitor.jsm b/toolkit/components/crashmonitor/CrashMonitor.jsm new file mode 100644 index 000000000..34f4f26d1 --- /dev/null +++ b/toolkit/components/crashmonitor/CrashMonitor.jsm @@ -0,0 +1,224 @@ +/* -*- 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. + */ + +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"); + +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: 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|. + */ + 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: function () { + this.previousCheckpoints = Task.spawn(function*() { + let data; + try { + data = yield 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; + } +}; + +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 + 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: 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); diff --git a/toolkit/components/crashmonitor/crashmonitor.manifest b/toolkit/components/crashmonitor/crashmonitor.manifest new file mode 100644 index 000000000..59e336f82 --- /dev/null +++ b/toolkit/components/crashmonitor/crashmonitor.manifest @@ -0,0 +1,3 @@ +component {d9d75e86-8f17-4c57-993e-f738f0d86d42} nsCrashMonitor.js +contract @mozilla.org/toolkit/crashmonitor;1 {d9d75e86-8f17-4c57-993e-f738f0d86d42} +category profile-after-change CrashMonitor @mozilla.org/toolkit/crashmonitor;1 diff --git a/toolkit/components/crashmonitor/moz.build b/toolkit/components/crashmonitor/moz.build new file mode 100644 index 000000000..4656f6ab8 --- /dev/null +++ b/toolkit/components/crashmonitor/moz.build @@ -0,0 +1,16 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini'] + +EXTRA_JS_MODULES += [ + 'CrashMonitor.jsm', +] + +EXTRA_COMPONENTS += [ + 'crashmonitor.manifest', + 'nsCrashMonitor.js', +] diff --git a/toolkit/components/crashmonitor/nsCrashMonitor.js b/toolkit/components/crashmonitor/nsCrashMonitor.js new file mode 100644 index 000000000..41e0dc901 --- /dev/null +++ b/toolkit/components/crashmonitor/nsCrashMonitor.js @@ -0,0 +1,29 @@ +/* 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/. */ + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +var Scope = {} +Components.utils.import("resource://gre/modules/CrashMonitor.jsm", Scope); +var MonitorAPI = Scope.CrashMonitor; + +function CrashMonitor() {} + +CrashMonitor.prototype = { + + classID: Components.ID("{d9d75e86-8f17-4c57-993e-f738f0d86d42}"), + contractID: "@mozilla.org/toolkit/crashmonitor;1", + + QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIObserver]), + + observe: function (aSubject, aTopic, aData) { + switch (aTopic) { + case "profile-after-change": + MonitorAPI.init(); + } + }, +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([CrashMonitor]); diff --git a/toolkit/components/crashmonitor/test/unit/.eslintrc.js b/toolkit/components/crashmonitor/test/unit/.eslintrc.js new file mode 100644 index 000000000..d35787cd2 --- /dev/null +++ b/toolkit/components/crashmonitor/test/unit/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../../testing/xpcshell/xpcshell.eslintrc.js" + ] +}; diff --git a/toolkit/components/crashmonitor/test/unit/head.js b/toolkit/components/crashmonitor/test/unit/head.js new file mode 100644 index 000000000..6d7d50d0c --- /dev/null +++ b/toolkit/components/crashmonitor/test/unit/head.js @@ -0,0 +1,22 @@ +/* -*- 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/. */ + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "OS", + "resource://gre/modules/osfile.jsm"); + +var sessionCheckpointsPath; + +/** + * Start the tasks of the different tests + */ +function run_test() +{ + do_get_profile(); + sessionCheckpointsPath = OS.Path.join(OS.Constants.Path.profileDir, + "sessionCheckpoints.json"); + Components.utils.import("resource://gre/modules/CrashMonitor.jsm"); + run_next_test(); +} diff --git a/toolkit/components/crashmonitor/test/unit/test_init.js b/toolkit/components/crashmonitor/test/unit/test_init.js new file mode 100644 index 000000000..d72f46aca --- /dev/null +++ b/toolkit/components/crashmonitor/test/unit/test_init.js @@ -0,0 +1,17 @@ +/* -*- 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/. */ + +/** + * Test that calling |init| twice throws an error + */ +add_task(function test_init() { + CrashMonitor.init(); + try { + CrashMonitor.init(); + do_check_true(false); + } catch (ex) { + do_check_true(true); + } +}); diff --git a/toolkit/components/crashmonitor/test/unit/test_invalid_file.js b/toolkit/components/crashmonitor/test/unit/test_invalid_file.js new file mode 100644 index 000000000..cc55a2755 --- /dev/null +++ b/toolkit/components/crashmonitor/test/unit/test_invalid_file.js @@ -0,0 +1,22 @@ +/* -*- 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/. */ + +/** + * Test with sessionCheckpoints.json containing invalid data + */ +add_task(function* test_invalid_file() { + // Write bogus data to checkpoint file + let data = "1234"; + yield OS.File.writeAtomic(sessionCheckpointsPath, data, + {tmpPath: sessionCheckpointsPath + ".tmp"}); + + // An invalid file will cause |init| to return null + let status = yield CrashMonitor.init(); + do_check_true(status === null ? true : false); + + // and |previousCheckpoints| will be null + let checkpoints = yield CrashMonitor.previousCheckpoints; + do_check_true(checkpoints === null ? true : false); +}); diff --git a/toolkit/components/crashmonitor/test/unit/test_invalid_json.js b/toolkit/components/crashmonitor/test/unit/test_invalid_json.js new file mode 100644 index 000000000..f3b05208a --- /dev/null +++ b/toolkit/components/crashmonitor/test/unit/test_invalid_json.js @@ -0,0 +1,18 @@ +/* -*- 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/. */ + +/** + * Test with sessionCheckpoints.json containing invalid JSON data + */ +add_task(function* test_invalid_file() { + // Write bogus data to checkpoint file + let data = "[}"; + yield OS.File.writeAtomic(sessionCheckpointsPath, data, + {tmpPath: sessionCheckpointsPath + ".tmp"}); + + CrashMonitor.init(); + let checkpoints = yield CrashMonitor.previousCheckpoints; + do_check_eq(checkpoints, null); +}); diff --git a/toolkit/components/crashmonitor/test/unit/test_missing_file.js b/toolkit/components/crashmonitor/test/unit/test_missing_file.js new file mode 100644 index 000000000..9ce31da95 --- /dev/null +++ b/toolkit/components/crashmonitor/test/unit/test_missing_file.js @@ -0,0 +1,13 @@ +/* -*- 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/. */ + +/** + * Test with non-existing sessionCheckpoints.json + */ +add_task(function* test_missing_file() { + CrashMonitor.init(); + let checkpoints = yield CrashMonitor.previousCheckpoints; + do_check_eq(checkpoints, null); +}); diff --git a/toolkit/components/crashmonitor/test/unit/test_register.js b/toolkit/components/crashmonitor/test/unit/test_register.js new file mode 100644 index 000000000..33c73a5ae --- /dev/null +++ b/toolkit/components/crashmonitor/test/unit/test_register.js @@ -0,0 +1,24 @@ +/* -*- 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/. */ + +/** + * Test that CrashMonitor.jsm is correctly loaded from XPCOM component + */ +add_task(function test_register() { + let cm = Components.classes["@mozilla.org/toolkit/crashmonitor;1"] + .createInstance(Components.interfaces.nsIObserver); + + // Send "profile-after-change" to trigger the initialization + cm.observe(null, "profile-after-change", null); + + // If CrashMonitor was initialized properly a new call to |init| + // should fail + try { + CrashMonitor.init(); + do_check_true(false); + } catch (ex) { + do_check_true(true); + } +}); diff --git a/toolkit/components/crashmonitor/test/unit/test_valid_file.js b/toolkit/components/crashmonitor/test/unit/test_valid_file.js new file mode 100644 index 000000000..d2f214cc0 --- /dev/null +++ b/toolkit/components/crashmonitor/test/unit/test_valid_file.js @@ -0,0 +1,20 @@ +/* -*- 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/. */ + +/** + * Test with sessionCheckpoints.json containing valid data + */ +add_task(function* test_valid_file() { + // Write valid data to checkpoint file + let data = JSON.stringify({"final-ui-startup": true}); + yield OS.File.writeAtomic(sessionCheckpointsPath, data, + {tmpPath: sessionCheckpointsPath + ".tmp"}); + + CrashMonitor.init(); + let checkpoints = yield CrashMonitor.previousCheckpoints; + + do_check_true(checkpoints["final-ui-startup"]); + do_check_eq(Object.keys(checkpoints).length, 1); +}); diff --git a/toolkit/components/crashmonitor/test/unit/xpcshell.ini b/toolkit/components/crashmonitor/test/unit/xpcshell.ini new file mode 100644 index 000000000..cd86b2535 --- /dev/null +++ b/toolkit/components/crashmonitor/test/unit/xpcshell.ini @@ -0,0 +1,11 @@ +[DEFAULT] +head = head.js +tail = +skip-if = toolkit == 'android' + +[test_init.js] +[test_valid_file.js] +[test_invalid_file.js] +[test_invalid_json.js] +[test_missing_file.js] +[test_register.js] |