Preventing race conditions in Google Now extension.
Introducing a task manager. A task is an event chain that does certain work that we want to be atomic. Execution of multiple tasks is serialized. Also, adding checks for anomalous situations, such as a forever-running task keeps the event page from unloading or a task still running when the event page unloads. BUG=164227 Review URL: https://chromiumcodereview.appspot.com/12316075 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@190023 0039d316-1c4b-4281-b951-d872f2087c98
This commit is contained in:
chrome/browser/resources
@@ -111,6 +111,7 @@
|
|||||||
</if>
|
</if>
|
||||||
<if expr="pp_ifdef('enable_google_now')">
|
<if expr="pp_ifdef('enable_google_now')">
|
||||||
<include name="IDR_GOOGLE_NOW_BACKGROUND_JS" file="google_now/background.js" type="BINDATA" />
|
<include name="IDR_GOOGLE_NOW_BACKGROUND_JS" file="google_now/background.js" type="BINDATA" />
|
||||||
|
<include name="IDR_GOOGLE_NOW_UTILITY_JS" file="google_now/utility.js" type="BINDATA" />
|
||||||
</if>
|
</if>
|
||||||
<include name="IDR_APPS_DEBUGGER_MAIN" file="apps_debugger/main.html" flattenhtml="true" allowexternalscript="true" type="BINDATA" />
|
<include name="IDR_APPS_DEBUGGER_MAIN" file="apps_debugger/main.html" flattenhtml="true" allowexternalscript="true" type="BINDATA" />
|
||||||
<include name="IDR_APPS_DEBUGGER_BACKGROUND_JS" file="apps_debugger/background.js" flattenhtml="true" type="BINDATA" />
|
<include name="IDR_APPS_DEBUGGER_BACKGROUND_JS" file="apps_debugger/background.js" flattenhtml="true" type="BINDATA" />
|
||||||
|
@@ -24,8 +24,8 @@
|
|||||||
// TODO(vadimt): Figure out the final values of the constants.
|
// TODO(vadimt): Figure out the final values of the constants.
|
||||||
// TODO(vadimt): Report internal and server errors. Collect UMAs on errors where
|
// TODO(vadimt): Report internal and server errors. Collect UMAs on errors where
|
||||||
// appropriate. Also consider logging errors or throwing exceptions.
|
// appropriate. Also consider logging errors or throwing exceptions.
|
||||||
|
|
||||||
// TODO(vadimt): Consider processing errors for all storage.set calls.
|
// TODO(vadimt): Consider processing errors for all storage.set calls.
|
||||||
|
|
||||||
// TODO(vadimt): Figure out the server name. Use it in the manifest and for
|
// TODO(vadimt): Figure out the server name. Use it in the manifest and for
|
||||||
// NOTIFICATION_CARDS_URL. Meanwhile, to use the feature, you need to manually
|
// NOTIFICATION_CARDS_URL. Meanwhile, to use the feature, you need to manually
|
||||||
// set the server name via local storage.
|
// set the server name via local storage.
|
||||||
@@ -52,10 +52,39 @@ var INITIAL_POLLING_PERIOD_SECONDS = 300; // 5 minutes
|
|||||||
*/
|
*/
|
||||||
var MAXIMUM_POLLING_PERIOD_SECONDS = 3600; // 1 hour
|
var MAXIMUM_POLLING_PERIOD_SECONDS = 3600; // 1 hour
|
||||||
|
|
||||||
|
var UPDATE_NOTIFICATIONS_ALARM_NAME = 'UPDATE';
|
||||||
|
|
||||||
var storage = chrome.storage.local;
|
var storage = chrome.storage.local;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show a notification and remember information associated with it.
|
* Names for tasks that can be created by the extension.
|
||||||
|
*/
|
||||||
|
var UPDATE_CARDS_TASK_NAME = 'update-cards';
|
||||||
|
var DISMISS_CARD_TASK_NAME = 'dismiss-card';
|
||||||
|
var CARD_CLICKED_TASK_NAME = 'card-clicked';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a new task can't be scheduled when another task is already
|
||||||
|
* scheduled.
|
||||||
|
* @param {string} newTaskName Name of the new task.
|
||||||
|
* @param {string} queuedTaskName Name of the task in the queue.
|
||||||
|
* @return {boolean} Whether the new task conflicts with the existing task.
|
||||||
|
*/
|
||||||
|
function areTasksConflicting(newTaskName, queuedTaskName) {
|
||||||
|
if (newTaskName == UPDATE_CARDS_TASK_NAME &&
|
||||||
|
queuedTaskName == UPDATE_CARDS_TASK_NAME) {
|
||||||
|
// If a card update is requested while an old update is still scheduled, we
|
||||||
|
// don't need the new update.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tasks = buildTaskManager(areTasksConflicting);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a notification and remembers information associated with it.
|
||||||
* @param {Object} card Google Now card represented as a set of parameters for
|
* @param {Object} card Google Now card represented as a set of parameters for
|
||||||
* showing a Chrome notification.
|
* showing a Chrome notification.
|
||||||
* @param {Object} notificationsUrlInfo Map from notification id to the
|
* @param {Object} notificationsUrlInfo Map from notification id to the
|
||||||
@@ -67,17 +96,18 @@ function createNotification(card, notificationsUrlInfo) {
|
|||||||
chrome.notifications.create(
|
chrome.notifications.create(
|
||||||
card.notificationId,
|
card.notificationId,
|
||||||
card.notification,
|
card.notification,
|
||||||
function(assignedNotificationId) {});
|
function() {});
|
||||||
|
|
||||||
notificationsUrlInfo[card.notificationId] = card.actionUrls;
|
notificationsUrlInfo[card.notificationId] = card.actionUrls;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse JSON response from the notification server, show notifications and
|
* Parses JSON response from the notification server, show notifications and
|
||||||
* schedule next update.
|
* schedule next update.
|
||||||
* @param {string} response Server response.
|
* @param {string} response Server response.
|
||||||
|
* @param {function()} callback Completion callback.
|
||||||
*/
|
*/
|
||||||
function parseAndShowNotificationCards(response) {
|
function parseAndShowNotificationCards(response, callback) {
|
||||||
try {
|
try {
|
||||||
var parsedResponse = JSON.parse(response);
|
var parsedResponse = JSON.parse(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -97,6 +127,8 @@ function parseAndShowNotificationCards(response) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.debugSetStepName(
|
||||||
|
'parseAndShowNotificationCards-get-active-notifications');
|
||||||
storage.get('activeNotifications', function(items) {
|
storage.get('activeNotifications', function(items) {
|
||||||
// Mark existing notifications that received an update in this server
|
// Mark existing notifications that received an update in this server
|
||||||
// response.
|
// response.
|
||||||
@@ -107,11 +139,12 @@ function parseAndShowNotificationCards(response) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delete notifications that didn't receive an update.
|
// Delete notifications that didn't receive an update.
|
||||||
for (var notificationId in items.activeNotifications)
|
for (var notificationId in items.activeNotifications) {
|
||||||
if (!items.activeNotifications[notificationId].hasUpdate) {
|
if (!items.activeNotifications[notificationId].hasUpdate) {
|
||||||
chrome.notifications.clear(
|
chrome.notifications.clear(
|
||||||
notificationId,
|
notificationId,
|
||||||
function(wasDeleted) {});
|
function() {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create/update notifications and store their new properties.
|
// Create/update notifications and store their new properties.
|
||||||
@@ -132,74 +165,80 @@ function parseAndShowNotificationCards(response) {
|
|||||||
// to the initial value. This retry period will be used the next time we
|
// to the initial value. This retry period will be used the next time we
|
||||||
// fail to get the server-provided period.
|
// fail to get the server-provided period.
|
||||||
storage.set({retryDelaySeconds: INITIAL_POLLING_PERIOD_SECONDS});
|
storage.set({retryDelaySeconds: INITIAL_POLLING_PERIOD_SECONDS});
|
||||||
|
callback();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request notification cards from the server.
|
* Requests notification cards from the server.
|
||||||
* @param {string} requestParameters Query string for the request.
|
* @param {string} requestParameters Query string for the request.
|
||||||
|
* @param {function()} callback Completion callback.
|
||||||
*/
|
*/
|
||||||
function requestNotificationCards(requestParameters) {
|
function requestNotificationCards(requestParameters, callback) {
|
||||||
// TODO(vadimt): Figure out how to send user's identity to the server.
|
// TODO(vadimt): Figure out how to send user's identity to the server.
|
||||||
var request = new XMLHttpRequest();
|
var request = new XMLHttpRequest();
|
||||||
|
|
||||||
request.responseType = 'text';
|
request.responseType = 'text';
|
||||||
request.onload = function(event) {
|
request.onloadend = function(event) {
|
||||||
if (request.status == HTTP_OK)
|
if (request.status == HTTP_OK)
|
||||||
parseAndShowNotificationCards(request.response);
|
parseAndShowNotificationCards(request.response, callback);
|
||||||
}
|
else
|
||||||
|
callback();
|
||||||
|
};
|
||||||
|
|
||||||
request.open(
|
request.open(
|
||||||
'GET',
|
'GET',
|
||||||
NOTIFICATION_CARDS_URL + '/notifications' + requestParameters,
|
NOTIFICATION_CARDS_URL + '/notifications' + requestParameters,
|
||||||
true);
|
true);
|
||||||
|
tasks.debugSetStepName('requestNotificationCards-send-request');
|
||||||
request.send();
|
request.send();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request notification cards from the server when we have geolocation.
|
* Requests notification cards from the server when we have geolocation.
|
||||||
* @param {Geoposition} position Location of this computer.
|
* @param {Geoposition} position Location of this computer.
|
||||||
|
* @param {function()} callback Completion callback.
|
||||||
*/
|
*/
|
||||||
function requestNotificationCardsWithLocation(position) {
|
function requestNotificationCardsWithLocation(position, callback) {
|
||||||
// TODO(vadimt): Should we use 'q' as the parameter name?
|
// TODO(vadimt): Should we use 'q' as the parameter name?
|
||||||
var requestParameters =
|
var requestParameters =
|
||||||
'?q=' + position.coords.latitude +
|
'?q=' + position.coords.latitude +
|
||||||
',' + position.coords.longitude +
|
',' + position.coords.longitude +
|
||||||
',' + position.coords.accuracy;
|
',' + position.coords.accuracy;
|
||||||
|
|
||||||
requestNotificationCards(requestParameters);
|
requestNotificationCards(requestParameters, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request notification cards from the server when we don't have geolocation.
|
* Obtains new location; requests and shows notification cards based on this
|
||||||
* @param {PositionError} positionError Position error.
|
|
||||||
*/
|
|
||||||
function requestNotificationCardsWithoutLocation(positionError) {
|
|
||||||
requestNotificationCards('');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtain new location; request and show notification cards based on this
|
|
||||||
* location.
|
* location.
|
||||||
*/
|
*/
|
||||||
function updateNotificationsCards() {
|
function updateNotificationsCards() {
|
||||||
storage.get('retryDelaySeconds', function(items) {
|
tasks.add(UPDATE_CARDS_TASK_NAME, function(callback) {
|
||||||
// Immediately schedule the update after the current retry period. Then,
|
tasks.debugSetStepName('updateNotificationsCards-get-retryDelaySeconds');
|
||||||
// we'll use update time from the server if available.
|
storage.get('retryDelaySeconds', function(items) {
|
||||||
scheduleNextUpdate(items.retryDelaySeconds);
|
// Immediately schedule the update after the current retry period. Then,
|
||||||
|
// we'll use update time from the server if available.
|
||||||
|
scheduleNextUpdate(items.retryDelaySeconds);
|
||||||
|
|
||||||
// TODO(vadimt): Consider interrupting waiting for the next update if we
|
// TODO(vadimt): Consider interrupting waiting for the next update if we
|
||||||
// detect that the network conditions have changed. Also, decide whether the
|
// detect that the network conditions have changed. Also, decide whether
|
||||||
// exponential backoff is needed both when we are offline and when there are
|
// the exponential backoff is needed both when we are offline and when
|
||||||
// failures on the server side.
|
// there are failures on the server side.
|
||||||
var newRetryDelaySeconds =
|
var newRetryDelaySeconds =
|
||||||
Math.min(items.retryDelaySeconds * 2 * (1 + 0.2 * Math.random()),
|
Math.min(items.retryDelaySeconds * 2 * (1 + 0.2 * Math.random()),
|
||||||
MAXIMUM_POLLING_PERIOD_SECONDS);
|
MAXIMUM_POLLING_PERIOD_SECONDS);
|
||||||
storage.set({retryDelaySeconds: newRetryDelaySeconds});
|
storage.set({retryDelaySeconds: newRetryDelaySeconds});
|
||||||
|
|
||||||
navigator.geolocation.getCurrentPosition(
|
tasks.debugSetStepName('updateNotificationsCards-get-location');
|
||||||
requestNotificationCardsWithLocation,
|
navigator.geolocation.getCurrentPosition(
|
||||||
requestNotificationCardsWithoutLocation);
|
function(position) {
|
||||||
|
requestNotificationCardsWithLocation(position, callback);
|
||||||
|
},
|
||||||
|
function() {
|
||||||
|
requestNotificationCards('', callback);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,21 +249,27 @@ function updateNotificationsCards() {
|
|||||||
* the clicked area from the button action URLs info.
|
* the clicked area from the button action URLs info.
|
||||||
*/
|
*/
|
||||||
function onNotificationClicked(notificationId, selector) {
|
function onNotificationClicked(notificationId, selector) {
|
||||||
storage.get('activeNotifications', function(items) {
|
tasks.add(CARD_CLICKED_TASK_NAME, function(callback) {
|
||||||
var actionUrls = items.activeNotifications[notificationId];
|
tasks.debugSetStepName('onNotificationClicked-get-activeNotifications');
|
||||||
if (typeof actionUrls != 'object') {
|
storage.get('activeNotifications', function(items) {
|
||||||
// TODO(vadimt): report an error.
|
var actionUrls = items.activeNotifications[notificationId];
|
||||||
return;
|
if (typeof actionUrls != 'object') {
|
||||||
}
|
// TODO(vadimt): report an error.
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var url = selector(actionUrls);
|
var url = selector(actionUrls);
|
||||||
|
|
||||||
if (typeof url != 'string') {
|
if (typeof url != 'string') {
|
||||||
// TODO(vadimt): report an error.
|
// TODO(vadimt): report an error.
|
||||||
return;
|
callback();
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
chrome.tabs.create({url: url});
|
chrome.tabs.create({url: url});
|
||||||
|
callback();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,46 +279,60 @@ function onNotificationClicked(notificationId, selector) {
|
|||||||
* @param {boolean} byUser Whether the notification was closed by the user.
|
* @param {boolean} byUser Whether the notification was closed by the user.
|
||||||
*/
|
*/
|
||||||
function onNotificationClosed(notificationId, byUser) {
|
function onNotificationClosed(notificationId, byUser) {
|
||||||
if (byUser) {
|
if (!byUser)
|
||||||
// TODO(vadimt): Analyze possible race conditions between request for cards
|
return;
|
||||||
// and dismissal.
|
|
||||||
|
tasks.add(DISMISS_CARD_TASK_NAME, function(callback) {
|
||||||
|
// Deleting the notification in case it was re-added while this task was
|
||||||
|
// waiting in the queue.
|
||||||
|
chrome.notifications.clear(
|
||||||
|
notificationId,
|
||||||
|
function() {});
|
||||||
|
|
||||||
// Send a dismiss request to the server.
|
// Send a dismiss request to the server.
|
||||||
var requestParameters = '?id=' + notificationId;
|
var requestParameters = '?id=' + notificationId;
|
||||||
var request = new XMLHttpRequest();
|
var request = new XMLHttpRequest();
|
||||||
request.responseType = 'text';
|
request.responseType = 'text';
|
||||||
|
request.onloadend = function(event) {
|
||||||
|
callback();
|
||||||
|
};
|
||||||
// TODO(vadimt): If the request fails, for example, because there is no
|
// TODO(vadimt): If the request fails, for example, because there is no
|
||||||
// internet connection, do retry with exponential backoff.
|
// internet connection, do retry with exponential backoff.
|
||||||
request.open(
|
request.open(
|
||||||
'GET',
|
'GET',
|
||||||
NOTIFICATION_CARDS_URL + '/dismiss' + requestParameters,
|
NOTIFICATION_CARDS_URL + '/dismiss' + requestParameters,
|
||||||
true);
|
true);
|
||||||
|
tasks.debugSetStepName('onNotificationClosed-send-request');
|
||||||
request.send();
|
request.send();
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schedule next update for notification cards.
|
* Schedules next update for notification cards.
|
||||||
* @param {int} delaySeconds Length of time in seconds after which the alarm
|
* @param {int} delaySeconds Length of time in seconds after which the alarm
|
||||||
* event should fire.
|
* event should fire.
|
||||||
*/
|
*/
|
||||||
function scheduleNextUpdate(delaySeconds) {
|
function scheduleNextUpdate(delaySeconds) {
|
||||||
// Schedule an alarm after the specified delay. 'periodInMinutes' is for the
|
// Schedule an alarm after the specified delay. 'periodInMinutes' is for the
|
||||||
// case when we fail to re-register the alarm.
|
// case when we fail to re-register the alarm.
|
||||||
chrome.alarms.create({
|
var alarmInfo = {
|
||||||
delayInMinutes: delaySeconds / 60,
|
delayInMinutes: delaySeconds / 60,
|
||||||
periodInMinutes: MAXIMUM_POLLING_PERIOD_SECONDS / 60
|
periodInMinutes: MAXIMUM_POLLING_PERIOD_SECONDS / 60
|
||||||
});
|
};
|
||||||
|
|
||||||
|
chrome.alarms.create(UPDATE_NOTIFICATIONS_ALARM_NAME, alarmInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the event page on install or on browser startup.
|
* Initializes the event page on install or on browser startup.
|
||||||
*/
|
*/
|
||||||
function initialize() {
|
function initialize() {
|
||||||
var initialStorage = {
|
var initialStorage = {
|
||||||
activeNotifications: {},
|
activeNotifications: {},
|
||||||
retryDelaySeconds: INITIAL_POLLING_PERIOD_SECONDS
|
retryDelaySeconds: INITIAL_POLLING_PERIOD_SECONDS
|
||||||
};
|
};
|
||||||
storage.set(initialStorage, updateNotificationsCards);
|
storage.set(initialStorage);
|
||||||
|
updateNotificationsCards();
|
||||||
}
|
}
|
||||||
|
|
||||||
chrome.runtime.onInstalled.addListener(function(details) {
|
chrome.runtime.onInstalled.addListener(function(details) {
|
||||||
@@ -286,7 +345,8 @@ chrome.runtime.onStartup.addListener(function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
chrome.alarms.onAlarm.addListener(function(alarm) {
|
chrome.alarms.onAlarm.addListener(function(alarm) {
|
||||||
updateNotificationsCards();
|
if (alarm.name == UPDATE_NOTIFICATIONS_ALARM_NAME)
|
||||||
|
updateNotificationsCards();
|
||||||
});
|
});
|
||||||
|
|
||||||
chrome.notifications.onClicked.addListener(
|
chrome.notifications.onClicked.addListener(
|
||||||
|
@@ -16,7 +16,7 @@
|
|||||||
],
|
],
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"background": {
|
"background": {
|
||||||
"scripts": ["background.js"],
|
"scripts": ["utility.js", "background.js"],
|
||||||
"persistent": false
|
"persistent": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
156
chrome/browser/resources/google_now/utility.js
Normal file
156
chrome/browser/resources/google_now/utility.js
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
// 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';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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) {
|
||||||
|
// TODO(vadimt): Send UMAs instead of showing alert.
|
||||||
|
// TODO(vadimt): Make sure the execution doesn't continue after this call.
|
||||||
|
if (!condition) {
|
||||||
|
var errorText = 'ASSERT: ' + message;
|
||||||
|
console.error(errorText);
|
||||||
|
alert(errorText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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');
|
||||||
|
var entry = queue[0];
|
||||||
|
stepName = entry.name + '-initial';
|
||||||
|
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))
|
||||||
|
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) {
|
||||||
|
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.');
|
||||||
|
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) {
|
||||||
|
// TODO(vadimt): Pass UMA counters instead of step names.
|
||||||
|
stepName = step;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
// TODO(vadimt): Uncomment the verify once this bug is fixed:
|
||||||
|
// crbug.com/177563
|
||||||
|
// verify(false, 'Event page didn\'t unload, queue = ' +
|
||||||
|
// JSON.stringify(tasks) + ', step = ' + stepName + ' (ignore this verify
|
||||||
|
// if devtools is attached).');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
chrome.runtime.onSuspend.addListener(function() {
|
||||||
|
chrome.alarms.clear(CANNOT_UNLOAD_ALARM_NAME);
|
||||||
|
verify(queue.length == 0 && stepName == null,
|
||||||
|
'Incomplete task when unloading event page, queue = ' +
|
||||||
|
JSON.stringify(queue) + ', step = ' + stepName);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
add: add,
|
||||||
|
debugSetStepName: debugSetStepName
|
||||||
|
};
|
||||||
|
}
|
Reference in New Issue
Block a user