author | Michael Ratcliffe <mratcliffe@mozilla.com> |
Sat, 20 Jun 2015 17:39:22 +0100 | |
changeset 249840 | ac02ae385e7521cd4e53d51dba815694ae2a58ce |
parent 249839 | e67d1044c4395975225f6adc7d4f1714233dcb2f |
child 249841 | fffa73480bd00129665b4e1ba61206d6dca9f206 |
push id | 28939 |
push user | cbook@mozilla.com |
push date | Mon, 22 Jun 2015 11:56:54 +0000 |
treeherder | mozilla-central@cddf3b36b5e2 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | past |
bugs | 1049888 |
milestone | 41.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/browser/devtools/framework/test/browser_toolbox_tool_ready.js +++ b/browser/devtools/framework/test/browser_toolbox_tool_ready.js @@ -13,16 +13,21 @@ function performChecks(target) { let toolIds = gDevTools.getToolDefinitionArray() .filter(def => def.isTargetSupported(target)) .map(def => def.id); let toolbox; for (let index = 0; index < toolIds.length; index++) { let toolId = toolIds[index]; + // FIXME Bug 1175850 - Enable storage inspector tests after upgrading for E10S + if (toolId === "storage") { + continue; + } + info("About to open " + index + "/" + toolId); toolbox = yield gDevTools.showToolbox(target, toolId); ok(toolbox, "toolbox exists for " + toolId); is(toolbox.currentToolId, toolId, "currentToolId should be " + toolId); let panel = toolbox.getCurrentPanel(); ok(panel.isReady, toolId + " panel should be ready"); }
--- a/browser/devtools/framework/test/browser_toolbox_tool_remote_reopen.js +++ b/browser/devtools/framework/test/browser_toolbox_tool_remote_reopen.js @@ -44,16 +44,21 @@ function runTools(target) { let toolIds = gDevTools.getToolDefinitionArray() .filter(def => def.isTargetSupported(target)) .map(def => def.id); let toolbox; for (let index = 0; index < toolIds.length; index++) { let toolId = toolIds[index]; + // FIXME Bug 1175850 - Enable storage inspector tests after upgrading for E10S + if (toolId === "storage") { + continue; + } + info("About to open " + index + "/" + toolId); toolbox = yield gDevTools.showToolbox(target, toolId, "window"); ok(toolbox, "toolbox exists for " + toolId); is(toolbox.currentToolId, toolId, "currentToolId should be " + toolId); let panel = toolbox.getCurrentPanel(); ok(panel.isReady, toolId + " panel should be ready"); }
--- a/browser/devtools/framework/test/browser_toolbox_window_shortcuts.js +++ b/browser/devtools/framework/test/browser_toolbox_window_shortcuts.js @@ -38,16 +38,22 @@ function testShortcuts(aToolbox, aIndex) } else if (aIndex == toolIDs.length) { tidyUp(); return; } toolbox = aToolbox; info("Toolbox fired a `ready` event"); + // FIXME Bug 1175850 - Enable storage inspector tests after upgrading for E10S + if (toolIDs[aIndex] === "storage") { + testShortcuts(toolbox, aIndex + 1); + return; + } + toolbox.once("select", selectCB); let key = gDevTools._tools.get(toolIDs[aIndex]).key; let toolModifiers = gDevTools._tools.get(toolIDs[aIndex]).modifiers; let modifiers = { accelKey: toolModifiers.includes("accel"), altKey: toolModifiers.includes("alt"), shiftKey: toolModifiers.includes("shift"),
--- a/toolkit/devtools/server/actors/storage.js +++ b/toolkit/devtools/server/actors/storage.js @@ -3,53 +3,51 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const {Cu, Cc, Ci} = require("chrome"); const events = require("sdk/event/core"); const protocol = require("devtools/server/protocol"); try { - const { indexedDB } = require("sdk/indexed-db"); + const { indexedDB } = require("sdk/indexed-db"); } catch (e) { - // In xpcshell tests, we can't actually have indexedDB, which is OK: - // we don't use it there anyway. + // In xpcshell tests, we can't actually have indexedDB, which is OK: + // we don't use it there anyway. } const {async} = require("devtools/async-utils"); const {Arg, Option, method, RetVal, types} = protocol; -const {LongStringActor, ShortLongString} = require("devtools/server/actors/string"); - -Cu.import("resource://gre/modules/Promise.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm"); +const {LongStringActor} = require("devtools/server/actors/string"); +const {DebuggerServer} = require("devtools/server/main"); +const Services = require("Services"); +const promise = require("promise"); -XPCOMUtils.defineLazyModuleGetter(this, "Sqlite", - "resource://gre/modules/Sqlite.jsm"); +loader.lazyImporter(this, "OS", "resource://gre/modules/osfile.jsm"); +loader.lazyImporter(this, "Sqlite", "resource://gre/modules/Sqlite.jsm"); +loader.lazyImporter(this, "LayoutHelpers", + "resource://gre/modules/devtools/LayoutHelpers.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "OS", - "resource://gre/modules/osfile.jsm"); - -// Global required for window less Indexed DB instantiation. -let global = this; +let gTrackedMessageManager = new Map(); // Maximum number of cookies/local storage key-value-pairs that can be sent // over the wire to the client in one request. const MAX_STORE_OBJECT_COUNT = 50; // Interval for the batch job that sends the accumilated update packets to the -// client. -const UPDATE_INTERVAL = 500; // ms +// client (ms). +const UPDATE_INTERVAL = 500; // A RegExp for characters that cannot appear in a file/directory name. This is // used to sanitize the host name for indexed db to lookup whether the file is // present in <profileDir>/storage/default/ location let illegalFileNameCharacters = [ "[", - "\\x00-\\x25", // Control characters \001 to \037 - "/:*?\\\"<>|\\\\", // Special characters + // Control characters \001 to \037 + "\\x00-\\x25", + // Special characters + "/:*?\\\"<>|\\\\", "]" ].join(""); let ILLEGAL_CHAR_REGEX = new RegExp(illegalFileNameCharacters, "g"); // Holder for all the registered storage actors. let storageTypePool = new Map(); /** @@ -66,25 +64,25 @@ function getRegisteredTypes() { /** * An async method equivalent to setTimeout but using Promises * * @param {number} time * The wait Ttme in milliseconds. */ function sleep(time) { - let wait = Promise.defer(); + let wait = promise.defer(); let updateTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); updateTimer.initWithCallback({ notify: function() { updateTimer.cancel(); updateTimer = null; wait.resolve(null); } - } , time, Ci.nsITimer.TYPE_ONE_SHOT); + }, time, Ci.nsITimer.TYPE_ONE_SHOT); return wait.promise; } // Cookies store object types.addDictType("cookieobject", { name: "string", value: "longstring", path: "nullable:string", @@ -317,17 +315,18 @@ StorageActors.defaults = function(typeNa * starting index and total size can be controlled via the options object * * @param {string} host * The host name for which the store values are required. * @param {array:string} names * Array containing the names of required store objects. Empty if all * items are required. * @param {object} options - * Additional options for the request containing following properties: + * Additional options for the request containing following + * properties: * - offset {number} : The begin index of the returned array amongst * the total values * - size {number} : The number of values required. * - sortOn {string} : The values should be sorted on this property. * - index {string} : In case of indexed db, the IDBIndex to be used * for fetching the values. * * @return {object} An object containing following properties: @@ -348,59 +347,75 @@ StorageActors.defaults = function(typeNa let toReturn = { offset: offset, total: 0, data: [] }; if (names) { for (let name of names) { - toReturn.data.push( - // yield because getValuesForHost is async for Indexed DB - ...(yield this.getValuesForHost(host, name, options)) - ); + let values = + yield this.getValuesForHost(host, name, options, this.hostVsStores); + + let {result, objectStores} = values; + + if (result && typeof result.objectsSize !== "undefined") { + for (let {key, count} of result.objectsSize) { + this.objectsSize[key] = count; + } + } + + if (result) { + toReturn.data.push(...result.data); + } else if (objectStores) { + toReturn.data.push(...objectStores); + } else { + toReturn.data.push(...values); + } } toReturn.total = this.getObjectsSize(host, names, options); if (offset > toReturn.total) { // In this case, toReturn.data is an empty array. toReturn.offset = toReturn.total; toReturn.data = []; - } - else { - toReturn.data = toReturn.data.sort((a,b) => { + } else { + toReturn.data = toReturn.data.sort((a, b) => { return a[sortOn] - b[sortOn]; }).slice(offset, offset + size).map(a => this.toStoreObject(a)); } - } - else { - let total = yield this.getValuesForHost(host); - toReturn.total = total.length; + } else { + let obj = yield this.getValuesForHost(host, undefined, undefined, + this.hostVsStores); + if (obj.dbs) { + obj = obj.dbs; + } + + toReturn.total = obj.length; if (offset > toReturn.total) { // In this case, toReturn.data is an empty array. toReturn.offset = offset = toReturn.total; toReturn.data = []; - } - else { - toReturn.data = total.sort((a,b) => { + } else { + toReturn.data = obj.sort((a, b) => { return a[sortOn] - b[sortOn]; }).slice(offset, offset + size) .map(object => this.toStoreObject(object)); } } return toReturn; }), { request: { host: Arg(0), names: Arg(1, "nullable:array:string"), options: Arg(2, "nullable:json") }, response: RetVal(storeObjectType) }) - } + }; }; /** * Creates an actor and its corresponding front and registers it to the Storage * Actor. * * @See StorageActors.defaults() * @@ -436,43 +451,54 @@ StorageActors.createActor = function(opt } this.actorID = form.actor; this.hosts = form.hosts; return null; } }); storageTypePool.set(actorObject.typeName, actor); -} +}; /** * The Cookies actor and front. */ StorageActors.createActor({ typeName: "cookies", storeObjectType: "cookiestoreobject" }, { initialize: function(storageActor) { protocol.Actor.prototype.initialize.call(this, null); this.storageActor = storageActor; + this.maybeSetupChildProcess(); this.populateStoresForHosts(); - Services.obs.addObserver(this, "cookie-changed", false); - Services.obs.addObserver(this, "http-on-response-set-cookie", false); + this.addCookieObservers(); + this.onWindowReady = this.onWindowReady.bind(this); this.onWindowDestroyed = this.onWindowDestroyed.bind(this); events.on(this.storageActor, "window-ready", this.onWindowReady); events.on(this.storageActor, "window-destroyed", this.onWindowDestroyed); }, destroy: function() { this.hostVsStores = null; - Services.obs.removeObserver(this, "cookie-changed", false); - Services.obs.removeObserver(this, "http-on-response-set-cookie", false); + + this.removeCookieObservers(); + + // prevents multiple subscriptions on the same messagemanager + let oldMM = gTrackedMessageManager.get("cookies"); + + if (oldMM) { + gTrackedMessageManager.delete("cookies"); + oldMM.removeMessageListener("storage:storage-cookie-request-parent", + cookieHelpers.handleChildRequest); + } + events.off(this.storageActor, "window-ready", this.onWindowReady); events.off(this.storageActor, "window-destroyed", this.onWindowDestroyed); }, /** * Given a cookie object, figure out all the matching hosts from the page that * the cookie belong to. */ @@ -491,210 +517,110 @@ StorageActors.createActor({ return [...hosts]; }, /** * Given a cookie object and a host, figure out if the cookie is valid for * that host. */ isCookieAtHost: function(cookie, host) { - try { - cookie = cookie.QueryInterface(Ci.nsICookie) - .QueryInterface(Ci.nsICookie2); - } catch(ex) { - return false; - } if (cookie.host == null) { return host == null; } if (cookie.host.startsWith(".")) { return host.endsWith(cookie.host); } - else { - return cookie.host == host; - } + return cookie.host == host; }, toStoreObject: function(cookie) { if (!cookie) { return null; } return { name: cookie.name, path: cookie.path || "", host: cookie.host || "", - expires: (cookie.expires || 0) * 1000, // because expires is in seconds - creationTime: cookie.creationTime / 1000, // because it is in micro seconds - lastAccessed: cookie.lastAccessed / 1000, // - do - + + // because expires is in seconds + expires: (cookie.expires || 0) * 1000, + + // because it is in micro seconds + creationTime: cookie.creationTime / 1000, + + // - do - + lastAccessed: cookie.lastAccessed / 1000, value: new LongStringActor(this.conn, cookie.value || ""), isDomain: cookie.isDomain, isSecure: cookie.isSecure, isHttpOnly: cookie.isHttpOnly - } + }; }, populateStoresForHost: function(host) { this.hostVsStores.set(host, new Map()); - let cookies = Services.cookies.getCookiesFromHost(host); - while (cookies.hasMoreElements()) { - let cookie = cookies.getNext().QueryInterface(Ci.nsICookie) - .QueryInterface(Ci.nsICookie2); + + let cookies = this.getCookiesFromHost(host); + + for (let cookie of cookies) { if (this.isCookieAtHost(cookie, host)) { this.hostVsStores.get(host).set(cookie.name, cookie); } } }, /** - * Converts the raw cookie string returned in http request's response header - * to a nsICookie compatible object. - * - * @param {string} cookieString - * The raw cookie string coming from response header. - * @param {string} domain - * The domain of the url of the nsiChannel the cookie corresponds to. - * This will be used when the cookie string does not have a domain. - * - * @returns {[object]} - * An array of nsICookie like objects representing the cookies. - */ - parseCookieString: function(cookieString, domain) { - /** - * Takes a date string present in raw cookie string coming from http - * request's response headers and returns the number of milliseconds elapsed - * since epoch. If the date string is undefined, its probably a session - * cookie so return 0. - */ - let parseDateString = dateString => { - return dateString ? new Date(dateString.replace(/-/g, " ")).getTime(): 0; - }; - - let cookies = []; - for (let string of cookieString.split("\n")) { - let keyVals = {}, name = null; - for (let keyVal of string.split(/;\s*/)) { - let tokens = keyVal.split(/\s*=\s*/); - if (!name) { - name = tokens[0]; - } - else { - tokens[0] = tokens[0].toLowerCase(); - } - keyVals[tokens.splice(0, 1)[0]] = tokens.join("="); - } - let expiresTime = parseDateString(keyVals.expires); - keyVals.domain = keyVals.domain || domain; - cookies.push({ - name: name, - value: keyVals[name] || "", - path: keyVals.path, - host: keyVals.domain, - expires: expiresTime/1000, // seconds, to match with nsiCookie.expires - lastAccessed: expiresTime * 1000, - // microseconds, to match with nsiCookie.lastAccessed - creationTime: expiresTime * 1000, - // microseconds, to match with nsiCookie.creationTime - isHttpOnly: true, - isSecure: keyVals.secure != null, - isDomain: keyVals.domain.startsWith("."), - }); - } - return cookies; - }, - - /** - * Notification observer for topics "http-on-response-set-cookie" and - * "cookie-change". + * Notification observer for "cookie-change". * * @param subject - * {nsiChannel} The channel associated to the SET-COOKIE response - * header in case of "http-on-response-set-cookie" topic. * {nsiCookie|[nsiCookie]} A single nsiCookie object or a list of it * depending on the action. Array is only in case of "batch-deleted" * action. * @param {string} topic * The topic of the notification. * @param {string} action * Additional data associated with the notification. Its the type of - * cookie change in case of "cookie-change" topic and the cookie string - * in case of "http-on-response-set-cookie" topic. + * cookie change in the "cookie-change" topic. */ - observe: function(subject, topic, action) { - if (topic == "http-on-response-set-cookie") { - // Some cookies got created as a result of http response header SET-COOKIE - // subject here is an nsIChannel object referring to the http request. - // We get the requestor of this channel and thus the content window. - let channel = subject.QueryInterface(Ci.nsIChannel); - let requestor = channel.notificationCallbacks || - channel.loadGroup.notificationCallbacks; - // requester can be null sometimes. - let window = requestor ? requestor.getInterface(Ci.nsIDOMWindow): null; - // Proceed only if this window is present on the currently targetted tab - if (window && this.storageActor.isIncludedInTopLevelWindow(window)) { - let host = this.getHostName(window.location); - if (this.hostVsStores.has(host)) { - let cookies = this.parseCookieString(action, channel.URI.host); - let data = {}; - data[host] = []; - for (let cookie of cookies) { - if (this.hostVsStores.get(host).has(cookie.name)) { - continue; - } - this.hostVsStores.get(host).set(cookie.name, cookie); - data[host].push(cookie.name); - } - if (data[host]) { - this.storageActor.update("added", "cookies", data); - } - } - } - return null; - } - - if (topic != "cookie-changed") { + onCookieChanged: function(subject, topic, action) { + if (topic != "cookie-changed" || !this.storageActor.windows) { return null; } let hosts = this.getMatchingHosts(subject); let data = {}; - switch(action) { + switch (action) { case "added": case "changed": if (hosts.length) { - subject = subject.QueryInterface(Ci.nsICookie) - .QueryInterface(Ci.nsICookie2); for (let host of hosts) { this.hostVsStores.get(host).set(subject.name, subject); data[host] = [subject.name]; } this.storageActor.update(action, "cookies", data); } break; case "deleted": if (hosts.length) { - subject = subject.QueryInterface(Ci.nsICookie) - .QueryInterface(Ci.nsICookie2); for (let host of hosts) { this.hostVsStores.get(host).delete(subject.name); data[host] = [subject.name]; } this.storageActor.update("deleted", "cookies", data); } break; case "batch-deleted": if (hosts.length) { for (let host of hosts) { let stores = []; for (let cookie of subject) { - cookie = cookie.QueryInterface(Ci.nsICookie) - .QueryInterface(Ci.nsICookie2); this.hostVsStores.get(host).delete(cookie.name); stores.push(cookie.name); } data[host] = stores; } this.storageActor.update("deleted", "cookies", data); } break; @@ -704,18 +630,172 @@ StorageActors.createActor({ break; case "reload": this.storageActor.update("reloaded", "cookies", hosts); break; } return null; }, + + maybeSetupChildProcess: function() { + cookieHelpers.onCookieChanged = this.onCookieChanged.bind(this); + + if (!DebuggerServer.isInChildProcess) { + this.getCookiesFromHost = cookieHelpers.getCookiesFromHost; + this.addCookieObservers = cookieHelpers.addCookieObservers; + this.removeCookieObservers = cookieHelpers.removeCookieObservers; + return; + } + + const { sendSyncMessage, addMessageListener } = + DebuggerServer.parentMessageManager; + + DebuggerServer.setupInParent({ + module: "devtools/server/actors/storage", + setupParent: "setupParentProcessForCookies" + }); + + this.getCookiesFromHost = + callParentProcess.bind(null, "getCookiesFromHost"); + this.addCookieObservers = + callParentProcess.bind(null, "addCookieObservers"); + this.removeCookieObservers = + callParentProcess.bind(null, "removeCookieObservers"); + + addMessageListener("storage:storage-cookie-request-child", + cookieHelpers.handleParentRequest); + + function callParentProcess(methodName, ...args) { + let reply = sendSyncMessage("storage:storage-cookie-request-parent", { + method: methodName, + args: args + }); + + if (reply.length === 0) { + console.error("ERR_DIRECTOR_CHILD_NO_REPLY from " + methodName); + throw Error("ERR_DIRECTOR_CHILD_NO_REPLY from " + methodName); + } else if (reply.length > 1) { + console.error("ERR_DIRECTOR_CHILD_MULTIPLE_REPLIES from " + methodName); + throw Error("ERR_DIRECTOR_CHILD_MULTIPLE_REPLIES from " + methodName); + } + + let result = reply[0]; + + if (methodName === "getCookiesFromHost") { + return JSON.parse(result); + } + + return result; + } + }, }); +let cookieHelpers = { + getCookiesFromHost: function(host) { + let cookies = Services.cookies.getCookiesFromHost(host); + let store = []; + + while (cookies.hasMoreElements()) { + let cookie = cookies.getNext().QueryInterface(Ci.nsICookie2); + store.push(cookie); + } + + return store; + }, + + addCookieObservers: function() { + Services.obs.addObserver(cookieHelpers, "cookie-changed", false); + return null; + }, + + removeCookieObservers: function() { + Services.obs.removeObserver(cookieHelpers, "cookie-changed", false); + return null; + }, + + observe: function(subject, topic, data) { + switch (topic) { + case "cookie-changed": + let cookie = subject.QueryInterface(Ci.nsICookie2); + cookieHelpers.onCookieChanged(cookie, topic, data); + break; + } + }, + + handleParentRequest: function(msg) { + switch (msg.json.method) { + case "onCookieChanged": + let [cookie, topic, data] = msg.data.args; + cookie = JSON.parse(cookie); + cookieHelpers.onCookieChanged(cookie, topic, data); + break; + } + }, + + handleChildRequest: function(msg) { + switch (msg.json.method) { + case "getCookiesFromHost": + let host = msg.data.args[0]; + let cookies = cookieHelpers.getCookiesFromHost(host); + return JSON.stringify(cookies); + case "addCookieObservers": + return cookieHelpers.addCookieObservers(); + break; + case "removeCookieObservers": + return cookieHelpers.removeCookieObservers(); + return null; + default: + console.error("ERR_DIRECTOR_PARENT_UNKNOWN_METHOD", msg.json.method); + throw new Error("ERR_DIRECTOR_PARENT_UNKNOWN_METHOD"); + } + }, +}; + +/** + * E10S parent/child setup helpers + */ + +exports.setupParentProcessForCookies = function({mm, childID}) { + cookieHelpers.onCookieChanged = + callChildProcess.bind(null, "onCookieChanged"); + + // listen for director-script requests from the child process + mm.addMessageListener("storage:storage-cookie-request-parent", + cookieHelpers.handleChildRequest); + + DebuggerServer.once("disconnected-from-child:" + childID, + handleMessageManagerDisconnected); + + gTrackedMessageManager.set("cookies", mm); + + function handleMessageManagerDisconnected(evt, { mm: disconnected_mm }) { + // filter out not subscribed message managers + if (disconnected_mm !== mm || !gTrackedMessageManager.has(mm)) { + return; + } + + gTrackedMessageManager.delete("cookies"); + + // unregister for director-script requests handlers from the parent process + // (if any) + mm.removeMessageListener("storage:storage-cookie-request-parent", + cookieHelpers.handleChildRequest); + } + + function callChildProcess(methodName, ...args) { + if (methodName === "onCookieChanged") { + args[0] = JSON.stringify(args[0]); + } + let reply = mm.sendAsyncMessage("storage:storage-cookie-request-child", { + method: methodName, + args: args + }); + } +}; /** * Helper method to create the overriden object required in * StorageActors.createActor for Local Storage and Session Storage. * This method exists as both Local Storage and Session Storage have almost * identical actors. */ function getObjectForLocalOrSessionStorage(type) { @@ -725,20 +805,20 @@ function getObjectForLocalOrSessionStora return Object.keys(storage); }, getValuesForHost: function(host, name) { let storage = this.hostVsStores.get(host); if (name) { return [{name: name, value: storage.getItem(name)}]; } - return Object.keys(storage).map(name => { + return Object.keys(storage).map(key => { return { - name: name, - value: storage.getItem(name) + name: key, + value: storage.getItem(key) }; }); }, getHostName: function(location) { if (!location.host) { return location.href; } @@ -753,17 +833,18 @@ function getObjectForLocalOrSessionStora } return null; }, populateStoresForHosts: function() { this.hostVsStores = new Map(); try { for (let window of this.windows) { - this.hostVsStores.set(this.getHostName(window.location), window[type]); + this.hostVsStores.set(this.getHostName(window.location), + window[type]); } } catch(ex) { // Exceptions happen when local or session storage is inaccessible } return null; }, observe: function(subject, topic, data) { @@ -775,48 +856,49 @@ function getObjectForLocalOrSessionStora if (!this.hostVsStores.has(host)) { return null; } let action = "changed"; if (subject.key == null) { return this.storageActor.update("cleared", type, [host]); - } - else if (subject.oldValue == null) { + } else if (subject.oldValue == null) { action = "added"; - } - else if (subject.newValue == null) { + } else if (subject.newValue == null) { action = "deleted"; } let updateData = {}; updateData[host] = [subject.key]; return this.storageActor.update(action, type, updateData); }, /** * Given a url, correctly determine its protocol + hostname part. */ getSchemaAndHost: function(url) { let uri = Services.io.newURI(url, null, null); + if (!uri.host) { + return uri.spec; + } return uri.scheme + "://" + uri.hostPort; }, toStoreObject: function(item) { if (!item) { return null; } return { name: item.name, value: new LongStringActor(this.conn, item.value || "") }; }, - } -}; + }; +} /** * The Local Storage actor and front. */ StorageActors.createActor({ typeName: "localStorage", observationTopic: "dom-storage2-changed", storeObjectType: "storagestoreobject" @@ -826,17 +908,16 @@ StorageActors.createActor({ * The Session Storage actor and front. */ StorageActors.createActor({ typeName: "sessionStorage", observationTopic: "dom-storage2-changed", storeObjectType: "storagestoreobject" }, getObjectForLocalOrSessionStorage("sessionStorage")); - /** * Code related to the Indexed DB actor and front */ // Metadata holder objects for various components of Indexed DB /** * Meta data object for a particular index in an object store @@ -866,21 +947,34 @@ IndexMetadata.prototype = { * * @param {IDBObjectStore} objectStore * The particular object store from the db. */ function ObjectStoreMetadata(objectStore) { this._name = objectStore.name; this._keyPath = objectStore.keyPath; this._autoIncrement = objectStore.autoIncrement; - this._indexes = new Map(); + this._indexes = []; for (let i = 0; i < objectStore.indexNames.length; i++) { let index = objectStore.index(objectStore.indexNames[i]); - this._indexes.set(index, new IndexMetadata(index)); + + let newIndex = { + keypath: index.keyPath, + multiEntry: index.multiEntry, + name: index.name, + objectStore: { + autoIncrement: index.objectStore.autoIncrement, + indexNames: [...index.objectStore.indexNames], + keyPath: index.objectStore.keyPath, + name: index.objectStore.name, + } + }; + + this._indexes.push([newIndex, new IndexMetadata(index)]); } } ObjectStoreMetadata.prototype = { toObject: function() { return { name: this._name, keyPath: this._keyPath, autoIncrement: this._autoIncrement, @@ -898,29 +992,29 @@ ObjectStoreMetadata.prototype = { * The host associated with this indexed db. * @param {IDBDatabase} db * The particular indexed db. */ function DatabaseMetadata(origin, db) { this._origin = origin; this._name = db.name; this._version = db.version; - this._objectStores = new Map(); + this._objectStores = []; if (db.objectStoreNames.length) { let transaction = db.transaction(db.objectStoreNames, "readonly"); for (let i = 0; i < transaction.objectStoreNames.length; i++) { let objectStore = transaction.objectStore(transaction.objectStoreNames[i]); - this._objectStores.set(transaction.objectStoreNames[i], - new ObjectStoreMetadata(objectStore)); + this._objectStores.push([transaction.objectStoreNames[i], + new ObjectStoreMetadata(objectStore)]); } } -}; +} DatabaseMetadata.prototype = { get objectStores() { return this._objectStores; }, toObject: function() { return { name: this._name, @@ -932,139 +1026,70 @@ DatabaseMetadata.prototype = { }; StorageActors.createActor({ typeName: "indexedDB", storeObjectType: "idbstoreobject" }, { initialize: function(storageActor) { protocol.Actor.prototype.initialize.call(this, null); + this.maybeSetupChildProcess(); + this.objectsSize = {}; this.storageActor = storageActor; this.onWindowReady = this.onWindowReady.bind(this); this.onWindowDestroyed = this.onWindowDestroyed.bind(this); + events.on(this.storageActor, "window-ready", this.onWindowReady); events.on(this.storageActor, "window-destroyed", this.onWindowDestroyed); }, destroy: function() { this.hostVsStores = null; this.objectsSize = null; + + // prevents multiple subscriptions on the same messagemanager + let oldMM = gTrackedMessageManager.get("indexedDB"); + + if (oldMM) { + gTrackedMessageManager.delete("indexedDB"); + oldMM.removeMessageListener("storage:storage-cookie-request-parent", + indexedDBHelpers.handleChildRequest); + } + events.off(this.storageActor, "window-ready", this.onWindowReady); events.off(this.storageActor, "window-destroyed", this.onWindowDestroyed); }, getHostName: function(location) { if (!location.host) { return location.href; } return location.protocol + "//" + location.host; }, /** * This method is overriden and left blank as for indexedDB, this operation * cannot be performed synchronously. Thus, the preListStores method exists to * do the same task asynchronously. */ - populateStoresForHosts: function() { - }, + populateStoresForHosts: function() {}, getNamesForHost: function(host) { let names = []; + for (let [dbName, metaData] of this.hostVsStores.get(host)) { for (let objectStore of metaData.objectStores.keys()) { names.push(JSON.stringify([dbName, objectStore])); } } return names; }, /** - * Returns all or requested entries from a particular objectStore from the db - * in the given host. - * - * @param {string} host - * The given host. - * @param {string} dbName - * The name of the indexed db from the above host. - * @param {string} objectStore - * The name of the object store from the above db. - * @param {string} id - * id of the requested entry from the above object store. - * null if all entries from the above object store are requested. - * @param {string} index - * name of the IDBIndex to be iterated on while fetching entries. - * null or "name" if no index is to be iterated. - * @param {number} offset - * ofsset of the entries to be fetched. - * @param {number} size - * The intended size of the entries to be fetched. - */ - getObjectStoreData: - function(host, dbName, objectStore, id, index, offset, size) { - let request = this.openWithOrigin(host, dbName); - let success = Promise.defer(); - let data = []; - if (!size || size > MAX_STORE_OBJECT_COUNT) { - size = MAX_STORE_OBJECT_COUNT; - } - - request.onsuccess = event => { - let db = event.target.result; - - let transaction = db.transaction(objectStore, "readonly"); - let source = transaction.objectStore(objectStore); - if (index && index != "name") { - source = source.index(index); - } - - source.count().onsuccess = event => { - let count = event.target.result; - this.objectsSize[host + dbName + objectStore + index] = count; - - if (!offset) { - offset = 0; - } - else if (offset > count) { - db.close(); - success.resolve([]); - return; - } - - if (id) { - source.get(id).onsuccess = event => { - db.close(); - success.resolve([{name: id, value: event.target.result}]); - }; - } - else { - source.openCursor().onsuccess = event => { - let cursor = event.target.result; - - if (!cursor || data.length >= size) { - db.close(); - success.resolve(data); - return; - } - if (offset-- <= 0) { - data.push({name: cursor.key, value: cursor.value}); - } - cursor.continue(); - }; - } - }; - }; - request.onerror = () => { - db.close(); - success.resolve([]); - }; - return success.promise; - }, - - /** * Returns the total number of entries for various types of requests to * getStoreObjects for Indexed DB actor. * * @param {string} host * The host for the request. * @param {array:string} names * Array of stringified name objects for indexed db actor. * The request type depends on the length of any parsed entry from this @@ -1081,224 +1106,65 @@ StorageActors.createActor({ // should follow in all entries. let name = names[0]; let parsedName = JSON.parse(name); if (parsedName.length == 3) { // This is the case where specific entries from an object store were // requested return names.length; - } - else if (parsedName.length == 2) { + } else if (parsedName.length == 2) { // This is the case where all entries from an object store are requested. let index = options.index; let [db, objectStore] = parsedName; if (this.objectsSize[host + db + objectStore + index]) { return this.objectsSize[host + db + objectStore + index]; } - } - else if (parsedName.length == 1) { + } else if (parsedName.length == 1) { // This is the case where details of all object stores in a db are // requested. if (this.hostVsStores.has(host) && this.hostVsStores.get(host).has(parsedName[0])) { return this.hostVsStores.get(host).get(parsedName[0]).objectStores.size; } - } - else if (!parsedName || !parsedName.length) { + } else if (!parsedName || !parsedName.length) { // This is the case were details of all dbs in a host are requested. if (this.hostVsStores.has(host)) { return this.hostVsStores.get(host).size; } } return 0; }, - getValuesForHost: async(function*(host, name = "null", options) { - name = JSON.parse(name); - if (!name || !name.length) { - // This means that details about the db in this particular host are - // requested. - let dbs = []; - if (this.hostVsStores.has(host)) { - for (let [dbName, db] of this.hostVsStores.get(host)) { - dbs.push(db.toObject()); - } - } - return dbs; - } - let [db, objectStore, id] = name; - if (!objectStore) { - // This means that details about all the object stores in this db are - // requested. - let objectStores = []; - if (this.hostVsStores.has(host) && this.hostVsStores.get(host).has(db)) { - for (let objectStore of this.hostVsStores.get(host).get(db).objectStores) { - objectStores.push(objectStore[1].toObject()); - } - } - return objectStores; - } - // Get either all entries from the object store, or a particular id - return yield this.getObjectStoreData(host, db, objectStore, id, - options.index, options.size); - }), - /** * Purpose of this method is same as populateStoresForHosts but this is async. * This exact same operation cannot be performed in populateStoresForHosts * method, as that method is called in initialize method of the actor, which * cannot be asynchronous. */ preListStores: async(function*() { this.hostVsStores = new Map(); + for (let host of this.hosts) { yield this.populateStoresForHost(host); } }), populateStoresForHost: async(function*(host) { let storeMap = new Map(); - for (let name of (yield this.getDBNamesForHost(host))) { - storeMap.set(name, yield this.getDBMetaData(host, name)); - } - this.hostVsStores.set(host, storeMap); - }), + let {names} = yield this.getDBNamesForHost(host); - /** - * Removes any illegal characters from the host name to make it a valid file - * name. - */ - getSanitizedHost: function(host) { - return host.replace(ILLEGAL_CHAR_REGEX, "+"); - }, + for (let name of names) { + let metadata = yield this.getDBMetaData(host, name); - /** - * Opens an indexed db connection for the given `host` and database `name`. - */ - openWithOrigin: function(host, name) { - let principal; - - if (/^(about:|chrome:)/.test(host)) { - principal = Services.scriptSecurityManager.getSystemPrincipal(); - } - else { - let uri = Services.io.newURI(host, null, null); - principal = Services.scriptSecurityManager.getCodebasePrincipal(uri); + indexedDBHelpers.patchMetadataMapsAndProtos(metadata); + storeMap.set(name, metadata); } - return require("indexedDB").openForPrincipal(principal, name); - }, - - /** - * Fetches and stores all the metadata information for the given database - * `name` for the given `host`. The stored metadata information is of - * `DatabaseMetadata` type. - */ - getDBMetaData: function(host, name) { - let request = this.openWithOrigin(host, name); - let success = Promise.defer(); - request.onsuccess = event => { - let db = event.target.result; - - let dbData = new DatabaseMetadata(host, db); - db.close(); - success.resolve(dbData); - }; - request.onerror = event => { - console.error("Error opening indexeddb database " + name + " for host " + - host); - success.resolve(null); - }; - return success.promise; - }, - - /** - * Retrives the proper indexed db database name from the provided .sqlite file - * location. - */ - getNameFromDatabaseFile: async(function*(path) { - let connection = null; - let retryCount = 0; - - // Content pages might be having an open transaction for the same indexed db - // which this sqlite file belongs to. In that case, sqlite.openConnection - // will throw. Thus we retey for some time to see if lock is removed. - while (!connection && retryCount++ < 25) { - try { - connection = yield Sqlite.openConnection({ path: path }); - } - catch (ex) { - // Continuously retrying is overkill. Waiting for 100ms before next try - yield sleep(100); - } - } - - if (!connection) { - return null; - } - - let rows = yield connection.execute("SELECT name FROM database"); - if (rows.length != 1) { - return null; - } - - let name = rows[0].getResultByName("name"); - - yield connection.close(); - - return name; - }), - - /** - * Fetches all the databases and their metadata for the given `host`. - */ - getDBNamesForHost: async(function*(host) { - let sanitizedHost = this.getSanitizedHost(host); - let directory = OS.Path.join(OS.Constants.Path.profileDir, "storage", - "default", sanitizedHost, "idb"); - - let exists = yield OS.File.exists(directory); - if (!exists && host.startsWith("about:")) { - // try for moz-safe-about directory - sanitizedHost = this.getSanitizedHost("moz-safe-" + host); - directory = OS.Path.join(OS.Constants.Path.profileDir, "storage", - "permanent", sanitizedHost, "idb"); - exists = yield OS.File.exists(directory); - } - if (!exists) { - return []; - } - - let names = []; - let dirIterator = new OS.File.DirectoryIterator(directory); - try { - yield dirIterator.forEach(file => { - // Skip directories. - if (file.isDir) { - return null; - } - - // Skip any non-sqlite files. - if (!file.name.endsWith(".sqlite")) { - return null; - } - - return this.getNameFromDatabaseFile(file.path).then(name => { - if (name) { - names.push(name); - } - return null; - }); - }); - } - finally { - dirIterator.close(); - } - return names; + this.hostVsStores.set(host, storeMap); }), /** * Returns the over-the-wire implementation of the indexed db entity. */ toStoreObject: function(item) { if (!item) { return null; @@ -1339,18 +1205,426 @@ StorageActors.createActor({ hosts[host] = this.getNamesForHost(host); } return { actor: this.actorID, hosts: hosts }; }, + + maybeSetupChildProcess: function() { + if (!DebuggerServer.isInChildProcess) { + this.backToChild = function(...args) { + return args[1]; + }; + this.getDBMetaData = indexedDBHelpers.getDBMetaData; + this.openWithOrigin = indexedDBHelpers.openWithOrigin; + this.getDBNamesForHost = indexedDBHelpers.getDBNamesForHost; + this.getSanitizedHost = indexedDBHelpers.getSanitizedHost; + this.getNameFromDatabaseFile = indexedDBHelpers.getNameFromDatabaseFile; + this.getValuesForHost = indexedDBHelpers.getValuesForHost; + this.getObjectStoreData = indexedDBHelpers.getObjectStoreData; + this.patchMetadataMapsAndProtos = + indexedDBHelpers.patchMetadataMapsAndProtos; + return; + } + + const { sendSyncMessage, addMessageListener } = + DebuggerServer.parentMessageManager; + + DebuggerServer.setupInParent({ + module: "devtools/server/actors/storage", + setupParent: "setupParentProcessForIndexedDB" + }); + + this.getDBMetaData = + callParentProcessAsync.bind(null, "getDBMetaData"); + this.getDBNamesForHost = + callParentProcessAsync.bind(null, "getDBNamesForHost"); + this.getValuesForHost = + callParentProcessAsync.bind(null, "getValuesForHost"); + + addMessageListener("storage:storage-indexedDB-request-child", msg => { + switch (msg.json.method) { + case "backToChild": + let func = msg.json.args.shift(); + let deferred = unresolvedPromises.get(func); + + if (deferred) { + unresolvedPromises.delete(func); + deferred.resolve(msg.json.args[0]); + } + break; + } + }); + + let unresolvedPromises = new Map(); + function callParentProcessAsync(methodName, ...args) { + let deferred = promise.defer(); + + unresolvedPromises.set(methodName, deferred); + + let reply = sendSyncMessage("storage:storage-indexedDB-request-parent", { + method: methodName, + args: args + }); + + return deferred.promise; + } + }, }); +let indexedDBHelpers = { + backToChild: function(...args) { + let mm = Cc["@mozilla.org/globalmessagemanager;1"] + .getService(Ci.nsIMessageListenerManager); + + mm.broadcastAsyncMessage("storage:storage-indexedDB-request-child", { + method: "backToChild", + args: args + }); + }, + + /** + * Fetches and stores all the metadata information for the given database + * `name` for the given `host`. The stored metadata information is of + * `DatabaseMetadata` type. + */ + getDBMetaData: async(function*(host, name) { + let request = this.openWithOrigin(host, name); + let success = promise.defer(); + + request.onsuccess = event => { + let db = event.target.result; + + let dbData = new DatabaseMetadata(host, db); + db.close(); + + this.backToChild("getDBMetaData", dbData); + success.resolve(dbData); + }; + request.onerror = () => { + console.error("Error opening indexeddb database " + name + " for host " + + host); + this.backToChild("getDBMetaData", null); + success.resolve(null); + }; + return success.promise; + }), + + /** + * Opens an indexed db connection for the given `host` and database `name`. + */ + openWithOrigin: function(host, name) { + let principal; + + if (/^(about:|chrome:)/.test(host)) { + principal = Services.scriptSecurityManager.getSystemPrincipal(); + } else { + let uri = Services.io.newURI(host, null, null); + principal = Services.scriptSecurityManager.getCodebasePrincipal(uri); + } + + return require("indexedDB").openForPrincipal(principal, name); + }, + + /** + * Fetches all the databases and their metadata for the given `host`. + */ + getDBNamesForHost: async(function*(host) { + let sanitizedHost = this.getSanitizedHost(host); + let directory = OS.Path.join(OS.Constants.Path.profileDir, "storage", + "default", sanitizedHost, "idb"); + + let exists = yield OS.File.exists(directory); + if (!exists && host.startsWith("about:")) { + // try for moz-safe-about directory + sanitizedHost = this.getSanitizedHost("moz-safe-" + host); + directory = OS.Path.join(OS.Constants.Path.profileDir, "storage", + "permanent", sanitizedHost, "idb"); + exists = yield OS.File.exists(directory); + } + if (!exists) { + return this.backToChild("getDBNamesForHost", {names: []}); + } + + let names = []; + let dirIterator = new OS.File.DirectoryIterator(directory); + try { + yield dirIterator.forEach(file => { + // Skip directories. + if (file.isDir) { + return null; + } + + // Skip any non-sqlite files. + if (!file.name.endsWith(".sqlite")) { + return null; + } + + return this.getNameFromDatabaseFile(file.path).then(name => { + if (name) { + names.push(name); + } + return null; + }); + }); + } finally { + dirIterator.close(); + } + return this.backToChild("getDBNamesForHost", {names: names}); + }), + + /** + * Removes any illegal characters from the host name to make it a valid file + * name. + */ + getSanitizedHost: function(host) { + return host.replace(ILLEGAL_CHAR_REGEX, "+"); + }, + + /** + * Retrieves the proper indexed db database name from the provided .sqlite + * file location. + */ + getNameFromDatabaseFile: async(function*(path) { + let connection = null; + let retryCount = 0; + + // Content pages might be having an open transaction for the same indexed db + // which this sqlite file belongs to. In that case, sqlite.openConnection + // will throw. Thus we retey for some time to see if lock is removed. + while (!connection && retryCount++ < 25) { + try { + connection = yield Sqlite.openConnection({ path: path }); + } catch (ex) { + // Continuously retrying is overkill. Waiting for 100ms before next try + yield sleep(100); + } + } + + if (!connection) { + return null; + } + + let rows = yield connection.execute("SELECT name FROM database"); + if (rows.length != 1) { + return null; + } + + let name = rows[0].getResultByName("name"); + + yield connection.close(); + + return name; + }), + + getValuesForHost: + async(function*(host, name = "null", options, hostVsStores) { + name = JSON.parse(name); + if (!name || !name.length) { + // This means that details about the db in this particular host are + // requested. + let dbs = []; + if (hostVsStores.has(host)) { + for (let [, db] of hostVsStores.get(host)) { + indexedDBHelpers.patchMetadataMapsAndProtos(db); + dbs.push(db.toObject()); + } + } + return this.backToChild("getValuesForHost", {dbs: dbs}); + } + + let [db2, objectStore, id] = name; + if (!objectStore) { + // This means that details about all the object stores in this db are + // requested. + let objectStores = []; + if (hostVsStores.has(host) && hostVsStores.get(host).has(db2)) { + let db = hostVsStores.get(host).get(db2); + + indexedDBHelpers.patchMetadataMapsAndProtos(db); + + let objectStores2 = db.objectStores; + + for (let objectStore2 of objectStores2) { + objectStores.push(objectStore2[1].toObject()); + } + } + return this.backToChild("getValuesForHost", {objectStores: objectStores}); + } + // Get either all entries from the object store, or a particular id + let result = yield this.getObjectStoreData(host, db2, objectStore, id, + options.index, options.size); + return this.backToChild("getValuesForHost", {result: result}); + }), + + /** + * Returns all or requested entries from a particular objectStore from the db + * in the given host. + * + * @param {string} host + * The given host. + * @param {string} dbName + * The name of the indexed db from the above host. + * @param {string} objectStore + * The name of the object store from the above db. + * @param {string} id + * id of the requested entry from the above object store. + * null if all entries from the above object store are requested. + * @param {string} index + * name of the IDBIndex to be iterated on while fetching entries. + * null or "name" if no index is to be iterated. + * @param {number} offset + * ofsset of the entries to be fetched. + * @param {number} size + * The intended size of the entries to be fetched. + */ + getObjectStoreData: + function(host, dbName, objectStore, id, index, offset, size) { + let request = this.openWithOrigin(host, dbName); + let success = promise.defer(); + let data = []; + let db; + + if (!size || size > MAX_STORE_OBJECT_COUNT) { + size = MAX_STORE_OBJECT_COUNT; + } + + request.onsuccess = event => { + db = event.target.result; + + let transaction = db.transaction(objectStore, "readonly"); + let source = transaction.objectStore(objectStore); + if (index && index != "name") { + source = source.index(index); + } + + source.count().onsuccess = event2 => { + let objectsSize = []; + let count = event2.target.result; + objectsSize.push({ + key: host + dbName + objectStore + index, + count: count + }); + + if (!offset) { + offset = 0; + } else if (offset > count) { + db.close(); + success.resolve([]); + return; + } + + if (id) { + source.get(id).onsuccess = event3 => { + db.close(); + success.resolve([{name: id, value: event3.target.result}]); + }; + } else { + source.openCursor().onsuccess = event4 => { + let cursor = event4.target.result; + + if (!cursor || data.length >= size) { + db.close(); + success.resolve({ + data: data, + objectsSize: objectsSize + }); + return; + } + if (offset-- <= 0) { + data.push({name: cursor.key, value: cursor.value}); + } + cursor.continue(); + }; + } + }; + }; + request.onerror = () => { + db.close(); + success.resolve([]); + }; + return success.promise; + }, + + patchMetadataMapsAndProtos: function(metadata) { + for (let [, store] of metadata._objectStores) { + store.__proto__ = ObjectStoreMetadata.prototype; + + if (typeof store._indexes.length !== "undefined") { + store._indexes = new Map(store._indexes); + } + + for (let [, value] of store._indexes) { + value.__proto__ = IndexMetadata.prototype; + } + } + + metadata._objectStores = new Map(metadata._objectStores); + metadata.__proto__ = DatabaseMetadata.prototype; + }, + + handleChildRequest: function(msg) { + let host; + let name; + let args = msg.data.args; + + switch (msg.json.method) { + case "getDBMetaData": + host = args[0]; + name = args[1]; + return indexedDBHelpers.getDBMetaData(host, name); + case "getDBNamesForHost": + host = args[0]; + return indexedDBHelpers.getDBNamesForHost(host); + case "getValuesForHost": + host = args[0]; + name = args[1]; + let options = args[2]; + let hostVsStores = args[3]; + return indexedDBHelpers.getValuesForHost(host, name, options, + hostVsStores); + default: + console.error("ERR_DIRECTOR_PARENT_UNKNOWN_METHOD", msg.json.method); + throw new Error("ERR_DIRECTOR_PARENT_UNKNOWN_METHOD"); + } + } +}; + +/** + * E10S parent/child setup helpers + */ + +exports.setupParentProcessForIndexedDB = function({mm, childID}) { + // listen for director-script requests from the child process + mm.addMessageListener("storage:storage-indexedDB-request-parent", + indexedDBHelpers.handleChildRequest); + + DebuggerServer.once("disconnected-from-child:" + childID, + handleMessageManagerDisconnected); + + gTrackedMessageManager.set("indexedDB", mm); + + function handleMessageManagerDisconnected(evt, { mm: disconnected_mm }) { + // filter out not subscribed message managers + if (disconnected_mm !== mm || !gTrackedMessageManager.has(mm)) { + return; + } + + gTrackedMessageManager.delete("indexedDB"); + + // unregister for director-script requests handlers from the parent process + // (if any) + mm.removeMessageListener("storage:storage-indexedDB-request-parent", + indexedDBHelpers.handleChildRequest); + } +}; + /** * The main Storage Actor. */ let StorageActor = exports.StorageActor = protocol.ActorClass({ typeName: "storage", get window() { return this.parentActor.window; @@ -1382,17 +1656,17 @@ let StorageActor = exports.StorageActor data: Arg(0, "json") }, "stores-reloaded": { type: "storesRelaoded", data: Arg(0, "json") } }, - initialize: function (conn, tabActor) { + initialize: function(conn, tabActor) { protocol.Actor.prototype.initialize.call(this, null); this.conn = conn; this.parentActor = tabActor; this.childActorPool = new Map(); this.childWindowPool = new Set(); @@ -1404,25 +1678,27 @@ let StorageActor = exports.StorageActor this.childActorPool.set(store, new actor(this)); } // Notifications that help us keep track of newly added windows and windows // that got removed Services.obs.addObserver(this, "content-document-global-created", false); Services.obs.addObserver(this, "inner-window-destroyed", false); this.onPageChange = this.onPageChange.bind(this); - tabActor.browser.addEventListener("pageshow", this.onPageChange, true); - tabActor.browser.addEventListener("pagehide", this.onPageChange, true); + + let handler = tabActor.chromeEventHandler; + handler.addEventListener("pageshow", this.onPageChange, true); + handler.addEventListener("pagehide", this.onPageChange, true); this.destroyed = false; this.boundUpdate = {}; // The time which periodically flushes and transfers the updated store // objects. this.updateTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - this.updateTimer.initWithCallback(this , UPDATE_INTERVAL, + this.updateTimer.initWithCallback(this, UPDATE_INTERVAL, Ci.nsITimer.TYPE_REPEATING_PRECISE_CAN_SKIP); // Layout helper for window.parent and window.top helper methods that work // accross devices. this.layoutHelper = new LayoutHelpers(this.window); }, destroy: function() { @@ -1444,17 +1720,17 @@ let StorageActor = exports.StorageActor actor.destroy(); } this.childActorPool.clear(); this.childWindowPool.clear(); this.childWindowPool = this.childActorPool = null; }, /** - * Given a docshell, recursively find otu all the child windows from it. + * Given a docshell, recursively find out all the child windows from it. * * @param {nsIDocShell} item * The docshell from which all inner windows need to be extracted. */ fetchChildWindows: function(item) { let docShell = item.QueryInterface(Ci.nsIDocShell) .QueryInterface(Ci.nsIDocShellTreeItem); if (!docShell.contentViewer) { @@ -1490,27 +1766,27 @@ let StorageActor = exports.StorageActor } return null; }, /** * Event handler for any docshell update. This lets us figure out whenever * any new window is added, or an existing window is removed. */ - observe: function(subject, topic, data) { + observe: function(subject, topic) { if (subject.location && (!subject.location.href || subject.location.href == "about:blank")) { return null; } + if (topic == "content-document-global-created" && this.isIncludedInTopLevelWindow(subject)) { this.childWindowPool.add(subject); events.emit(this, "window-ready", subject); - } - else if (topic == "inner-window-destroyed") { + } else if (topic == "inner-window-destroyed") { let window = this.getWindowFromInnerWindowID(subject); if (window) { this.childWindowPool.delete(window); events.emit(this, "window-destroyed", window); } } return null; }, @@ -1526,57 +1802,61 @@ let StorageActor = exports.StorageActor * - persisted {boolean} true if there was no * "content-document-global-created" notification along * this event. */ onPageChange: function({target, type, persisted}) { if (this.destroyed) { return; } + let window = target.defaultView; + if (type == "pagehide" && this.childWindowPool.delete(window)) { - events.emit(this, "window-destroyed", window) - } - else if (type == "pageshow" && persisted && window.location.href && - window.location.href != "about:blank" && - this.isIncludedInTopLevelWindow(window)) { + events.emit(this, "window-destroyed", window); + } else if (type == "pageshow" && persisted && window.location.href && + window.location.href != "about:blank" && + this.isIncludedInTopLevelWindow(window)) { this.childWindowPool.add(window); events.emit(this, "window-ready", window); } }, /** * Lists the available hosts for all the registered storage types. * * @returns {object} An object containing with the following structure: * - <storageType> : [{ * actor: <actorId>, * host: <hostname> * }] */ listStores: method(async(function*() { let toReturn = {}; + for (let [name, value] of this.childActorPool) { if (value.preListStores) { yield value.preListStores(); } toReturn[name] = value; } + return toReturn; }), { response: RetVal(types.addDictType("storelist", getRegisteredTypes())) }), /** * Notifies the client front with the updates in stores at regular intervals. */ notify: function() { if (!this.updatePending || this.updatingUpdateObject) { return null; } + events.emit(this, "stores-update", this.boundUpdate); this.boundUpdate = {}; this.updatePending = false; return null; }, /** * This method is called by the registered storage types so as to tell the @@ -1598,52 +1878,49 @@ let StorageActor = exports.StorageActor * [<store_namesX] is an array of the names of the changed store * objects. Leave it empty if the host was completely removed. * When the action is "reloaded" or "cleared", `data` is an array of * hosts for which the stores were cleared or reloaded. */ update: function(action, storeType, data) { if (action == "cleared" || action == "reloaded") { let toSend = {}; - toSend[storeType] = data + toSend[storeType] = data; events.emit(this, "stores-" + action, toSend); return null; } this.updatingUpdateObject = true; if (!this.boundUpdate[action]) { this.boundUpdate[action] = {}; } if (!this.boundUpdate[action][storeType]) { this.boundUpdate[action][storeType] = {}; } this.updatePending = true; for (let host in data) { if (!this.boundUpdate[action][storeType][host] || action == "deleted") { this.boundUpdate[action][storeType][host] = data[host]; - } - else { + } else { this.boundUpdate[action][storeType][host] = this.boundUpdate[action][storeType][host].concat(data[host]); } } if (action == "added") { // If the same store name was previously deleted or changed, but now is // added somehow, dont send the deleted or changed update. this.removeNamesFromUpdateList("deleted", storeType, data); this.removeNamesFromUpdateList("changed", storeType, data); - } - else if (action == "changed" && this.boundUpdate.added && + } else if (action == "changed" && this.boundUpdate.added && this.boundUpdate.added[storeType]) { // If something got added and changed at the same time, then remove those // items from changed instead. this.removeNamesFromUpdateList("changed", storeType, this.boundUpdate.added[storeType]); - } - else if (action == "deleted") { + } else if (action == "deleted") { // If any item got delete, or a host got delete, no point in sending // added or changed update this.removeNamesFromUpdateList("added", storeType, data); this.removeNamesFromUpdateList("changed", storeType, data); for (let host in data) { if (data[host].length == 0 && this.boundUpdate.added && this.boundUpdate.added[storeType] && this.boundUpdate.added[storeType][host]) {