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
|
// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// 'use strict'; TODO(vadimt): Uncomment once crbug.com/237617 is fixed.
// TODO(vadimt): Remove alerts.
/**
* @fileoverview Utility objects and functions for Google Now extension.
*/
/**
* Checks for internal errors.
* @param {boolean} condition Condition that must be true.
* @param {string} message Diagnostic message for the case when the condition is
* false.
*/
function verify(condition, message) {
if (!condition)
throw new Error('ASSERT: ' + message);
}
/**
* Builds the object to manage tasks (mutually exclusive chains of events).
* @param {function(string, string): boolean} areConflicting Function that
* checks if a new task can't be added to a task queue that contains an
* existing task.
* @return {Object} Task manager interface.
*/
function buildTaskManager(areConflicting) {
/**
* Name of the alarm that triggers the error saying that the event page cannot
* unload.
*/
var CANNOT_UNLOAD_ALARM_NAME = 'CANNOT-UNLOAD';
/**
* Maximal time we expect the event page to stay loaded after starting a task.
*/
var MAXIMUM_LOADED_TIME_MINUTES = 5;
/**
* Queue of scheduled tasks. The first element, if present, corresponds to the
* currently running task.
* @type {Array.<Object.<string, function(function())>>}
*/
var queue = [];
/**
* Name of the current step of the currently running task if present,
* otherwise, null. For diagnostics only.
* It's set when the task is started and before each asynchronous operation.
*/
var stepName = null;
/**
* Starts the first queued task.
*/
function startFirst() {
verify(queue.length >= 1, 'startFirst: queue is empty');
// Set alarm to verify that the event page will unload in a reasonable time.
chrome.alarms.create(CANNOT_UNLOAD_ALARM_NAME,
{delayInMinutes: MAXIMUM_LOADED_TIME_MINUTES});
// Start the oldest queued task, but don't remove it from the queue.
verify(
stepName == null,
'tasks.startFirst: stepName is not null: ' + stepName +
', queue = ' + JSON.stringify(queue));
var entry = queue[0];
stepName = entry.name + '-initial';
console.log('Starting task ' + entry.name);
entry.task(finish);
}
/**
* Checks if a new task can be added to the task queue.
* @param {string} taskName Name of the new task.
* @return {boolean} Whether the new task can be added.
*/
function canQueue(taskName) {
for (var i = 0; i < queue.length; ++i) {
if (areConflicting(taskName, queue[i].name)) {
console.log('Conflict: new=' + taskName +
', scheduled=' + queue[i].name);
return false;
}
}
return true;
}
/**
* Adds a new task. If another task is not running, runs the task immediately.
* If any task in the queue is not compatible with the task, ignores the new
* task. Otherwise, stores the task for future execution.
* @param {string} taskName Name of the task.
* @param {function(function())} task Function to run. Takes a callback
* parameter.
*/
function add(taskName, task) {
console.log('Adding task ' + taskName);
if (!canQueue(taskName))
return;
queue.push({name: taskName, task: task});
if (queue.length == 1) {
startFirst();
}
}
/**
* Completes the current task and starts the next queued task if available.
*/
function finish() {
verify(queue.length >= 1,
'tasks.finish: The task queue is empty; step = ' + stepName);
console.log('Finishing task ' + queue[0].name);
queue.shift();
stepName = null;
if (queue.length >= 1)
startFirst();
}
/**
* Associates a name with the current step of the task. Used for diagnostics
* only. A task is a chain of asynchronous events; debugSetStepName should be
* called before starting any asynchronous operation.
* @param {string} step Name of new step.
*/
function debugSetStepName(step) {
stepName = step;
}
// Limiting 1 alert per background page load.
var alertShown = false;
/**
* Adds error processing to an API callback.
* @param {Function} callback Callback to instrument.
* @return {Function} Instrumented callback.
*/
function wrapCallback(callback) {
return function() {
// This is the wrapper for the callback.
try {
return callback.apply(null, arguments);
} catch (error) {
var message = 'Uncaught exception:\n' + error.stack;
console.error(message);
if (!alertShown) {
alertShown = true;
alert(message);
}
}
};
}
/**
* Instruments an API function to add error processing to its user
* code-provided callback.
* @param {Object} namespace Namespace of the API function.
* @param {string} functionName Name of the API function.
* @param {number} callbackParameter Index of the callback parameter to this
* API function.
*/
function instrumentApiFunction(namespace, functionName, callbackParameter) {
var originalFunction = namespace[functionName];
if (!originalFunction)
alert('Cannot instrument ' + functionName);
namespace[functionName] = function() {
// This is the wrapper for the API function. Pass the wrapped callback to
// the original function.
var callback = arguments[callbackParameter];
if (typeof callback != 'function') {
alert('Argument ' + callbackParameter + ' of ' + functionName +
' is not a function');
}
arguments[callbackParameter] = wrapCallback(callback);
return originalFunction.apply(namespace, arguments);
};
}
instrumentApiFunction(chrome.alarms.onAlarm, 'addListener', 0);
instrumentApiFunction(chrome.runtime.onSuspend, 'addListener', 0);
chrome.alarms.onAlarm.addListener(function(alarm) {
if (alarm.name == CANNOT_UNLOAD_ALARM_NAME) {
// Error if the event page wasn't unloaded after a reasonable timeout
// since starting the last task.
verify(false, 'Event page didn\'t unload, queue=' +
JSON.stringify(queue) + ', step=' + stepName +
' (ignore this assert if devtools is open).');
}
});
chrome.runtime.onSuspend.addListener(function() {
chrome.alarms.clear(CANNOT_UNLOAD_ALARM_NAME);
verify(
queue.length == 0,
'Incomplete task when unloading event page, queue = ' +
JSON.stringify(queue) + ', step = ' + stepName);
verify(
stepName == null,
'Step name not null when unloading event page, queue = ' +
JSON.stringify(queue) + ', step = ' + stepName);
});
return {
add: add,
// TODO(vadimt): Replace with instrumenting callbacks.
debugSetStepName: debugSetStepName,
instrumentApiFunction: instrumentApiFunction,
wrapCallback: wrapCallback
};
}
var storage = chrome.storage.local;
/**
* Builds an object to manage retrying activities with exponential backoff.
* @param {string} name Name of this attempt manager.
* @param {function()} attempt Activity that the manager retries until it
* calls 'stop' method.
* @param {number} initialDelaySeconds Default first delay until first retry.
* @param {number} maximumDelaySeconds Maximum delay between retries.
* @return {Object} Attempt manager interface.
*/
function buildAttemptManager(
name, attempt, initialDelaySeconds, maximumDelaySeconds) {
var alarmName = name + '-scheduler';
var currentDelayStorageKey = name + '-current-delay';
/**
* Creates an alarm for the next attempt. The alarm is repeating for the case
* when the next attempt crashes before registering next alarm.
* @param {number} delaySeconds Delay until next retry.
*/
function createAlarm(delaySeconds) {
var alarmInfo = {
delayInMinutes: delaySeconds / 60,
periodInMinutes: maximumDelaySeconds / 60
};
chrome.alarms.create(alarmName, alarmInfo);
}
/**
* Schedules next attempt.
* @param {number=} opt_previousDelaySeconds Previous delay in a sequence of
* retry attempts, if specified. Not specified for scheduling first retry
* in the exponential sequence.
*/
function scheduleNextAttempt(opt_previousDelaySeconds) {
var base = opt_previousDelaySeconds ? opt_previousDelaySeconds * 2 :
initialDelaySeconds;
var newRetryDelaySeconds =
Math.min(base * (1 + 0.2 * Math.random()), maximumDelaySeconds);
createAlarm(newRetryDelaySeconds);
var items = {};
items[currentDelayStorageKey] = newRetryDelaySeconds;
storage.set(items);
}
/**
* Starts repeated attempts.
* @param {number=} opt_firstDelaySeconds Time until the first attempt, if
* specified. Otherwise, initialDelaySeconds will be used for the first
* attempt.
*/
function start(opt_firstDelaySeconds) {
if (opt_firstDelaySeconds) {
createAlarm(opt_firstDelaySeconds);
storage.remove(currentDelayStorageKey);
} else {
scheduleNextAttempt();
}
}
/**
* Stops repeated attempts.
*/
function stop() {
chrome.alarms.clear(alarmName);
storage.remove(currentDelayStorageKey);
}
/**
* Plans for the next attempt.
* @param {function()} callback Completion callback. It will be invoked after
* the planning is done.
*/
function planForNext(callback) {
tasks.debugSetStepName('planForNext-get-storage');
storage.get(currentDelayStorageKey, function(items) {
console.log('planForNext-get-storage ' + JSON.stringify(items));
scheduleNextAttempt(items[currentDelayStorageKey]);
callback();
});
}
chrome.alarms.onAlarm.addListener(function(alarm) {
if (alarm.name == alarmName)
attempt();
});
return {
start: start,
planForNext: planForNext,
stop: stop
};
}
|