diff options
Diffstat (limited to 'toolkit/components/crashmonitor')
12 files changed, 452 insertions, 0 deletions
diff --git a/toolkit/components/crashmonitor/CrashMonitor.sys.mjs b/toolkit/components/crashmonitor/CrashMonitor.sys.mjs new file mode 100644 index 0000000000..c4dfdcbd83 --- /dev/null +++ b/toolkit/components/crashmonitor/CrashMonitor.sys.mjs @@ -0,0 +1,233 @@ +/* -*- 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. + */ + +import { PrivateBrowsingUtils } from "resource://gre/modules/PrivateBrowsingUtils.sys.mjs"; + +const SESSIONSTORE_WINDOWS_RESTORED_TOPIC = "sessionstore-windows-restored"; +const SESSIONSTORE_FINAL_STATE_WRITE_COMPLETE_TOPIC = + "sessionstore-final-state-write-complete"; + +const NOTIFICATIONS = [ + "final-ui-startup", + SESSIONSTORE_WINDOWS_RESTORED_TOPIC, + "quit-application-granted", + "quit-application", + "profile-change-net-teardown", + "profile-change-teardown", + SESSIONSTORE_FINAL_STATE_WRITE_COMPLETE_TOPIC, +]; + +const SHUTDOWN_PHASES = ["profile-before-change"]; + +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: {}, + + /** + * A deferred promise that resolves when all checkpoints have been written. + */ + sessionStoreFinalWriteComplete: Promise.withResolvers(), + + /** + * 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, + + /** + * Path to checkpoint file. + * + * Each time a new notification is received, this file is written to + * disc to reflect the information in |checkpoints|. + */ + path: PathUtils.join( + Services.dirsvc.get("ProfD", Ci.nsIFile).path, + "sessionCheckpoints.json" + ), + + /** + * Load checkpoints from previous session asynchronously. + * + * @return {Promise} A promise that resolves/rejects once loading is complete + */ + loadPreviousCheckpoints() { + this.previousCheckpoints = (async function () { + let notifications; + try { + notifications = await IOUtils.readJSON(CrashMonitorInternal.path); + } catch (ex) { + // Ignore file not found errors, but report all others. + if (ex.name !== "NotFoundError") { + console.error("Error while loading crash monitor data:", ex.message); + } + return null; + } + + // If `notifications` isn't an object, then the monitor data isn't valid. + if (Object(notifications) !== notifications) { + console.error( + "Error while parsing crash monitor data: invalid monitor data" + ); + return null; + } + + return Object.freeze(notifications); + })(); + + return this.previousCheckpoints; + }, +}; + +export var 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() { + 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); + }, this); + + // Add shutdown blocker for profile-before-change + IOUtils.profileBeforeChange.addBlocker( + "CrashMonitor: Writing notifications to file after receiving profile-before-change and awaiting all checkpoints written", + async () => { + await this.writeCheckpoint("profile-before-change"); + + // If SessionStore has not initialized, we don't want to wait for + // checkpoints that we won't hit, or we'll crash the browser during + // async shutdown. + if ( + !PrivateBrowsingUtils.permanentPrivateBrowsing && + CrashMonitorInternal.checkpoints[SESSIONSTORE_WINDOWS_RESTORED_TOPIC] + ) { + await CrashMonitorInternal.sessionStoreFinalWriteComplete.promise; + } + }, + () => CrashMonitorInternal.checkpoints + ); + + CrashMonitorInternal.initialized = true; + return promise; + }, + + /** + * Handle registered notifications. + * + * Update checkpoint file for every new notification received. + */ + observe(aSubject, aTopic, aData) { + this.writeCheckpoint(aTopic); + + if ( + NOTIFICATIONS.every(elem => elem in CrashMonitorInternal.checkpoints) && + SHUTDOWN_PHASES.every(elem => elem in CrashMonitorInternal.checkpoints) + ) { + // All notifications received, unregister observers + NOTIFICATIONS.forEach(function (aTopic) { + Services.obs.removeObserver(this, aTopic); + }, this); + } + + if (aTopic === SESSIONSTORE_FINAL_STATE_WRITE_COMPLETE_TOPIC) { + CrashMonitorInternal.sessionStoreFinalWriteComplete.resolve(); + } + }, + + async writeCheckpoint(aCheckpoint) { + if (!(aCheckpoint in CrashMonitorInternal.checkpoints)) { + // If this is the first time this notification is received, + // remember it and write it to file + CrashMonitorInternal.checkpoints[aCheckpoint] = true; + + /* 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. */ + await IOUtils.writeJSON( + CrashMonitorInternal.path, + CrashMonitorInternal.checkpoints, + { + tmpPath: CrashMonitorInternal.path + ".tmp", + } + ); + } + }, +}; + +Object.freeze(CrashMonitor); diff --git a/toolkit/components/crashmonitor/components.conf b/toolkit/components/crashmonitor/components.conf new file mode 100644 index 0000000000..e9f9d6a2dd --- /dev/null +++ b/toolkit/components/crashmonitor/components.conf @@ -0,0 +1,15 @@ +# -*- 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/. + +Classes = [ + { + 'cid': '{d9d75e86-8f17-4c57-993e-f738f0d86d42}', + 'contract_ids': ['@mozilla.org/toolkit/crashmonitor;1'], + 'esModule': 'resource://gre/modules/nsCrashMonitor.sys.mjs', + 'constructor': 'CrashMonitor', + 'categories': {'profile-after-change': 'CrashMonitor'}, + }, +] diff --git a/toolkit/components/crashmonitor/moz.build b/toolkit/components/crashmonitor/moz.build new file mode 100644 index 0000000000..522ad326b2 --- /dev/null +++ b/toolkit/components/crashmonitor/moz.build @@ -0,0 +1,19 @@ +# -*- 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/. + +with Files("**"): + BUG_COMPONENT = ("Toolkit", "General") + +XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.toml"] + +EXTRA_JS_MODULES += [ + "CrashMonitor.sys.mjs", + "nsCrashMonitor.sys.mjs", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] diff --git a/toolkit/components/crashmonitor/nsCrashMonitor.sys.mjs b/toolkit/components/crashmonitor/nsCrashMonitor.sys.mjs new file mode 100644 index 0000000000..0e91fc0859 --- /dev/null +++ b/toolkit/components/crashmonitor/nsCrashMonitor.sys.mjs @@ -0,0 +1,23 @@ +/* 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/. */ + +var MonitorAPI = ChromeUtils.importESModule( + "resource://gre/modules/CrashMonitor.sys.mjs" +).CrashMonitor; + +export function CrashMonitor() {} + +CrashMonitor.prototype = { + classID: Components.ID("{d9d75e86-8f17-4c57-993e-f738f0d86d42}"), + contractID: "@mozilla.org/toolkit/crashmonitor;1", + + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + + observe(aSubject, aTopic, aData) { + switch (aTopic) { + case "profile-after-change": + MonitorAPI.init(); + } + }, +}; diff --git a/toolkit/components/crashmonitor/test/unit/head.js b/toolkit/components/crashmonitor/test/unit/head.js new file mode 100644 index 0000000000..1d3be083de --- /dev/null +++ b/toolkit/components/crashmonitor/test/unit/head.js @@ -0,0 +1,26 @@ +/* -*- 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/. */ + +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +var sessionCheckpointsPath; +var CrashMonitor; + +/** + * Start the tasks of the different tests + */ +function run_test() { + do_get_profile(); + sessionCheckpointsPath = PathUtils.join( + PathUtils.profileDir, + "sessionCheckpoints.json" + ); + ({ CrashMonitor } = ChromeUtils.importESModule( + "resource://gre/modules/CrashMonitor.sys.mjs" + )); + 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 0000000000..633a5701d6 --- /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(); + Assert.ok(false); + } catch (ex) { + Assert.ok(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 0000000000..f64fc9183c --- /dev/null +++ b/toolkit/components/crashmonitor/test/unit/test_invalid_file.js @@ -0,0 +1,23 @@ +/* -*- 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(async function test_invalid_file() { + // Write bogus data to checkpoint file + let data = "1234"; + await IOUtils.writeUTF8(sessionCheckpointsPath, data, { + tmpPath: sessionCheckpointsPath + ".tmp", + }); + + // An invalid file will cause |init| to return null + let status = await CrashMonitor.init(); + Assert.ok(!!(status === null)); + + // and |previousCheckpoints| will be null + let checkpoints = await CrashMonitor.previousCheckpoints; + Assert.ok(!!(checkpoints === null)); +}); 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 0000000000..d49852a280 --- /dev/null +++ b/toolkit/components/crashmonitor/test/unit/test_invalid_json.js @@ -0,0 +1,19 @@ +/* -*- 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(async function test_invalid_file() { + // Write bogus data to checkpoint file + let data = "[}"; + await IOUtils.writeUTF8(sessionCheckpointsPath, data, { + tmpPath: sessionCheckpointsPath + ".tmp", + }); + + CrashMonitor.init(); + let checkpoints = await CrashMonitor.previousCheckpoints; + Assert.equal(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 0000000000..a8b86fe6c1 --- /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(async function test_missing_file() { + CrashMonitor.init(); + let checkpoints = await CrashMonitor.previousCheckpoints; + Assert.equal(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 0000000000..1957af4ee1 --- /dev/null +++ b/toolkit/components/crashmonitor/test/unit/test_register.js @@ -0,0 +1,25 @@ +/* -*- 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 = Cc["@mozilla.org/toolkit/crashmonitor;1"].createInstance( + Ci.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(); + Assert.ok(false); + } catch (ex) { + Assert.ok(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 0000000000..2a15d25a08 --- /dev/null +++ b/toolkit/components/crashmonitor/test/unit/test_valid_file.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 with sessionCheckpoints.json containing valid data + */ +add_task(async function test_valid_file() { + // Write valid data to checkpoint file + await IOUtils.writeJSON( + sessionCheckpointsPath, + { "final-ui-startup": true }, + { + tmpPath: sessionCheckpointsPath + ".tmp", + } + ); + + CrashMonitor.init(); + let checkpoints = await CrashMonitor.previousCheckpoints; + + Assert.ok(checkpoints["final-ui-startup"]); + Assert.equal(Object.keys(checkpoints).length, 1); +}); diff --git a/toolkit/components/crashmonitor/test/unit/xpcshell.toml b/toolkit/components/crashmonitor/test/unit/xpcshell.toml new file mode 100644 index 0000000000..87e09a0089 --- /dev/null +++ b/toolkit/components/crashmonitor/test/unit/xpcshell.toml @@ -0,0 +1,15 @@ +[DEFAULT] +head = "head.js" +skip-if = ["os == 'android'"] + +["test_init.js"] + +["test_invalid_file.js"] + +["test_invalid_json.js"] + +["test_missing_file.js"] + +["test_register.js"] + +["test_valid_file.js"] |