1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
6 * Managing safe shutdown of asynchronous services.
8 * Firefox shutdown is composed of phases that take place
9 * sequentially. Typically, each shutdown phase removes some
10 * capabilities from the application. For instance, at the end of
11 * phase profileBeforeChange, no service is permitted to write to the
12 * profile directory (with the exception of Telemetry). Consequently,
13 * if any service has requested I/O to the profile directory before or
14 * during phase profileBeforeChange, the system must be informed that
15 * these requests need to be completed before the end of phase
16 * profileBeforeChange. Failing to inform the system of this
17 * requirement can (and has been known to) cause data loss.
19 * Example: At some point during shutdown, the Add-On Manager needs to
20 * ensure that all add-ons have safely written their data to disk,
21 * before writing its own data. Since the data is saved to the
22 * profile, this must be completed during phase profileBeforeChange.
24 * AsyncShutdown.profileBeforeChange.addBlocker(
25 * "Add-on manager: shutting down",
26 * function condition() {
28 * // Perform I/O that must take place during phase profile-before-change
33 * In this example, function |condition| will be called at some point
34 * during phase profileBeforeChange and phase profileBeforeChange
35 * itself is guaranteed to not terminate until |promise| is either
36 * resolved or rejected.
39 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
43 XPCOMUtils.defineLazyServiceGetter(
46 "@mozilla.org/xpcom/debug;1",
50 // `true` if this is a content process, `false` otherwise.
51 // It would be nicer to go through `Services.appinfo`, but some tests need to be
52 // able to replace that field with a custom implementation before it is first
55 // eslint-disable-next-line mozilla/use-services
56 Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).processType ==
57 Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT;
59 // Display timeout warnings after 10 seconds
60 const DELAY_WARNING_MS = 10 * 1000;
62 // Crash the process if shutdown is really too long
63 // (allowing for sleep).
64 const PREF_DELAY_CRASH_MS = "toolkit.asyncshutdown.crash_timeout";
65 var DELAY_CRASH_MS = Services.prefs.getIntPref(PREF_DELAY_CRASH_MS, 60 * 1000); // One minute
66 Services.prefs.addObserver(PREF_DELAY_CRASH_MS, function () {
67 DELAY_CRASH_MS = Services.prefs.getIntPref(PREF_DELAY_CRASH_MS);
71 * Any addBlocker calls that failed. We add this into barrier wait
72 * crash annotations to help with debugging. When we fail to add
73 * shutdown blockers that can break shutdown. We track these globally
74 * rather than per barrier because one failure mode is when the
75 * barrier has already finished by the time addBlocker is invoked -
76 * but the failure to add the blocker may result in later barriers
77 * waiting indefinitely, so the debug information is still useful
78 * for those later barriers. See bug 1801674 for more context.
80 let gBrokenAddBlockers = [];
83 * A set of Promise that supports waiting.
85 * Promise items may be added or removed during the wait. The wait will
86 * resolve once all Promise items have been resolved or removed.
88 function PromiseSet() {
90 * key: the Promise passed pass the client of the `PromiseSet`.
91 * value: an indirection on top of `key`, as an object with
92 * the following fields:
93 * - indirection: a Promise resolved if `key` is resolved or
94 * if `resolve` is called
95 * - resolve: a function used to resolve the indirection.
97 this._indirections = new Map();
98 // Once all the tracked promises have been resolved we are done. Once Wait()
99 // resolves, it should not be possible anymore to add further promises.
100 // This covers for a possibly rare case, where something may try to add a
101 // blocker after wait() is done, that would never be awaited for.
104 PromiseSet.prototype = {
106 * Wait until all Promise have been resolved or removed.
108 * Note that calling `wait()` causes Promise to be removed from the
109 * Set once they are resolved.
110 * @param {function} onDoneCb invoked synchronously once all the entries
111 * have been handled and no new entries will be accepted.
112 * @return {Promise} Resolved once all Promise have been resolved or removed,
113 * or rejected after at least one Promise has rejected.
116 // Pick an arbitrary element in the map, if any exists.
117 let entry = this._indirections.entries().next();
119 // No indirections left, we are done.
122 return Promise.resolve();
125 let [, indirection] = entry.value;
126 let promise = indirection.promise;
127 promise = promise.then(() =>
128 // At this stage, the entry has been cleaned up.
135 * Add a new Promise to the set.
137 * Calls to wait (including ongoing calls) will only return once
138 * `key` has either resolved or been removed.
142 throw new Error("Wait is complete, cannot add further promises.");
144 this._ensurePromise(key);
145 let indirection = Promise.withResolvers();
149 // Clean up immediately.
150 // This needs to be done before the call to `resolve`, otherwise
151 // `wait()` may loop forever.
152 this._indirections.delete(key);
153 indirection.resolve(x);
156 this._indirections.delete(key);
157 indirection.reject(err);
161 this._indirections.delete(key);
162 // Normally the promise is resolved or rejected, but if its global
163 // goes away, only finally may be invoked. In all the other cases this
164 // is a no-op since the promise has been fulfilled already.
166 new Error("Promise not fulfilled, did it lost its global?")
169 this._indirections.set(key, indirection);
173 * Remove a Promise from the set.
175 * Calls to wait (including ongoing calls) will ignore this promise,
176 * unless it is added again.
179 this._ensurePromise(key);
180 let value = this._indirections.get(key);
184 this._indirections.delete(key);
189 _ensurePromise(key) {
190 if (!key || typeof key != "object") {
191 throw new Error("Expected an object");
193 if (!("then" in key) || typeof key.then != "function") {
194 throw new Error("Expected a Promise");
202 * As this code is generally used during shutdown, there are chances
203 * that the UX will not be available to display warnings on the
204 * console. We therefore use dump() rather than Cu.reportError().
206 function log(msg, prefix = "", error = null) {
208 dump(prefix + msg + "\n");
210 dump(prefix + error + "\n");
211 if (typeof error == "object" && "stack" in error) {
212 dump(prefix + error.stack + "\n");
216 dump("INTERNAL ERROR in AsyncShutdown: cannot log message.\n");
219 const PREF_DEBUG_LOG = "toolkit.asyncshutdown.log";
220 var DEBUG_LOG = Services.prefs.getBoolPref(PREF_DEBUG_LOG, false);
221 Services.prefs.addObserver(PREF_DEBUG_LOG, function () {
222 DEBUG_LOG = Services.prefs.getBoolPref(PREF_DEBUG_LOG);
225 function debug(msg, error = null) {
227 log(msg, "DEBUG: ", error);
230 function warn(msg, error = null) {
231 log(msg, "WARNING: ", error);
233 function fatalerr(msg, error = null) {
234 log(msg, "FATAL ERROR: ", error);
237 // Utility function designed to get the current state of execution
239 // We are a little paranoid here to ensure that in case of evaluation
240 // error we do not block the AsyncShutdown.
241 function safeGetState(fetchState) {
247 // Evaluate fetchState(), normalize the result into something that we can
248 // safely stringify or upload.
249 let state = fetchState();
253 string = JSON.stringify(state);
254 data = JSON.parse(string);
255 // Simplify the rest of the code by ensuring that we can simply
256 // concatenate the result to a message.
257 if (data && typeof data == "object") {
258 data.toString = function () {
264 // Make sure that this causes test failures
271 return "Error getting state: " + ex + " at " + ex.stack;
273 return "Error getting state but could not display error";
279 * Countdown for a given duration, skipping beats if the computer is too busy,
280 * sleeping or otherwise unavailable.
282 * @param {number} delay An approximate delay to wait in milliseconds (rounded
283 * up to the closest second).
287 function looseTimer(delay) {
288 let DELAY_BEAT = 1000;
289 let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
290 let beats = Math.ceil(delay / DELAY_BEAT);
291 let deferred = Promise.withResolvers();
292 timer.initWithCallback(
300 Ci.nsITimer.TYPE_REPEATING_PRECISE_CAN_SKIP
302 // Ensure that the timer is both canceled once we are done with it
303 // and not garbage-collected until then.
304 deferred.promise.then(
305 () => timer.cancel(),
312 * Given an nsIStackFrame object, find the caller filename, line number,
313 * and stack if necessary, and return them as an object.
315 * @param {nsIStackFrame} topFrame Top frame of the call stack.
316 * @param {string} filename Pre-supplied filename or null if unknown.
317 * @param {number} lineNumber Pre-supplied line number or null if unknown.
318 * @param {string} stack Pre-supplied stack or null if unknown.
322 function getOrigin(topFrame, filename = null, lineNumber = null, stack = null) {
324 // Determine the filename and line number of the caller.
325 let frame = topFrame;
327 for (; frame && frame.filename == topFrame.filename; frame = frame.caller) {
328 // Climb up the stack
331 if (filename == null) {
332 filename = frame ? frame.filename : "?";
334 if (lineNumber == null) {
335 lineNumber = frame ? frame.lineNumber : 0;
338 // Now build the rest of the stack as a string.
340 while (frame != null) {
341 stack.push(frame.filename + ":" + frame.name + ":" + frame.lineNumber);
342 frame = frame.caller;
353 filename: "<internal error: could not get origin>",
355 stack: "<internal error: could not get origin>",
361 * {string} topic -> phase
363 var gPhases = new Map();
365 export var AsyncShutdown = {
367 * Access function getPhase. For testing purposes only.
370 let accepted = Services.prefs.getBoolPref(
371 "toolkit.asyncshutdown.testing",
381 * This constant is used as the amount of milliseconds to allow shutdown to be
382 * blocked until we crash the process forcibly and is read from the
383 * 'toolkit.asyncshutdown.crash_timeout' pref.
385 get DELAY_CRASH_MS() {
386 return DELAY_CRASH_MS;
391 * Register a new phase.
393 * @param {string} topic The notification topic for this Phase.
394 * @see {https://developer.mozilla.org/en-US/docs/Observer_Notifications}
396 function getPhase(topic) {
397 let phase = gPhases.get(topic);
401 let spinner = new Spinner(topic);
402 phase = Object.freeze({
404 * Register a blocker for the completion of a phase.
406 * @param {string} name The human-readable name of the blocker. Used
407 * for debugging/error reporting. Please make sure that the name
408 * respects the following model: "Some Service: some action in progress" -
409 * for instance "OS.File: flushing all pending I/O";
410 * @param {function|promise|*} condition A condition blocking the
411 * completion of the phase. Generally, this is a function
412 * returning a promise. This function is evaluated during the
413 * phase and the phase is guaranteed to not terminate until the
414 * resulting promise is either resolved or rejected. If
415 * |condition| is not a function but another value |v|, it behaves
416 * as if it were a function returning |v|.
417 * @param {object*} details Optionally, an object with details
418 * that may be useful for error reporting, as a subset of of the following
420 * - fetchState (strongly recommended) A function returning
421 * information about the current state of the blocker as an
422 * object. Used for providing more details when logging errors or
424 * - stack. A string containing stack information. This module can
425 * generally infer stack information if it is not provided.
426 * - lineNumber A number containing the line number for the caller.
427 * This module can generally infer this information if it is not
429 * - filename A string containing the filename for the caller. This
430 * module can generally infer the information if it is not provided.
433 * AsyncShutdown.profileBeforeChange.addBlocker("Module: just a promise",
434 * promise); // profileBeforeChange will not complete until
435 * // promise is resolved or rejected
437 * AsyncShutdown.profileBeforeChange.addBlocker("Module: a callback",
438 * function callback() {
440 * // Execute this code during profileBeforeChange
442 * // profileBeforeChange will not complete until promise
443 * // is resolved or rejected
446 * AsyncShutdown.profileBeforeChange.addBlocker("Module: trivial callback",
447 * function callback() {
449 * // Execute this code during profileBeforeChange
450 * // No specific guarantee about completion of profileBeforeChange
453 addBlocker(name, condition, details = null) {
454 spinner.addBlocker(name, condition, details);
457 * Remove the blocker for a condition.
459 * If several blockers have been registered for the same
460 * condition, remove all these blockers. If no blocker has been
461 * registered for this condition, this is a noop.
463 * @return {boolean} true if a blocker has been removed, false
464 * otherwise. Note that a result of false may mean either that
465 * the blocker has never been installed or that the phase has
466 * completed and the blocker has already been resolved.
468 removeBlocker(condition) {
469 return spinner.removeBlocker(condition);
477 return spinner.isClosed;
481 * Trigger the phase without having to broadcast a
482 * notification. For testing purposes only.
485 let accepted = Services.prefs.getBoolPref(
486 "toolkit.asyncshutdown.testing",
490 return () => spinner.observe();
495 gPhases.set(topic, phase);
500 * Utility class used to spin the event loop until all blockers for a
501 * Phase are satisfied.
503 * @param {string} topic The xpcom notification for that phase.
505 function Spinner(topic) {
506 this._barrier = new Barrier(topic);
508 Services.obs.addObserver(this, topic);
511 Spinner.prototype = {
513 * Register a new condition for this phase.
515 * See the documentation of `addBlocker` in property `client`
516 * of instances of `Barrier`.
518 addBlocker(name, condition, details) {
519 this._barrier.client.addBlocker(name, condition, details);
522 * Remove the blocker for a condition.
524 * See the documentation of `removeBlocker` in rpoperty `client`
525 * of instances of `Barrier`
527 * @return {boolean} true if a blocker has been removed, false
528 * otherwise. Note that a result of false may mean either that
529 * the blocker has never been installed or that the phase has
530 * completed and the blocker has already been resolved.
532 removeBlocker(condition) {
533 return this._barrier.client.removeBlocker(condition);
537 return this._barrier.client.name;
541 return this._barrier.client.isClosed;
544 // nsIObserver.observe
546 let topic = this._topic;
547 debug(`Starting phase ${topic}`);
548 Services.obs.removeObserver(this, topic);
550 // Setup the promise that will signal our phase's end.
551 let isPhaseEnd = false;
555 warnAfterMS: DELAY_WARNING_MS,
556 crashAfterMS: DELAY_CRASH_MS,
562 debug("Error waiting for notification");
566 // Now, spin the event loop. In case of a hang we will just crash without
567 // ever leaving this loop.
568 debug("Spinning the event loop");
569 Services.tm.spinEventLoopUntil(
570 `AsyncShutdown Spinner for ${topic}`,
574 debug(`Finished phase ${topic}`);
579 * A mechanism used to register blockers that prevent some action from
582 * An instance of |Barrier| provides a capability |client| that
583 * clients can use to register blockers. The barrier is resolved once
584 * all registered blockers have been resolved. The owner of the
585 * |Barrier| may wait for the resolution of the barrier and obtain
586 * information on which blockers have not been resolved yet.
588 * @param {string} name The name of the blocker. Used mainly for error-
591 function Barrier(name) {
593 throw new TypeError("Instances of Barrier need a (non-empty) name");
597 * The set of all Promise for which we need to wait before the barrier
598 * is lifted. Note that this set may be changed while we are waiting.
600 * Set to `null` once the wait is complete.
602 this._waitForMe = new PromiseSet();
605 * A map from conditions, as passed by users during the call to `addBlocker`,
606 * to `promise`, as present in `this._waitForMe`.
608 * Used to let users perform cleanup through `removeBlocker`.
609 * Set to `null` once the wait is complete.
611 * Key: condition (any, as passed by user)
612 * Value: promise used as a key in `this._waitForMe`. Note that there is
613 * no guarantee that the key is still present in `this._waitForMe`.
615 this._conditionToPromise = new Map();
618 * A map from Promise, as present in `this._waitForMe` or
619 * `this._conditionToPromise`, to information on blockers.
621 * Key: Promise (as present in this._waitForMe or this._conditionToPromise).
626 * fetchState: function,
632 this._promiseToBlocker = new Map();
635 * The name of the barrier.
637 if (typeof name != "string") {
638 throw new TypeError("The name of the barrier must be a string");
643 * A cache for the promise returned by wait().
645 this._promise = null;
648 * `true` once we have started waiting.
650 this._isStarted = false;
653 * `true` once we're done and won't accept any new blockers.
655 this._isClosed = false;
658 * The capability of adding blockers. This object may safely be returned
659 * or passed to clients.
663 * The name of the barrier owning this client.
670 * Register a blocker for the completion of this barrier.
672 * @param {string} name The human-readable name of the blocker. Used
673 * for debugging/error reporting. Please make sure that the name
674 * respects the following model: "Some Service: some action in progress" -
675 * for instance "OS.File: flushing all pending I/O";
676 * @param {function|promise|*} condition A condition blocking the
677 * completion of the phase. Generally, this is a function
678 * returning a promise. This function is evaluated during the
679 * phase and the phase is guaranteed to not terminate until the
680 * resulting promise is either resolved or rejected. If
681 * |condition| is not a function but another value |v|, it behaves
682 * as if it were a function returning |v|.
683 * @param {object*} details Optionally, an object with details
684 * that may be useful for error reporting, as a subset of of the following
686 * - fetchState (strongly recommended) A function returning
687 * information about the current state of the blocker as an
688 * object. Used for providing more details when logging errors or
690 * - stack. A string containing stack information. This module can
691 * generally infer stack information if it is not provided.
692 * - lineNumber A number containing the line number for the caller.
693 * This module can generally infer this information if it is not
695 * - filename A string containing the filename for the caller. This
696 * module can generally infer the information if it is not provided.
698 addBlocker: (name, condition, details) => {
699 if (typeof name != "string") {
700 gBrokenAddBlockers.push("No-name blocker");
701 throw new TypeError("Expected a human-readable name as first argument");
703 if (details && typeof details == "function") {
707 } else if (!details) {
710 if (typeof details != "object") {
711 gBrokenAddBlockers.push(`${name} - invalid details`);
713 "Expected an object as third argument to `addBlocker`, got " + details
716 if (!this._waitForMe) {
717 gBrokenAddBlockers.push(`${name} - ${this._name} finished`);
719 `Phase "${this._name}" is finished, it is too late to register completion condition "${name}"`
722 debug(`Adding blocker ${name} for phase ${this._name}`);
725 this.client._internalAddBlocker(name, condition, details);
727 gBrokenAddBlockers.push(`${name} - ${ex.message}`);
732 _internalAddBlocker: (
735 { fetchState = null, filename = null, lineNumber = null, stack = null }
737 if (fetchState != null && typeof fetchState != "function") {
738 throw new TypeError("Expected a function for option `fetchState`");
741 // Split the condition between a trigger function and a promise.
743 // The function to call to notify the blocker that we have started waiting.
744 // This function returns a promise resolved/rejected once the
745 // condition is complete, and never throws.
748 // A promise resolved once the condition is complete.
750 if (typeof condition == "function") {
751 promise = new Promise((resolve, reject) => {
754 resolve(condition());
761 // If `condition` is not a function, `trigger` is not particularly
762 // interesting, and `condition` needs to be normalized to a promise.
764 promise = Promise.resolve(condition);
767 // Make sure that `promise` never rejects.
770 let msg = `A blocker encountered an error while we were waiting.
773 State: ${safeGetState(fetchState)}`;
776 // The error should remain uncaught, to ensure that it
777 // still causes tests to fail.
778 Promise.reject(error);
780 // Added as a last line of defense, in case `warn`, `this._name` or
781 // `safeGetState` somehow throws an error.
785 if (filename == null || lineNumber == null || stack == null) {
786 topFrame = Components.stack;
794 getOrigin: () => getOrigin(topFrame, filename, lineNumber, stack),
797 this._waitForMe.add(promise);
798 this._promiseToBlocker.set(promise, blocker);
799 this._conditionToPromise.set(condition, promise);
801 // As conditions may hold lots of memory, we attempt to cleanup
802 // as soon as we are done (which might be in the next tick, if
803 // we have been passed a resolved promise).
804 promise = promise.then(() => {
805 debug(`Completed blocker ${name} for phase ${this._name}`);
806 this._removeBlocker(condition);
809 if (this._isStarted) {
810 // The wait has already started. The blocker should be
811 // notified asap. We do it out of band as clients probably
812 // expect `addBlocker` to return immediately.
813 Promise.resolve().then(trigger);
818 * Remove the blocker for a condition.
820 * If several blockers have been registered for the same
821 * condition, remove all these blockers. If no blocker has been
822 * registered for this condition, this is a noop.
824 * @return {boolean} true if at least one blocker has been
825 * removed, false otherwise.
827 removeBlocker: condition => {
828 return this._removeBlocker(condition);
833 * Whether this client still accepts new blockers.
835 Object.defineProperty(this.client, "isClosed", {
837 return this._isClosed;
841 Barrier.prototype = Object.freeze({
843 * The current state of the barrier, as a JSON-serializable object
844 * designed for error-reporting.
847 if (!this._isStarted) {
848 return "Not started";
850 if (!this._waitForMe) {
854 for (let blocker of this._promiseToBlocker.values()) {
855 let { name, fetchState } = blocker;
856 let { stack, filename, lineNumber } = blocker.getOrigin();
859 state: safeGetState(fetchState),
869 * Wait until all currently registered blockers are complete.
871 * Once this method has been called, any attempt to register a new blocker
872 * for this barrier will cause an error.
874 * Successive calls to this method always return the same value.
876 * @param {object=} options Optionally, an object that may contain
877 * the following fields:
878 * {number} warnAfterMS If provided and > 0, print a warning if the barrier
879 * has not been resolved after the given number of milliseconds.
880 * {number} crashAfterMS If provided and > 0, crash the process if the barrier
881 * has not been resolved after the give number of milliseconds (rounded up
882 * to the next second). To avoid crashing simply because the computer is busy
883 * or going to sleep, we actually wait for ceil(crashAfterMS/1000) successive
884 * periods of at least one second. Upon crashing, if a crash reporter is present,
885 * prepare a crash report with the state of this barrier.
888 * @return {Promise} A promise satisfied once all blockers are complete.
891 // This method only implements caching on top of _wait()
893 return this._promise;
895 return (this._promise = this._wait(options));
899 if (this._isStarted) {
900 throw new TypeError("Internal error: already started " + this._name);
904 !this._conditionToPromise ||
905 !this._promiseToBlocker
907 throw new TypeError("Internal error: already finished " + this._name);
910 let topic = this._name;
913 for (let blocker of this._promiseToBlocker.values()) {
914 blocker.trigger(); // We have guarantees that this method will never throw
917 this._isStarted = true;
920 let promise = this._waitForMe.wait(() => {
921 this._isClosed = true;
924 promise = promise.catch(function onError(error) {
925 // I don't think that this can happen.
926 // However, let's be overcautious with async/shutdown error reporting.
928 "An uncaught error appeared while completing the phase." +
934 promise = promise.then(() => {
936 this._waitForMe = null;
937 this._promiseToBlocker = null;
938 this._conditionToPromise = null;
941 // Now handle warnings and crashes
942 let warnAfterMS = DELAY_WARNING_MS;
943 if (options && "warnAfterMS" in options) {
945 typeof options.warnAfterMS == "number" ||
946 options.warnAfterMS == null
948 // Change the delay or deactivate warnAfterMS
949 warnAfterMS = options.warnAfterMS;
951 throw new TypeError("Wrong option value for warnAfterMS");
955 if (warnAfterMS && warnAfterMS > 0) {
956 // If the promise takes too long to be resolved/rejected,
957 // we need to notify the user.
958 let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
959 timer.initWithCallback(
962 "At least one completion condition is taking too long to complete." +
964 JSON.stringify(this.state) +
970 Ci.nsITimer.TYPE_ONE_SHOT
973 promise = promise.then(function onSuccess() {
975 // As a side-effect, this prevents |timer| from
976 // being garbage-collected too early.
980 let crashAfterMS = DELAY_CRASH_MS;
981 if (options && "crashAfterMS" in options) {
983 typeof options.crashAfterMS == "number" ||
984 options.crashAfterMS == null
986 // Change the delay or deactivate crashAfterMS
987 crashAfterMS = options.crashAfterMS;
989 throw new TypeError("Wrong option value for crashAfterMS");
993 if (crashAfterMS > 0) {
994 let timeToCrash = null;
996 // If after |crashAfterMS| milliseconds (adjusted to take into
997 // account sleep and otherwise busy computer) we have not finished
998 // this shutdown phase, we assume that the shutdown is somehow
999 // frozen, presumably deadlocked. At this stage, the only thing we
1000 // can do to avoid leaving the user's computer in an unstable (and
1001 // battery-sucking) situation is report the issue and crash.
1002 timeToCrash = looseTimer(crashAfterMS);
1003 timeToCrash.promise.then(
1005 // Report the problem as best as we can, then crash.
1006 let state = this.state;
1008 // If you change the following message, please make sure
1009 // that any information on the topic and state appears
1010 // within the first 200 characters of the message. This
1011 // helps automatically sort oranges.
1013 "AsyncShutdown timeout in " +
1016 JSON.stringify(state) +
1017 " At least one completion condition failed to complete" +
1018 " within a reasonable amount of time. Causing a crash to" +
1019 " ensure that we do not leave the user with an unresponsive" +
1020 " process draining resources.";
1022 if (gBrokenAddBlockers.length) {
1024 "Broken addBlocker calls: " + JSON.stringify(gBrokenAddBlockers)
1027 if (Services.appinfo.crashReporterEnabled) {
1028 Services.appinfo.annotateCrashReport(
1029 "AsyncShutdownTimeout",
1030 JSON.stringify(this._gatherCrashReportTimeoutData(topic, state))
1033 warn("No crash reporter available");
1036 // To help sorting out bugs, we want to make sure that the
1037 // call to nsIDebug2.abort points to a guilty client, rather
1038 // than to AsyncShutdown itself. We pick a client that is
1039 // still blocking and use its filename/lineNumber,
1040 // which have been determined during the call to `addBlocker`.
1042 let lineNumber = -1;
1043 for (let blocker of this._promiseToBlocker.values()) {
1044 ({ filename, lineNumber } = blocker.getOrigin());
1047 lazy.gDebug.abort(filename, lineNumber);
1049 function onSatisfied() {
1050 // The promise has been rejected, which means that we have satisfied
1051 // all completion conditions.
1055 promise = promise.then(
1057 timeToCrash.reject();
1058 } /* No error is possible here*/
1065 _gatherCrashReportTimeoutData(phase, conditions) {
1066 let data = { phase, conditions };
1067 if (gBrokenAddBlockers.length) {
1068 data.brokenAddBlockers = gBrokenAddBlockers;
1073 _removeBlocker(condition) {
1076 !this._promiseToBlocker ||
1077 !this._conditionToPromise
1079 // We have already cleaned up everything.
1083 let promise = this._conditionToPromise.get(condition);
1085 // The blocker has already been removed
1088 this._conditionToPromise.delete(condition);
1089 this._promiseToBlocker.delete(promise);
1090 return this._waitForMe.delete(promise);
1094 // List of well-known phases
1095 // Ideally, phases should be registered from the component that decides
1096 // when they start/stop. For compatibility with existing startup/shutdown
1097 // mechanisms, we register a few phases here.
1101 AsyncShutdown.profileChangeTeardown = getPhase("profile-change-teardown");
1102 AsyncShutdown.profileBeforeChange = getPhase("profile-before-change");
1103 AsyncShutdown.sendTelemetry = getPhase("profile-before-change-telemetry");
1106 // Notifications that fire in the parent and content process, but should
1107 // only have phases in the parent process.
1109 AsyncShutdown.quitApplicationGranted = getPhase("quit-application-granted");
1112 // Don't add a barrier for content-child-shutdown because this
1113 // makes it easier to cause shutdown hangs.
1116 AsyncShutdown.webWorkersShutdown = getPhase("web-workers-shutdown");
1117 AsyncShutdown.xpcomWillShutdown = getPhase("xpcom-will-shutdown");
1119 AsyncShutdown.Barrier = Barrier;
1121 Object.freeze(AsyncShutdown);