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