|
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/. */ |
|
4 |
|
5 /** |
|
6 * Managing safe shutdown of asynchronous services. |
|
7 * |
|
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. |
|
18 * |
|
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. |
|
23 * |
|
24 * AsyncShutdown.profileBeforeChange.addBlocker( |
|
25 * "Add-on manager: shutting down", |
|
26 * function condition() { |
|
27 * // Do things. |
|
28 * // Perform I/O that must take place during phase profile-before-change |
|
29 * return promise; |
|
30 * } |
|
31 * }); |
|
32 * |
|
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. |
|
37 */ |
|
38 |
|
39 "use strict"; |
|
40 |
|
41 const Cu = Components.utils; |
|
42 const Cc = Components.classes; |
|
43 const Ci = Components.interfaces; |
|
44 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); |
|
45 Cu.import("resource://gre/modules/Services.jsm", this); |
|
46 |
|
47 XPCOMUtils.defineLazyModuleGetter(this, "Promise", |
|
48 "resource://gre/modules/Promise.jsm"); |
|
49 XPCOMUtils.defineLazyServiceGetter(this, "gDebug", |
|
50 "@mozilla.org/xpcom/debug;1", "nsIDebug"); |
|
51 Object.defineProperty(this, "gCrashReporter", { |
|
52 get: function() { |
|
53 delete this.gCrashReporter; |
|
54 try { |
|
55 let reporter = Cc["@mozilla.org/xre/app-info;1"]. |
|
56 getService(Ci.nsICrashReporter); |
|
57 return this.gCrashReporter = reporter; |
|
58 } catch (ex) { |
|
59 return this.gCrashReporter = null; |
|
60 } |
|
61 }, |
|
62 configurable: true |
|
63 }); |
|
64 |
|
65 // Display timeout warnings after 10 seconds |
|
66 const DELAY_WARNING_MS = 10 * 1000; |
|
67 |
|
68 |
|
69 // Crash the process if shutdown is really too long |
|
70 // (allowing for sleep). |
|
71 const PREF_DELAY_CRASH_MS = "toolkit.asyncshutdown.crash_timeout"; |
|
72 let DELAY_CRASH_MS = 60 * 1000; // One minute |
|
73 try { |
|
74 DELAY_CRASH_MS = Services.prefs.getIntPref(PREF_DELAY_CRASH_MS); |
|
75 } catch (ex) { |
|
76 // Ignore errors |
|
77 } |
|
78 Services.prefs.addObserver(PREF_DELAY_CRASH_MS, function() { |
|
79 DELAY_CRASH_MS = Services.prefs.getIntPref(PREF_DELAY_CRASH_MS); |
|
80 }, false); |
|
81 |
|
82 |
|
83 /** |
|
84 * Display a warning. |
|
85 * |
|
86 * As this code is generally used during shutdown, there are chances |
|
87 * that the UX will not be available to display warnings on the |
|
88 * console. We therefore use dump() rather than Cu.reportError(). |
|
89 */ |
|
90 function log(msg, prefix = "", error = null) { |
|
91 dump(prefix + msg + "\n"); |
|
92 if (error) { |
|
93 dump(prefix + error + "\n"); |
|
94 if (typeof error == "object" && "stack" in error) { |
|
95 dump(prefix + error.stack + "\n"); |
|
96 } |
|
97 } |
|
98 } |
|
99 function warn(msg, error = null) { |
|
100 return log(msg, "WARNING: ", error); |
|
101 } |
|
102 function err(msg, error = null) { |
|
103 return log(msg, "ERROR: ", error); |
|
104 } |
|
105 |
|
106 // Utility function designed to get the current state of execution |
|
107 // of a blocker. |
|
108 // We are a little paranoid here to ensure that in case of evaluation |
|
109 // error we do not block the AsyncShutdown. |
|
110 function safeGetState(state) { |
|
111 if (!state) { |
|
112 return "(none)"; |
|
113 } |
|
114 let data, string; |
|
115 try { |
|
116 // Evaluate state(), normalize the result into something that we can |
|
117 // safely stringify or upload. |
|
118 string = JSON.stringify(state()); |
|
119 data = JSON.parse(string); |
|
120 // Simplify the rest of the code by ensuring that we can simply |
|
121 // concatenate the result to a message. |
|
122 if (data && typeof data == "object") { |
|
123 data.toString = function() { |
|
124 return string; |
|
125 }; |
|
126 } |
|
127 return data; |
|
128 } catch (ex) { |
|
129 if (string) { |
|
130 return string; |
|
131 } |
|
132 try { |
|
133 return "Error getting state: " + ex + " at " + ex.stack; |
|
134 } catch (ex2) { |
|
135 return "Error getting state but could not display error"; |
|
136 } |
|
137 } |
|
138 } |
|
139 |
|
140 /** |
|
141 * Countdown for a given duration, skipping beats if the computer is too busy, |
|
142 * sleeping or otherwise unavailable. |
|
143 * |
|
144 * @param {number} delay An approximate delay to wait in milliseconds (rounded |
|
145 * up to the closest second). |
|
146 * |
|
147 * @return Deferred |
|
148 */ |
|
149 function looseTimer(delay) { |
|
150 let DELAY_BEAT = 1000; |
|
151 let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); |
|
152 let beats = Math.ceil(delay / DELAY_BEAT); |
|
153 let deferred = Promise.defer(); |
|
154 timer.initWithCallback(function() { |
|
155 if (beats <= 0) { |
|
156 deferred.resolve(); |
|
157 } |
|
158 --beats; |
|
159 }, DELAY_BEAT, Ci.nsITimer.TYPE_REPEATING_PRECISE_CAN_SKIP); |
|
160 // Ensure that the timer is both canceled once we are done with it |
|
161 // and not garbage-collected until then. |
|
162 deferred.promise.then(() => timer.cancel(), () => timer.cancel()); |
|
163 return deferred; |
|
164 } |
|
165 |
|
166 this.EXPORTED_SYMBOLS = ["AsyncShutdown"]; |
|
167 |
|
168 /** |
|
169 * {string} topic -> phase |
|
170 */ |
|
171 let gPhases = new Map(); |
|
172 |
|
173 this.AsyncShutdown = { |
|
174 /** |
|
175 * Access function getPhase. For testing purposes only. |
|
176 */ |
|
177 get _getPhase() { |
|
178 let accepted = false; |
|
179 try { |
|
180 accepted = Services.prefs.getBoolPref("toolkit.asyncshutdown.testing"); |
|
181 } catch (ex) { |
|
182 // Ignore errors |
|
183 } |
|
184 if (accepted) { |
|
185 return getPhase; |
|
186 } |
|
187 return undefined; |
|
188 } |
|
189 }; |
|
190 |
|
191 /** |
|
192 * Register a new phase. |
|
193 * |
|
194 * @param {string} topic The notification topic for this Phase. |
|
195 * @see {https://developer.mozilla.org/en-US/docs/Observer_Notifications} |
|
196 */ |
|
197 function getPhase(topic) { |
|
198 let phase = gPhases.get(topic); |
|
199 if (phase) { |
|
200 return phase; |
|
201 } |
|
202 let spinner = new Spinner(topic); |
|
203 phase = Object.freeze({ |
|
204 /** |
|
205 * Register a blocker for the completion of a phase. |
|
206 * |
|
207 * @param {string} name The human-readable name of the blocker. Used |
|
208 * for debugging/error reporting. Please make sure that the name |
|
209 * respects the following model: "Some Service: some action in progress" - |
|
210 * for instance "OS.File: flushing all pending I/O"; |
|
211 * @param {function|promise|*} condition A condition blocking the |
|
212 * completion of the phase. Generally, this is a function |
|
213 * returning a promise. This function is evaluated during the |
|
214 * phase and the phase is guaranteed to not terminate until the |
|
215 * resulting promise is either resolved or rejected. If |
|
216 * |condition| is not a function but another value |v|, it behaves |
|
217 * as if it were a function returning |v|. |
|
218 * @param {function*} state Optionally, a function returning |
|
219 * information about the current state of the blocker as an |
|
220 * object. Used for providing more details when logging errors or |
|
221 * crashing. |
|
222 * |
|
223 * Examples: |
|
224 * AsyncShutdown.profileBeforeChange.addBlocker("Module: just a promise", |
|
225 * promise); // profileBeforeChange will not complete until |
|
226 * // promise is resolved or rejected |
|
227 * |
|
228 * AsyncShutdown.profileBeforeChange.addBlocker("Module: a callback", |
|
229 * function callback() { |
|
230 * // ... |
|
231 * // Execute this code during profileBeforeChange |
|
232 * return promise; |
|
233 * // profileBeforeChange will not complete until promise |
|
234 * // is resolved or rejected |
|
235 * }); |
|
236 * |
|
237 * AsyncShutdown.profileBeforeChange.addBlocker("Module: trivial callback", |
|
238 * function callback() { |
|
239 * // ... |
|
240 * // Execute this code during profileBeforeChange |
|
241 * // No specific guarantee about completion of profileBeforeChange |
|
242 * }); |
|
243 * |
|
244 */ |
|
245 addBlocker: function(name, condition, state = null) { |
|
246 if (typeof name != "string") { |
|
247 throw new TypeError("Expected a human-readable name as first argument"); |
|
248 } |
|
249 if (state && typeof state != "function") { |
|
250 throw new TypeError("Expected nothing or a function as third argument"); |
|
251 } |
|
252 spinner.addBlocker({name: name, condition: condition, state: state}); |
|
253 } |
|
254 }); |
|
255 gPhases.set(topic, phase); |
|
256 return phase; |
|
257 } |
|
258 |
|
259 /** |
|
260 * Utility class used to spin the event loop until all blockers for a |
|
261 * Phase are satisfied. |
|
262 * |
|
263 * @param {string} topic The xpcom notification for that phase. |
|
264 */ |
|
265 function Spinner(topic) { |
|
266 this._topic = topic; |
|
267 this._conditions = new Set(); // set to |null| once it is too late to register |
|
268 Services.obs.addObserver(this, topic, false); |
|
269 } |
|
270 |
|
271 Spinner.prototype = { |
|
272 /** |
|
273 * Register a new condition for this phase. |
|
274 * |
|
275 * @param {object} condition A Condition that must be fulfilled before |
|
276 * we complete this Phase. |
|
277 * Must contain fields: |
|
278 * - {string} name The human-readable name of the condition. Used |
|
279 * for debugging/error reporting. |
|
280 * - {function} action An action that needs to be completed |
|
281 * before we proceed to the next runstate. If |action| returns a promise, |
|
282 * we wait until the promise is resolved/rejected before proceeding |
|
283 * to the next runstate. |
|
284 */ |
|
285 addBlocker: function(condition) { |
|
286 if (!this._conditions) { |
|
287 throw new Error("Phase " + this._topic + |
|
288 " has already begun, it is too late to register" + |
|
289 " completion condition '" + condition.name + "'."); |
|
290 } |
|
291 this._conditions.add(condition); |
|
292 }, |
|
293 |
|
294 observe: function() { |
|
295 let topic = this._topic; |
|
296 Services.obs.removeObserver(this, topic); |
|
297 |
|
298 let conditions = this._conditions; |
|
299 this._conditions = null; // Too late to register |
|
300 |
|
301 if (conditions.size == 0) { |
|
302 // No need to spin anything |
|
303 return; |
|
304 } |
|
305 |
|
306 // The promises for which we are waiting. |
|
307 let allPromises = []; |
|
308 |
|
309 // Information to determine and report to the user which conditions |
|
310 // are not satisfied yet. |
|
311 let allMonitors = []; |
|
312 |
|
313 for (let {condition, name, state} of conditions) { |
|
314 // Gather all completion conditions |
|
315 |
|
316 try { |
|
317 if (typeof condition == "function") { |
|
318 // Normalize |condition| to the result of the function. |
|
319 try { |
|
320 condition = condition(topic); |
|
321 } catch (ex) { |
|
322 condition = Promise.reject(ex); |
|
323 } |
|
324 } |
|
325 // Normalize to a promise. Of course, if |condition| was not a |
|
326 // promise in the first place (in particular if the above |
|
327 // function returned |undefined| or failed), that new promise |
|
328 // isn't going to be terribly interesting, but it will behave |
|
329 // as a promise. |
|
330 condition = Promise.resolve(condition); |
|
331 |
|
332 // If the promise takes too long to be resolved/rejected, |
|
333 // we need to notify the user. |
|
334 // |
|
335 // If it takes way too long, we need to crash. |
|
336 |
|
337 let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); |
|
338 timer.initWithCallback(function() { |
|
339 let msg = "A phase completion condition is" + |
|
340 " taking too long to complete." + |
|
341 " Condition: " + monitor.name + |
|
342 " Phase: " + topic + |
|
343 " State: " + safeGetState(state); |
|
344 warn(msg); |
|
345 }, DELAY_WARNING_MS, Ci.nsITimer.TYPE_ONE_SHOT); |
|
346 |
|
347 let monitor = { |
|
348 isFrozen: true, |
|
349 name: name, |
|
350 state: state |
|
351 }; |
|
352 condition = condition.then(function onSuccess() { |
|
353 timer.cancel(); // As a side-effect, this prevents |timer| from |
|
354 // being garbage-collected too early. |
|
355 monitor.isFrozen = false; |
|
356 }, function onError(error) { |
|
357 timer.cancel(); |
|
358 let msg = "A completion condition encountered an error" + |
|
359 " while we were spinning the event loop." + |
|
360 " Condition: " + name + |
|
361 " Phase: " + topic + |
|
362 " State: " + safeGetState(state); |
|
363 warn(msg, error); |
|
364 monitor.isFrozen = false; |
|
365 }); |
|
366 allMonitors.push(monitor); |
|
367 allPromises.push(condition); |
|
368 |
|
369 } catch (error) { |
|
370 let msg = "A completion condition encountered an error" + |
|
371 " while we were initializing the phase." + |
|
372 " Condition: " + name + |
|
373 " Phase: " + topic + |
|
374 " State: " + safeGetState(state); |
|
375 warn(msg, error); |
|
376 } |
|
377 |
|
378 } |
|
379 conditions = null; |
|
380 |
|
381 let promise = Promise.all(allPromises); |
|
382 allPromises = null; |
|
383 |
|
384 promise = promise.then(null, function onError(error) { |
|
385 // I don't think that this can happen. |
|
386 // However, let's be overcautious with async/shutdown error reporting. |
|
387 let msg = "An uncaught error appeared while completing the phase." + |
|
388 " Phase: " + topic; |
|
389 warn(msg, error); |
|
390 }); |
|
391 |
|
392 let satisfied = false; // |true| once we have satisfied all conditions |
|
393 |
|
394 // If after DELAY_CRASH_MS (approximately one minute, adjusted to take |
|
395 // into account sleep and otherwise busy computer) we have not finished |
|
396 // this shutdown phase, we assume that the shutdown is somehow frozen, |
|
397 // presumably deadlocked. At this stage, the only thing we can do to |
|
398 // avoid leaving the user's computer in an unstable (and battery-sucking) |
|
399 // situation is report the issue and crash. |
|
400 let timeToCrash = looseTimer(DELAY_CRASH_MS); |
|
401 timeToCrash.promise.then( |
|
402 function onTimeout() { |
|
403 // Report the problem as best as we can, then crash. |
|
404 let frozen = []; |
|
405 let states = []; |
|
406 for (let {name, isFrozen, state} of allMonitors) { |
|
407 if (isFrozen) { |
|
408 frozen.push({name: name, state: safeGetState(state)}); |
|
409 } |
|
410 } |
|
411 |
|
412 let msg = "At least one completion condition failed to complete" + |
|
413 " within a reasonable amount of time. Causing a crash to" + |
|
414 " ensure that we do not leave the user with an unresponsive" + |
|
415 " process draining resources." + |
|
416 " Conditions: " + JSON.stringify(frozen) + |
|
417 " Phase: " + topic; |
|
418 err(msg); |
|
419 if (gCrashReporter && gCrashReporter.enabled) { |
|
420 let data = { |
|
421 phase: topic, |
|
422 conditions: frozen |
|
423 }; |
|
424 gCrashReporter.annotateCrashReport("AsyncShutdownTimeout", |
|
425 JSON.stringify(data)); |
|
426 } else { |
|
427 warn("No crash reporter available"); |
|
428 } |
|
429 |
|
430 let error = new Error(); |
|
431 gDebug.abort(error.fileName, error.lineNumber + 1); |
|
432 }, |
|
433 function onSatisfied() { |
|
434 // The promise has been rejected, which means that we have satisfied |
|
435 // all completion conditions. |
|
436 }); |
|
437 |
|
438 promise = promise.then(function() { |
|
439 satisfied = true; |
|
440 timeToCrash.reject(); |
|
441 }/* No error is possible here*/); |
|
442 |
|
443 // Now, spin the event loop |
|
444 let thread = Services.tm.mainThread; |
|
445 while(!satisfied) { |
|
446 thread.processNextEvent(true); |
|
447 } |
|
448 } |
|
449 }; |
|
450 |
|
451 |
|
452 // List of well-known runstates |
|
453 // Ideally, runstates should be registered from the component that decides |
|
454 // when they start/stop. For compatibility with existing startup/shutdown |
|
455 // mechanisms, we register a few runstates here. |
|
456 |
|
457 this.AsyncShutdown.profileChangeTeardown = getPhase("profile-change-teardown"); |
|
458 this.AsyncShutdown.profileBeforeChange = getPhase("profile-before-change"); |
|
459 this.AsyncShutdown.sendTelemetry = getPhase("profile-before-change2"); |
|
460 this.AsyncShutdown.webWorkersShutdown = getPhase("web-workers-shutdown"); |
|
461 Object.freeze(this.AsyncShutdown); |