From b65231c2ed399c88a1c219028a9eb52b6575e732 Mon Sep 17 00:00:00 2001 From: "vadimt@chromium.org" Date: Fri, 23 Aug 2013 22:20:24 +0000 Subject: [PATCH] Cleaning up task manager. Task manager has grown to ~350 lines, and contains a mix of code dealing with tasks and with instrumenting callbacks. This change detangles instrumentation (wrapper) from task management (buildTaskManager). Wrapper has an extensibility mechanism that Task Manager uses to inject its own code (and state) into instrumented callbacks. BUG=164227 TEST=No Review URL: https://chromiumcodereview.appspot.com/22986007 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@219357 0039d316-1c4b-4281-b951-d872f2087c98 --- chrome/browser/resources/google_now/background.js | 38 +- .../resources/google_now/background_test_util.js | 8 +- chrome/browser/resources/google_now/utility.js | 552 +++++++++++++-------- 3 files changed, 357 insertions(+), 241 deletions(-) diff --git a/chrome/browser/resources/google_now/background.js b/chrome/browser/resources/google_now/background.js index 399ed960aeb7..ba36c2a3cba2 100644 --- a/chrome/browser/resources/google_now/background.js +++ b/chrome/browser/resources/google_now/background.js @@ -116,29 +116,29 @@ function areTasksConflicting(newTaskName, scheduledTaskName) { var tasks = buildTaskManager(areTasksConflicting); // Add error processing to API calls. -tasks.instrumentChromeApiFunction('location.onLocationUpdate.addListener', 0); -tasks.instrumentChromeApiFunction('metricsPrivate.getVariationParams', 1); -tasks.instrumentChromeApiFunction('notifications.create', 2); -tasks.instrumentChromeApiFunction('notifications.update', 2); -tasks.instrumentChromeApiFunction('notifications.getAll', 0); -tasks.instrumentChromeApiFunction( +wrapper.instrumentChromeApiFunction('location.onLocationUpdate.addListener', 0); +wrapper.instrumentChromeApiFunction('metricsPrivate.getVariationParams', 1); +wrapper.instrumentChromeApiFunction('notifications.create', 2); +wrapper.instrumentChromeApiFunction('notifications.update', 2); +wrapper.instrumentChromeApiFunction('notifications.getAll', 0); +wrapper.instrumentChromeApiFunction( 'notifications.onButtonClicked.addListener', 0); -tasks.instrumentChromeApiFunction('notifications.onClicked.addListener', 0); -tasks.instrumentChromeApiFunction('notifications.onClosed.addListener', 0); -tasks.instrumentChromeApiFunction('omnibox.onInputEntered.addListener', 0); -tasks.instrumentChromeApiFunction( +wrapper.instrumentChromeApiFunction('notifications.onClicked.addListener', 0); +wrapper.instrumentChromeApiFunction('notifications.onClosed.addListener', 0); +wrapper.instrumentChromeApiFunction('omnibox.onInputEntered.addListener', 0); +wrapper.instrumentChromeApiFunction( 'preferencesPrivate.googleGeolocationAccessEnabled.get', 1); -tasks.instrumentChromeApiFunction( +wrapper.instrumentChromeApiFunction( 'preferencesPrivate.googleGeolocationAccessEnabled.onChange.addListener', 0); -tasks.instrumentChromeApiFunction('permissions.contains', 1); -tasks.instrumentChromeApiFunction('permissions.remove', 1); -tasks.instrumentChromeApiFunction('permissions.request', 1); -tasks.instrumentChromeApiFunction('runtime.onInstalled.addListener', 0); -tasks.instrumentChromeApiFunction('runtime.onStartup.addListener', 0); -tasks.instrumentChromeApiFunction('tabs.create', 1); -tasks.instrumentChromeApiFunction('storage.local.get', 1); +wrapper.instrumentChromeApiFunction('permissions.contains', 1); +wrapper.instrumentChromeApiFunction('permissions.remove', 1); +wrapper.instrumentChromeApiFunction('permissions.request', 1); +wrapper.instrumentChromeApiFunction('runtime.onInstalled.addListener', 0); +wrapper.instrumentChromeApiFunction('runtime.onStartup.addListener', 0); +wrapper.instrumentChromeApiFunction('tabs.create', 1); +wrapper.instrumentChromeApiFunction('storage.local.get', 1); var updateCardsAttempts = buildAttemptManager( 'cards-update', @@ -208,7 +208,7 @@ function setAuthorization(request, callbackBoolean) { // Instrument onloadend to remove stale auth tokens. var originalOnLoadEnd = request.onloadend; - request.onloadend = tasks.wrapCallback(function(event) { + request.onloadend = wrapper.wrapCallback(function(event) { if (request.status == HTTP_FORBIDDEN || request.status == HTTP_UNAUTHORIZED) { tasks.debugSetStepName('setAuthorization-removeToken'); diff --git a/chrome/browser/resources/google_now/background_test_util.js b/chrome/browser/resources/google_now/background_test_util.js index 3d59e0f6d3f5..d50207b1c42a 100644 --- a/chrome/browser/resources/google_now/background_test_util.js +++ b/chrome/browser/resources/google_now/background_test_util.js @@ -6,11 +6,10 @@ function emptyMock() {} +var wrapper = {instrumentChromeApiFunction: emptyMock}; + function buildTaskManager() { - return { - debugSetStepName: emptyMock, - instrumentChromeApiFunction: emptyMock, - }; + return {debugSetStepName: emptyMock}; } function buildAuthenticationManager() { @@ -40,4 +39,3 @@ instrumented['runtime'] = { onInstalled: emptyListener, onStartup: emptyListener }; - diff --git a/chrome/browser/resources/google_now/utility.js b/chrome/browser/resources/google_now/utility.js index 7878ac4652eb..a987dd1420ee 100644 --- a/chrome/browser/resources/google_now/utility.js +++ b/chrome/browser/resources/google_now/utility.js @@ -6,6 +6,20 @@ /** * @fileoverview Utility objects and functions for Google Now extension. + * Most important entities here: + * (1) 'wrapper' is a module used to add error handling and other services to + * callbacks for HTML and Chrome functions and Chrome event listeners. + * Chrome invokes extension code through event listeners. Once entered via + * an event listener, the extension may call a Chrome/HTML API method + * passing a callback (and so forth), and that callback must occur later, + * otherwise, we generate an error. Chrome may unload event pages waiting + * for an event. When the event fires, Chrome will reload the event page. We + * don't require event listeners to fire because they are generally not + * predictable (like a location change event). + * (2) Task Manager (built with buildTaskManager() call) provides controlling + * mutually excluding chains of callbacks called tasks. Task Manager uses + * WrapperPlugins to add instrumentation code to 'wrapper' to determine + * when a task completes. */ // TODO(vadimt): Figure out the server name. Use it in the manifest and for @@ -58,264 +72,209 @@ function buildServerRequest(handlerName, contentType) { return request; } -// Partial mirror of chrome.* for all instrumented functions. -var instrumented = {}; - /** - * 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. + * Sends an error report to the server. + * @param {Error} error Error to send. */ -function buildTaskManager(areConflicting) { - /** - * Queue of scheduled tasks. The first element, if present, corresponds to the - * currently running task. - * @type {Array.>} - */ - var queue = []; +function sendErrorReport(error) { + // Don't remove 'error.stack.replace' below! + var filteredStack = error.canSendMessageToServer ? + error.stack : error.stack.replace(/.*\n/, '(message removed)\n'); + var file; + var line; + var topFrameLineMatch = filteredStack.match(/\n at .*\n/); + var topFrame = topFrameLineMatch && topFrameLineMatch[0]; + if (topFrame) { + // Examples of a frame: + // 1. '\n at someFunction (chrome-extension:// + // pmofbkohncoogjjhahejjfbppikbjigm/background.js:915:15)\n' + // 2. '\n at chrome-extension://pmofbkohncoogjjhahejjfbppikbjigm/ + // utility.js:269:18\n' + // 3. '\n at Function.target.(anonymous function) (extensions:: + // SafeBuiltins:19:14)\n' + // 4. '\n at Event.dispatchToListener (event_bindings:382:22)\n' + var errorLocation; + // Find the the parentheses at the end of the line, if any. + var parenthesesMatch = topFrame.match(/\(.*\)\n/); + if (parenthesesMatch && parenthesesMatch[0]) { + errorLocation = + parenthesesMatch[0].substring(1, parenthesesMatch[0].length - 2); + } else { + errorLocation = topFrame; + } - /** - * Count of unfinished callbacks of the current task. - * @type {number} - */ - var taskPendingCallbackCount = 0; + var topFrameElements = errorLocation.split(':'); + // topFrameElements is an array that ends like: + // [N-3] //pmofbkohncoogjjhahejjfbppikbjigm/utility.js + // [N-2] 308 + // [N-1] 19 + if (topFrameElements.length >= 3) { + file = topFrameElements[topFrameElements.length - 3]; + line = topFrameElements[topFrameElements.length - 2]; + } + } - /** - * Required callbacks that are not yet called. Includes both task and non-task - * callbacks. This is a map from unique callback id to the stack at the moment - * when the callback was wrapped. This stack identifies the callback. - * Used only for diagnostics. - * @type {Object.} - */ - var pendingCallbacks = {}; + var requestParameters = + 'error=' + encodeURIComponent(error.name) + + '&script=' + encodeURIComponent(file) + + '&line=' + encodeURIComponent(line) + + '&trace=' + encodeURIComponent(filteredStack); + var request = buildServerRequest('jserror', + 'application/x-www-form-urlencoded'); + request.onloadend = function(event) { + console.log('sendErrorReport status: ' + request.status); + }; + request.send(requestParameters); +} - /** - * True if currently executed code is a part of a task. - * @type {boolean} - */ - var isInTask = false; +// Limiting 1 error report per background page load. +var errorReported = false; + +/** + * Reports an error to the server and the user, as appropriate. + * @param {Error} error Error to report. + */ +function reportError(error) { + var message = 'Critical error:\n' + error.stack; + console.error(message); + if (!errorReported) { + errorReported = true; + chrome.metricsPrivate.getIsCrashReportingEnabled(function(isEnabled) { + if (isEnabled) + sendErrorReport(error); + if (DEBUG_MODE) + alert(message); + }); + } +} + +// Partial mirror of chrome.* for all instrumented functions. +var instrumented = {}; + +/** + * Wrapper plugin. These plugins extend instrumentation added by + * wrapper.wrapCallback by adding code that executes before and after the call + * to the original callback provided by the extension. + * + * @typedef {{ + * prologue: function (), + * epilogue: function () + * }} + */ +var WrapperPlugin; +/** + * Wrapper for callbacks. Used to add error handling and other services to + * callbacks for HTML and Chrome functions and events. + */ +var wrapper = (function() { /** - * True if currently executed code runs in an instrumented callback. - * @type {boolean} + * Factory for wrapper plugins. If specified, it's used to generate an + * instance of WrapperPlugin each time we wrap a callback (which corresponds + * to addListener call for Chrome events, and to every API call that specifies + * a callback). WrapperPlugin's lifetime ends when the callback for which it + * was generated, exits. It's possible to have several instances of + * WrapperPlugin at the same time. + * An instance of WrapperPlugin can have state that can be shared by its + * constructor, prologue() and epilogue(). Also WrapperPlugins can change + * state of other objects, for example, to do refcounting. + * @type {function(): WrapperPlugin} */ - var isInInstrumentedCallback = false; + var wrapperPluginFactory = null; /** - * Checks that we run in an instrumented callback. + * Registers a wrapper plugin factory. + * @param {function(): WrapperPlugin} factory Wrapper plugin factory. */ - function checkInInstrumentedCallback() { - if (!isInInstrumentedCallback) { + function registerWrapperPluginFactory(factory) { + if (wrapperPluginFactory) { reportError(buildErrorWithMessageForServer( - 'Not in instrumented callback')); + 'registerWrapperPluginFactory: factory is already registered.')); } - } - - /** - * Starts the first queued task. - */ - function startFirst() { - verify(queue.length >= 1, 'startFirst: queue is empty'); - verify(!isInTask, 'startFirst: already in task'); - isInTask = true; - - // Start the oldest queued task, but don't remove it from the queue. - verify( - taskPendingCallbackCount == 0, - 'tasks.startFirst: still have pending task callbacks: ' + - taskPendingCallbackCount + - ', queue = ' + JSON.stringify(queue) + - ', pendingCallbacks = ' + JSON.stringify(pendingCallbacks)); - var entry = queue[0]; - console.log('Starting task ' + entry.name); - entry.task(function() {}); // TODO(vadimt): Don't pass parameter. - - verify(isInTask, 'startFirst: not in task at exit'); - isInTask = false; - if (taskPendingCallbackCount == 0) - finish(); + wrapperPluginFactory = factory; } /** - * 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. + * True if currently executed code runs in a callback or event handler that + * was instrumented by wrapper.wrapCallback() call. + * @type {boolean} */ - 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; - } + var isInWrappedCallback = false; /** - * 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. Call this callback on completion. + * Required callbacks that are not yet called. Includes both task and non-task + * callbacks. This is a map from unique callback id to the stack at the moment + * when the callback was wrapped. This stack identifies the callback. + * Used only for diagnostics. + * @type {Object.} */ - function add(taskName, task) { - checkInInstrumentedCallback(); - console.log('Adding task ' + taskName); - if (!canQueue(taskName)) - return; - - queue.push({name: taskName, task: task}); - - if (queue.length == 1) { - startFirst(); - } - } + var pendingCallbacks = {}; /** - * Completes the current task and starts the next queued task if available. + * Unique ID of the next callback. + * @type {number} */ - function finish() { - verify(queue.length >= 1, - 'tasks.finish: The task queue is empty'); - console.log('Finishing task ' + queue[0].name); - queue.shift(); - - if (queue.length >= 1) - startFirst(); - } - - // Limiting 1 error report per background page load. - var errorReported = false; + var nextCallbackId = 0; /** - * Sends an error report to the server. - * @param {Error} error Error to send. + * Gets diagnostic string with the status of the wrapper. + * @return {string} Diagnostic string. */ - function sendErrorReport(error) { - var filteredStack = error.canSendMessageToServer ? - error.stack : error.stack.replace(/.*\n/, '(message removed)\n'); - var file; - var line; - var topFrameLineMatch = filteredStack.match(/\n at .*\n/); - var topFrame = topFrameLineMatch && topFrameLineMatch[0]; - if (topFrame) { - // Examples of a frame: - // 1. '\n at someFunction (chrome-extension:// - // pmofbkohncoogjjhahejjfbppikbjigm/background.js:915:15)\n' - // 2. '\n at chrome-extension://pmofbkohncoogjjhahejjfbppikbjigm/ - // utility.js:269:18\n' - // 3. '\n at Function.target.(anonymous function) (extensions:: - // SafeBuiltins:19:14)\n' - // 4. '\n at Event.dispatchToListener (event_bindings:382:22)\n' - var errorLocation; - // Find the the parentheses at the end of the line, if any. - var parenthesesMatch = topFrame.match(/\(.*\)\n/); - if (parenthesesMatch && parenthesesMatch[0]) { - errorLocation = - parenthesesMatch[0].substring(1, parenthesesMatch[0].length - 2); - } else { - errorLocation = topFrame; - } - - var topFrameElements = errorLocation.split(':'); - // topFrameElements is an array that ends like: - // [N-3] //pmofbkohncoogjjhahejjfbppikbjigm/utility.js - // [N-2] 308 - // [N-1] 19 - if (topFrameElements.length >= 3) { - file = topFrameElements[topFrameElements.length - 3]; - line = topFrameElements[topFrameElements.length - 2]; - } - } - - var requestParameters = - 'error=' + encodeURIComponent(error.name) + - '&script=' + encodeURIComponent(file) + - '&line=' + encodeURIComponent(line) + - '&trace=' + encodeURIComponent(filteredStack); - var request = buildServerRequest('jserror', - 'application/x-www-form-urlencoded'); - request.onloadend = function(event) { - console.log('sendErrorReport status: ' + request.status); - }; - request.send(requestParameters); + function debugGetStateString() { + return 'pendingCallbacks = ' + JSON.stringify(pendingCallbacks); } /** - * Reports an error to the server and the user, as appropriate. - * @param {Error} error Error to report. + * Checks that we run in a wrapped callback. */ - function reportError(error) { - var message = 'Critical error:\n' + error.stack; - console.error(message); - if (!errorReported) { - errorReported = true; - chrome.metricsPrivate.getIsCrashReportingEnabled(function(isEnabled) { - if (isEnabled) - sendErrorReport(error); - if (DEBUG_MODE) - alert(message); - }); + function checkInWrappedCallback() { + if (!isInWrappedCallback) { + reportError(buildErrorWithMessageForServer( + 'Not in instrumented callback')); } } /** - * Unique ID of the next callback. - * @type {number} - */ - var nextCallbackId = 0; - - /** * Adds error processing to an API callback. * @param {Function} callback Callback to instrument. - * @param {boolean=} opt_isEventListener True if the callback is an event - * listener. + * @param {boolean=} opt_isEventListener True if the callback is a listener to + * a Chrome API event. * @return {Function} Instrumented callback. */ function wrapCallback(callback, opt_isEventListener) { - verify(!(opt_isEventListener && isInTask), - 'Unrequired callback in a task.'); var callbackId = nextCallbackId++; - var isTaskCallback = isInTask; - if (isTaskCallback) - ++taskPendingCallbackCount; + if (!opt_isEventListener) { - checkInInstrumentedCallback(); + checkInWrappedCallback(); pendingCallbacks[callbackId] = new Error().stack; } + // wrapperPluginFactory may be null before task manager is built, and in + // tests. + var wrapperPluginInstance = wrapperPluginFactory && wrapperPluginFactory(); + return function() { // This is the wrapper for the callback. try { - verify(!isInInstrumentedCallback, 'Re-entering instrumented callback'); - isInInstrumentedCallback = true; + verify(!isInWrappedCallback, 'Re-entering instrumented callback'); + isInWrappedCallback = true; - if (isTaskCallback) { - verify(!isInTask, 'wrapCallback: already in task'); - isInTask = true; - } if (!opt_isEventListener) delete pendingCallbacks[callbackId]; + if (wrapperPluginInstance) + wrapperPluginInstance.prologue(); + // Call the original callback. callback.apply(null, arguments); - if (isTaskCallback) { - verify(isInTask, 'wrapCallback: not in task at exit'); - isInTask = false; - if (--taskPendingCallbackCount == 0) - finish(); - } + if (wrapperPluginInstance) + wrapperPluginInstance.epilogue(); - verify(isInInstrumentedCallback, + verify(isInWrappedCallback, 'Instrumented callback is not instrumented upon exit'); - isInInstrumentedCallback = false; + isInWrappedCallback = false; } catch (error) { reportError(error); } @@ -324,11 +283,12 @@ function buildTaskManager(areConflicting) { /** * Returns an instrumented function. - * @param {array} functionIdentifierParts Path to the chrome.* function. + * @param {!Array.} functionIdentifierParts Path to the chrome.* + * function. * @param {string} functionName Name of the chrome API function. * @param {number} callbackParameter Index of the callback parameter to this * API function. - * @return {function} An instrumented function. + * @return {Function} An instrumented function. */ function createInstrumentedFunction( functionIdentifierParts, @@ -348,7 +308,7 @@ function buildTaskManager(areConflicting) { callback, functionName == 'addListener'); var chromeContainer = chrome; - functionIdentifierParts.map(function(fragment) { + functionIdentifierParts.forEach(function(fragment) { chromeContainer = chromeContainer[fragment]; }); return chromeContainer[functionName]. @@ -369,7 +329,7 @@ function buildTaskManager(areConflicting) { var functionName = functionIdentifierParts.pop(); var chromeContainer = chrome; var instrumentedContainer = instrumented; - functionIdentifierParts.map(function(fragment) { + functionIdentifierParts.forEach(function(fragment) { chromeContainer = chromeContainer[fragment]; if (!chromeContainer) { reportError(buildErrorWithMessageForServer( @@ -394,26 +354,184 @@ function buildTaskManager(areConflicting) { callbackParameter); } - instrumentChromeApiFunction('alarms.get', 1); - instrumentChromeApiFunction('alarms.onAlarm.addListener', 0); - instrumentChromeApiFunction('identity.getAuthToken', 1); - instrumentChromeApiFunction('identity.removeCachedAuthToken', 1); instrumentChromeApiFunction('runtime.onSuspend.addListener', 0); - chrome.runtime.onSuspend.addListener(function() { + instrumented.runtime.onSuspend.addListener(function() { var stringifiedPendingCallbacks = JSON.stringify(pendingCallbacks); verify( - queue.length == 0 && stringifiedPendingCallbacks == '{}', - 'Incomplete task or pending callbacks when unloading event page,' + - ' queue = ' + JSON.stringify(queue) + - ', pendingCallbacks = ' + stringifiedPendingCallbacks); + stringifiedPendingCallbacks == '{}', + 'Pending callbacks when unloading event page:' + + stringifiedPendingCallbacks); }); return { - add: add, - debugSetStepName: function() {}, // TODO(vadimt): remove + wrapCallback: wrapCallback, instrumentChromeApiFunction: instrumentChromeApiFunction, - wrapCallback: wrapCallback + registerWrapperPluginFactory: registerWrapperPluginFactory, + checkInWrappedCallback: checkInWrappedCallback, + debugGetStateString: debugGetStateString + }; +})(); + +wrapper.instrumentChromeApiFunction('alarms.get', 1); +wrapper.instrumentChromeApiFunction('alarms.onAlarm.addListener', 0); +wrapper.instrumentChromeApiFunction('identity.getAuthToken', 1); +wrapper.instrumentChromeApiFunction('identity.removeCachedAuthToken', 1); + +/** + * 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) { + /** + * Queue of scheduled tasks. The first element, if present, corresponds to the + * currently running task. + * @type {Array.>} + */ + var queue = []; + + /** + * Count of unfinished callbacks of the current task. + * @type {number} + */ + var taskPendingCallbackCount = 0; + + /** + * True if currently executed code is a part of a task. + * @type {boolean} + */ + var isInTask = false; + + /** + * Starts the first queued task. + */ + function startFirst() { + verify(queue.length >= 1, 'startFirst: queue is empty'); + verify(!isInTask, 'startFirst: already in task'); + isInTask = true; + + // Start the oldest queued task, but don't remove it from the queue. + verify( + taskPendingCallbackCount == 0, + 'tasks.startFirst: still have pending task callbacks: ' + + taskPendingCallbackCount + + ', queue = ' + JSON.stringify(queue) + ', ' + + wrapper.debugGetStateString()); + var entry = queue[0]; + console.log('Starting task ' + entry.name); + + entry.task(function() {}); // TODO(vadimt): Don't pass parameter. + + verify(isInTask, 'startFirst: not in task at exit'); + isInTask = false; + if (taskPendingCallbackCount == 0) + 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. Call this callback on completion. + */ + function add(taskName, task) { + wrapper.checkInWrappedCallback(); + 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'); + console.log('Finishing task ' + queue[0].name); + queue.shift(); + + if (queue.length >= 1) + startFirst(); + } + + instrumented.runtime.onSuspend.addListener(function() { + verify( + queue.length == 0, + 'Incomplete task when unloading event page,' + + ' queue = ' + JSON.stringify(queue) + ', ' + + wrapper.debugGetStateString()); + }); + + + /** + * Wrapper plugin for tasks. + * @constructor + */ + function TasksWrapperPlugin() { + this.isTaskCallback = isInTask; + if (this.isTaskCallback) + ++taskPendingCallbackCount; + } + + TasksWrapperPlugin.prototype = { + /** + * Plugin code to be executed before invoking the original callback. + */ + prologue: function() { + if (this.isTaskCallback) { + verify(!isInTask, 'TasksWrapperPlugin.prologue: already in task'); + isInTask = true; + } + }, + + /** + * Plugin code to be executed after invoking the original callback. + */ + epilogue: function() { + if (this.isTaskCallback) { + verify(isInTask, 'TasksWrapperPlugin.epilogue: not in task at exit'); + isInTask = false; + if (--taskPendingCallbackCount == 0) + finish(); + } + } + }; + + wrapper.registerWrapperPluginFactory(function() { + return new TasksWrapperPlugin(); + }); + + return { + add: add, + debugSetStepName: function() {} // TODO(vadimt): remove }; } -- 2.11.4.GIT