utility.js revision 558790d6acca3451cf3a6b497803a5f07d0bec58
1// Copyright (c) 2013 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5'use strict'; 6 7// TODO(vadimt): Remove alerts. 8 9/** 10 * @fileoverview Utility objects and functions for Google Now extension. 11 */ 12 13// TODO(vadimt): Figure out the server name. Use it in the manifest and for 14// NOTIFICATION_CARDS_URL. Meanwhile, to use the feature, you need to manually 15// set the server name via local storage. 16/** 17 * Notification server URL. 18 */ 19var NOTIFICATION_CARDS_URL = localStorage['server_url']; 20 21/** 22 * Checks for internal errors. 23 * @param {boolean} condition Condition that must be true. 24 * @param {string} message Diagnostic message for the case when the condition is 25 * false. 26 */ 27function verify(condition, message) { 28 if (!condition) 29 throw new Error('\nASSERT: ' + message); 30} 31 32/** 33 * Builds a request to the notification server. 34 * @param {string} handlerName Server handler to send the request to. 35 * @param {string} contentType Value for the Content-type header. 36 * @return {XMLHttpRequest} Server request. 37 */ 38function buildServerRequest(handlerName, contentType) { 39 var request = new XMLHttpRequest(); 40 41 request.responseType = 'text'; 42 request.open('POST', NOTIFICATION_CARDS_URL + '/' + handlerName, true); 43 request.setRequestHeader('Content-type', contentType); 44 45 return request; 46} 47 48/** 49 * Builds the object to manage tasks (mutually exclusive chains of events). 50 * @param {function(string, string): boolean} areConflicting Function that 51 * checks if a new task can't be added to a task queue that contains an 52 * existing task. 53 * @return {Object} Task manager interface. 54 */ 55function buildTaskManager(areConflicting) { 56 /** 57 * Queue of scheduled tasks. The first element, if present, corresponds to the 58 * currently running task. 59 * @type {Array.<Object.<string, function(function())>>} 60 */ 61 var queue = []; 62 63 /** 64 * Count of unfinished callbacks of the current task. 65 * @type {number} 66 */ 67 var taskPendingCallbackCount = 0; 68 69 /** 70 * Required callbacks that are not yet called. Includes both task and non-task 71 * callbacks. This is a map from unique callback id to the stack at the moment 72 * when the callback was wrapped. This stack identifies the callback. 73 * Used only for diagnostics. 74 * @type {Object.<number, string>} 75 */ 76 var pendingCallbacks = {}; 77 78 /** 79 * True if currently executed code is a part of a task. 80 * @type {boolean} 81 */ 82 var isInTask = false; 83 84 /** 85 * Starts the first queued task. 86 */ 87 function startFirst() { 88 verify(queue.length >= 1, 'startFirst: queue is empty'); 89 verify(!isInTask, 'startFirst: already in task'); 90 isInTask = true; 91 92 // Start the oldest queued task, but don't remove it from the queue. 93 verify( 94 taskPendingCallbackCount == 0, 95 'tasks.startFirst: still have pending task callbacks: ' + 96 taskPendingCallbackCount + 97 ', queue = ' + JSON.stringify(queue) + 98 ', pendingCallbacks = ' + JSON.stringify(pendingCallbacks)); 99 var entry = queue[0]; 100 console.log('Starting task ' + entry.name); 101 102 entry.task(function() {}); // TODO(vadimt): Don't pass parameter. 103 104 verify(isInTask, 'startFirst: not in task at exit'); 105 isInTask = false; 106 if (taskPendingCallbackCount == 0) 107 finish(); 108 } 109 110 /** 111 * Checks if a new task can be added to the task queue. 112 * @param {string} taskName Name of the new task. 113 * @return {boolean} Whether the new task can be added. 114 */ 115 function canQueue(taskName) { 116 for (var i = 0; i < queue.length; ++i) { 117 if (areConflicting(taskName, queue[i].name)) { 118 console.log('Conflict: new=' + taskName + 119 ', scheduled=' + queue[i].name); 120 return false; 121 } 122 } 123 124 return true; 125 } 126 127 /** 128 * Adds a new task. If another task is not running, runs the task immediately. 129 * If any task in the queue is not compatible with the task, ignores the new 130 * task. Otherwise, stores the task for future execution. 131 * @param {string} taskName Name of the task. 132 * @param {function(function())} task Function to run. Takes a callback 133 * parameter. 134 */ 135 function add(taskName, task) { 136 console.log('Adding task ' + taskName); 137 if (!canQueue(taskName)) 138 return; 139 140 queue.push({name: taskName, task: task}); 141 142 if (queue.length == 1) { 143 startFirst(); 144 } 145 } 146 147 /** 148 * Completes the current task and starts the next queued task if available. 149 */ 150 function finish() { 151 verify(queue.length >= 1, 152 'tasks.finish: The task queue is empty'); 153 console.log('Finishing task ' + queue[0].name); 154 queue.shift(); 155 156 if (queue.length >= 1) 157 startFirst(); 158 } 159 160 // Limiting 1 error report per background page load. 161 var errorReported = false; 162 163 /** 164 * Sends an error report to the server. 165 * @param {Error} error Error to report. 166 */ 167 function sendErrorReport(error) { 168 var filteredStack = error.stack.replace(/.*\n/, '\n'); 169 var file; 170 var line; 171 var topFrameMatches = filteredStack.match(/\(.*\)/); 172 // topFrameMatches's example: 173 // (chrome-extension://pmofbkohncoogjjhahejjfbppikbjigm/utility.js:308:19) 174 var crashLocation = topFrameMatches && topFrameMatches[0]; 175 if (crashLocation) { 176 var topFrameElements = 177 crashLocation.substring(1, crashLocation.length - 1).split(':'); 178 // topFrameElements for the above example will look like: 179 // [0] chrome-extension 180 // [1] //pmofbkohncoogjjhahejjfbppikbjigm/utility.js 181 // [2] 308 182 // [3] 19 183 if (topFrameElements.length >= 3) { 184 file = topFrameElements[0] + ':' + topFrameElements[1]; 185 line = topFrameElements[2]; 186 } 187 } 188 var requestParameters = 189 'error=' + encodeURIComponent(error.name) + 190 '&script=' + encodeURIComponent(file) + 191 '&line=' + encodeURIComponent(line) + 192 '&trace=' + encodeURIComponent(filteredStack); 193 var request = buildServerRequest('jserror', 194 'application/x-www-form-urlencoded'); 195 request.onloadend = function(event) { 196 console.log('sendErrorReport status: ' + request.status); 197 }; 198 request.send(requestParameters); 199 } 200 201 /** 202 * Unique ID of the next callback. 203 * @type {number} 204 */ 205 var nextCallbackId = 0; 206 207 /** 208 * Adds error processing to an API callback. 209 * @param {Function} callback Callback to instrument. 210 * @param {boolean=} opt_dontRequire True if the callback is not required to 211 * be invoked. 212 * @return {Function} Instrumented callback. 213 */ 214 function wrapCallback(callback, opt_dontRequire) { 215 verify(!(opt_dontRequire && isInTask), 'Unrequired callback in a task.'); 216 var callbackId = nextCallbackId++; 217 var isTaskCallback = isInTask; 218 if (isTaskCallback) 219 ++taskPendingCallbackCount; 220 if (!opt_dontRequire) 221 pendingCallbacks[callbackId] = new Error().stack; 222 223 return function() { 224 // This is the wrapper for the callback. 225 try { 226 if (isTaskCallback) { 227 verify(!isInTask, 'wrapCallback: already in task'); 228 isInTask = true; 229 } 230 if (!opt_dontRequire) 231 delete pendingCallbacks[callbackId]; 232 233 // Call the original callback. 234 callback.apply(null, arguments); 235 236 if (isTaskCallback) { 237 verify(isInTask, 'wrapCallback: not in task at exit'); 238 isInTask = false; 239 if (--taskPendingCallbackCount == 0) 240 finish(); 241 } 242 } catch (error) { 243 var message = 'Uncaught exception:\n' + error.stack; 244 console.error(message); 245 if (!errorReported) { 246 errorReported = true; 247 chrome.metricsPrivate.getIsCrashReportingEnabled(function(isEnabled) { 248 if (isEnabled) 249 sendErrorReport(error); 250 }); 251 alert(message); 252 } 253 } 254 }; 255 } 256 257 /** 258 * Instruments an API function to add error processing to its user 259 * code-provided callback. 260 * @param {Object} namespace Namespace of the API function. 261 * @param {string} functionName Name of the API function. 262 * @param {number} callbackParameter Index of the callback parameter to this 263 * API function. 264 */ 265 function instrumentApiFunction(namespace, functionName, callbackParameter) { 266 var originalFunction = namespace[functionName]; 267 268 if (!originalFunction) 269 alert('Cannot instrument ' + functionName); 270 271 namespace[functionName] = function() { 272 // This is the wrapper for the API function. Pass the wrapped callback to 273 // the original function. 274 var callback = arguments[callbackParameter]; 275 if (typeof callback != 'function') { 276 alert('Argument ' + callbackParameter + ' of ' + functionName + 277 ' is not a function'); 278 } 279 arguments[callbackParameter] = wrapCallback( 280 callback, functionName == 'addListener'); 281 return originalFunction.apply(namespace, arguments); 282 }; 283 } 284 285 instrumentApiFunction(chrome.alarms.onAlarm, 'addListener', 0); 286 instrumentApiFunction(chrome.runtime.onSuspend, 'addListener', 0); 287 288 chrome.runtime.onSuspend.addListener(function() { 289 var stringifiedPendingCallbacks = JSON.stringify(pendingCallbacks); 290 verify( 291 queue.length == 0 && stringifiedPendingCallbacks == '{}', 292 'Incomplete task or pending callbacks when unloading event page,' + 293 ' queue = ' + JSON.stringify(queue) + 294 ', pendingCallbacks = ' + stringifiedPendingCallbacks); 295 }); 296 297 return { 298 add: add, 299 debugSetStepName: function() {}, // TODO(vadimt): remove 300 instrumentApiFunction: instrumentApiFunction, 301 wrapCallback: wrapCallback 302 }; 303} 304 305var storage = chrome.storage.local; 306 307/** 308 * Builds an object to manage retrying activities with exponential backoff. 309 * @param {string} name Name of this attempt manager. 310 * @param {function()} attempt Activity that the manager retries until it 311 * calls 'stop' method. 312 * @param {number} initialDelaySeconds Default first delay until first retry. 313 * @param {number} maximumDelaySeconds Maximum delay between retries. 314 * @return {Object} Attempt manager interface. 315 */ 316function buildAttemptManager( 317 name, attempt, initialDelaySeconds, maximumDelaySeconds) { 318 var alarmName = 'attempt-scheduler-' + name; 319 var currentDelayStorageKey = 'current-delay-' + name; 320 321 /** 322 * Creates an alarm for the next attempt. The alarm is repeating for the case 323 * when the next attempt crashes before registering next alarm. 324 * @param {number} delaySeconds Delay until next retry. 325 */ 326 function createAlarm(delaySeconds) { 327 var alarmInfo = { 328 delayInMinutes: delaySeconds / 60, 329 periodInMinutes: maximumDelaySeconds / 60 330 }; 331 chrome.alarms.create(alarmName, alarmInfo); 332 } 333 334 /** 335 * Schedules next attempt. 336 * @param {number=} opt_previousDelaySeconds Previous delay in a sequence of 337 * retry attempts, if specified. Not specified for scheduling first retry 338 * in the exponential sequence. 339 */ 340 function scheduleNextAttempt(opt_previousDelaySeconds) { 341 var base = opt_previousDelaySeconds ? opt_previousDelaySeconds * 2 : 342 initialDelaySeconds; 343 var newRetryDelaySeconds = 344 Math.min(base * (1 + 0.2 * Math.random()), maximumDelaySeconds); 345 346 createAlarm(newRetryDelaySeconds); 347 348 var items = {}; 349 items[currentDelayStorageKey] = newRetryDelaySeconds; 350 storage.set(items); 351 } 352 353 /** 354 * Starts repeated attempts. 355 * @param {number=} opt_firstDelaySeconds Time until the first attempt, if 356 * specified. Otherwise, initialDelaySeconds will be used for the first 357 * attempt. 358 */ 359 function start(opt_firstDelaySeconds) { 360 if (opt_firstDelaySeconds) { 361 createAlarm(opt_firstDelaySeconds); 362 storage.remove(currentDelayStorageKey); 363 } else { 364 scheduleNextAttempt(); 365 } 366 } 367 368 /** 369 * Stops repeated attempts. 370 */ 371 function stop() { 372 chrome.alarms.clear(alarmName); 373 storage.remove(currentDelayStorageKey); 374 } 375 376 /** 377 * Plans for the next attempt. 378 * @param {function()} callback Completion callback. It will be invoked after 379 * the planning is done. 380 */ 381 function planForNext(callback) { 382 storage.get(currentDelayStorageKey, function(items) { 383 console.log('planForNext-get-storage ' + JSON.stringify(items)); 384 scheduleNextAttempt(items[currentDelayStorageKey]); 385 callback(); 386 }); 387 } 388 389 chrome.alarms.onAlarm.addListener(function(alarm) { 390 if (alarm.name == alarmName) 391 attempt(); 392 }); 393 394 return { 395 start: start, 396 planForNext: planForNext, 397 stop: stop 398 }; 399} 400