Bug 1666534 - [devtools] Listen to extension storages via a server side watcher. r=devtools-reviewers,nchevobbe,jdescottes

This was the last resource type requiring to keep the old storage actor as-is.
This will help drastically simplify it and move storage type code into each Resource Watcher class.

Differential Revision: https://phabricator.services.mozilla.com/D166661
This commit is contained in:
Alexandre Poirot 2023-01-29 20:50:47 +00:00
parent 7539d4e3f4
commit 94e02496ef
14 changed files with 191 additions and 307 deletions

View file

@ -315,17 +315,22 @@ class StorageUI {
this._onResourceListAvailable = this._onResourceListAvailable.bind(this); this._onResourceListAvailable = this._onResourceListAvailable.bind(this);
const { resourceCommand } = this._toolbox; const { resourceCommand } = this._toolbox;
await this._toolbox.resourceCommand.watchResources(
[ this._listenedResourceTypes = [
// The first item in this list will be the first selected storage item // The first item in this list will be the first selected storage item
// Tests assume Cookie -- moving cookie will break tests // Tests assume Cookie -- moving cookie will break tests
resourceCommand.TYPES.COOKIE, resourceCommand.TYPES.COOKIE,
resourceCommand.TYPES.CACHE_STORAGE, resourceCommand.TYPES.CACHE_STORAGE,
resourceCommand.TYPES.EXTENSION_STORAGE,
resourceCommand.TYPES.INDEXED_DB, resourceCommand.TYPES.INDEXED_DB,
resourceCommand.TYPES.LOCAL_STORAGE, resourceCommand.TYPES.LOCAL_STORAGE,
resourceCommand.TYPES.SESSION_STORAGE, resourceCommand.TYPES.SESSION_STORAGE,
], ];
// EXTENSION_STORAGE is only relevant when debugging web extensions
if (this._commands.descriptorFront.isWebExtensionDescriptor) {
this._listenedResourceTypes.push(resourceCommand.TYPES.EXTENSION_STORAGE);
}
await this._toolbox.resourceCommand.watchResources(
this._listenedResourceTypes,
{ {
onAvailable: this._onResourceListAvailable, onAvailable: this._onResourceListAvailable,
} }
@ -443,19 +448,9 @@ class StorageUI {
this._destroyed = true; this._destroyed = true;
const { resourceCommand } = this._toolbox; const { resourceCommand } = this._toolbox;
resourceCommand.unwatchResources( resourceCommand.unwatchResources(this._listenedResourceTypes, {
[
resourceCommand.TYPES.COOKIE,
resourceCommand.TYPES.CACHE_STORAGE,
resourceCommand.TYPES.EXTENSION_STORAGE,
resourceCommand.TYPES.INDEXED_DB,
resourceCommand.TYPES.LOCAL_STORAGE,
resourceCommand.TYPES.SESSION_STORAGE,
],
{
onAvailable: this._onResourceListAvailable, onAvailable: this._onResourceListAvailable,
} });
);
this.table.off(TableWidget.EVENTS.ROW_SELECTED, this.updateObjectSidebar); this.table.off(TableWidget.EVENTS.ROW_SELECTED, this.updateObjectSidebar);
this.table.off(TableWidget.EVENTS.SCROLL_END, this.loadMoreItems); this.table.off(TableWidget.EVENTS.SCROLL_END, this.loadMoreItems);

View file

@ -26,6 +26,7 @@ const TYPES = {
// storage types // storage types
CACHE_STORAGE: "Cache", CACHE_STORAGE: "Cache",
COOKIE: "cookies", COOKIE: "cookies",
EXTENSION_STORAGE: "extension-storage",
INDEXED_DB: "indexed-db", INDEXED_DB: "indexed-db",
LOCAL_STORAGE: "local-storage", LOCAL_STORAGE: "local-storage",
SESSION_STORAGE: "session-storage", SESSION_STORAGE: "session-storage",
@ -160,6 +161,9 @@ const ParentProcessResources = augmentResourceDictionary({
[TYPES.COOKIE]: { [TYPES.COOKIE]: {
path: "devtools/server/actors/resources/storage-cookie", path: "devtools/server/actors/resources/storage-cookie",
}, },
[TYPES.EXTENSION_STORAGE]: {
path: "devtools/server/actors/resources/storage-extension",
},
[TYPES.INDEXED_DB]: { [TYPES.INDEXED_DB]: {
path: "devtools/server/actors/resources/storage-indexed-db", path: "devtools/server/actors/resources/storage-indexed-db",
}, },

View file

@ -27,6 +27,7 @@ DevToolsModules(
"sources.js", "sources.js",
"storage-cache.js", "storage-cache.js",
"storage-cookie.js", "storage-cookie.js",
"storage-extension.js",
"storage-indexed-db.js", "storage-indexed-db.js",
"storage-local-storage.js", "storage-local-storage.js",
"storage-session-storage.js", "storage-session-storage.js",

View file

@ -0,0 +1,27 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {
TYPES: { EXTENSION_STORAGE },
} = require("resource://devtools/server/actors/resources/index.js");
const ParentProcessStorage = require("resource://devtools/server/actors/resources/utils/parent-process-storage.js");
class ExtensionStorageWatcher extends ParentProcessStorage {
constructor() {
super("extensionStorage", EXTENSION_STORAGE);
}
async watch(watcherActor, { onAvailable }) {
if (watcherActor.sessionContext.type != "webextension") {
throw new Error(
"EXTENSION_STORAGE should only be listened when debugging a webextension"
);
}
return super.watch(watcherActor, { onAvailable });
}
}
module.exports = ExtensionStorageWatcher;

View file

@ -407,7 +407,10 @@ class StorageActorMock extends EventEmitter {
} }
get parentActor() { get parentActor() {
return { isRootActor: this.watcherActor.sessionContext.type == "all" }; return {
isRootActor: this.watcherActor.sessionContext.type == "all",
addonId: this.watcherActor.sessionContext.addonId,
};
} }
/** /**

View file

@ -1488,14 +1488,6 @@ StorageActors.createActor(
); );
const extensionStorageHelpers = { const extensionStorageHelpers = {
unresolvedPromises: new Map(),
// Map of addonId => onStorageChange listeners in the parent process. Each addon toolbox targets
// a single addon, and multiple addon toolboxes could be open at the same time.
onChangedParentListeners: new Map(),
// Set of onStorageChange listeners in the extension child process. Each addon toolbox will create
// a separate extensionStorage actor targeting that addon. The addonId is passed into the listener,
// so that changes propagate only if the storage actor has a matching addonId.
onChangedChildListeners: new Set(),
/** /**
* Editing is supported only for serializable types. Examples of unserializable * Editing is supported only for serializable types. Examples of unserializable
* types include Map, Set and ArrayBuffer. * types include Map, Set and ArrayBuffer.
@ -1610,172 +1602,6 @@ const extensionStorageHelpers = {
}, },
}, },
}, },
// Sets the parent process message manager
setPpmm(ppmm) {
this.ppmm = ppmm;
},
// A promise in the main process has resolved, and we need to pass the return value(s)
// back to the child process
backToChild(...args) {
Services.mm.broadcastAsyncMessage(
"debug:storage-extensionStorage-request-child",
{
method: "backToChild",
args,
}
);
},
// Send a message from the main process to a listener in the child process that the
// extension has modified storage local data
fireStorageOnChanged({ addonId, changes }) {
Services.mm.broadcastAsyncMessage(
"debug:storage-extensionStorage-request-child",
{
addonId,
changes,
method: "storageOnChanged",
}
);
},
// Subscribe a listener for event notifications from the WE storage API when
// storage local data has been changed by the extension, and keep track of the
// listener to remove it when the debugger is being disconnected.
subscribeOnChangedListenerInParent(addonId) {
if (!this.onChangedParentListeners.has(addonId)) {
const onChangedListener = changes => {
this.fireStorageOnChanged({ addonId, changes });
};
ExtensionStorageIDB.addOnChangedListener(addonId, onChangedListener);
this.onChangedParentListeners.set(addonId, onChangedListener);
}
},
// The main process does not require an extension context to select the backend
// Bug 1542038, 1542039: Each storage area will need its own implementation, as
// they use different storage backends.
async setupStorageInParent(addonId) {
const { extension } = WebExtensionPolicy.getByID(addonId);
try {
// Make sure the extension storage APIs have been loaded,
// otherwise the DevTools storage panel would not be updated
// automatically when the extension storage data is being changed
// if the parent ext-storage.js module wasn't already loaded
// (See Bug 1802929).
await extension.apiManager.asyncGetAPI("storage", extension);
} catch (err) {
console.error(err);
}
const parentResult = await ExtensionStorageIDB.selectBackend({ extension });
const result = {
...parentResult,
// Received as a StructuredCloneHolder, so we need to deserialize
storagePrincipal: parentResult.storagePrincipal.deserialize(this, true),
};
this.subscribeOnChangedListenerInParent(addonId);
return this.backToChild("setupStorageInParent", result);
},
onDisconnected() {
for (const [addonId, listener] of this.onChangedParentListeners) {
ExtensionStorageIDB.removeOnChangedListener(addonId, listener);
}
this.onChangedParentListeners.clear();
},
// Runs in the main process. This determines what code to execute based on the message
// received from the child process.
async handleChildRequest(msg) {
switch (msg.json.method) {
case "setupStorageInParent":
const addonId = msg.data.args[0];
const result = await extensionStorageHelpers.setupStorageInParent(
addonId
);
return result;
default:
console.error("ERR_DIRECTOR_PARENT_UNKNOWN_METHOD", msg.json.method);
throw new Error("ERR_DIRECTOR_PARENT_UNKNOWN_METHOD");
}
},
// Runs in the child process. This determines what code to execute based on the message
// received from the parent process.
handleParentRequest(msg) {
switch (msg.json.method) {
case "backToChild": {
const [func, rv] = msg.json.args;
const resolve = this.unresolvedPromises.get(func);
if (resolve) {
this.unresolvedPromises.delete(func);
resolve(rv);
}
break;
}
case "storageOnChanged": {
const { addonId, changes } = msg.data;
for (const listener of this.onChangedChildListeners) {
try {
listener({ addonId, changes });
} catch (err) {
console.error(err);
// Ignore errors raised from listeners.
}
}
break;
}
default:
console.error("ERR_DIRECTOR_CLIENT_UNKNOWN_METHOD", msg.json.method);
throw new Error("ERR_DIRECTOR_CLIENT_UNKNOWN_METHOD");
}
},
callParentProcessAsync(methodName, ...args) {
const promise = new Promise(resolve => {
this.unresolvedPromises.set(methodName, resolve);
});
this.ppmm.sendAsyncMessage(
"debug:storage-extensionStorage-request-parent",
{
method: methodName,
args,
}
);
return promise;
},
};
/**
* E10S parent/child setup helpers
* Add a message listener in the parent process to receive messages from the child
* process.
*/
exports.setupParentProcessForExtensionStorage = function({ mm, prefix }) {
// listen for director-script requests from the child process
mm.addMessageListener(
"debug:storage-extensionStorage-request-parent",
extensionStorageHelpers.handleChildRequest
);
return {
onDisconnected: () => {
// Although "disconnected-from-child" implies that the child is already
// disconnected this is not the case. The disconnection takes place after
// this method has finished. This gives us chance to clean up items within
// the parent process e.g. observers.
mm.removeMessageListener(
"debug:storage-extensionStorage-request-parent",
extensionStorageHelpers.handleChildRequest
);
extensionStorageHelpers.onDisconnected();
},
};
}; };
/** /**
@ -1815,8 +1641,6 @@ StorageActors.createActor(
this.onStorageChange = this.onStorageChange.bind(this); this.onStorageChange = this.onStorageChange.bind(this);
this.setupChildProcess();
this.onWindowReady = this.onWindowReady.bind(this); this.onWindowReady = this.onWindowReady.bind(this);
this.onWindowDestroyed = this.onWindowDestroyed.bind(this); this.onWindowDestroyed = this.onWindowDestroyed.bind(this);
this.storageActor.on("window-ready", this.onWindowReady); this.storageActor.on("window-ready", this.onWindowReady);
@ -1828,7 +1652,8 @@ StorageActors.createActor(
}, },
destroy() { destroy() {
extensionStorageHelpers.onChangedChildListeners.delete( ExtensionStorageIDB.removeOnChangedListener(
this.addonId,
this.onStorageChange this.onStorageChange
); );
@ -1841,42 +1666,12 @@ StorageActors.createActor(
this.storageActor = null; this.storageActor = null;
}, },
setupChildProcess() {
const ppmm = this.conn.parentMessageManager;
extensionStorageHelpers.setPpmm(ppmm);
// eslint-disable-next-line no-restricted-properties
this.conn.setupInParent({
module: "devtools/server/actors/storage",
setupParent: "setupParentProcessForExtensionStorage",
});
extensionStorageHelpers.onChangedChildListeners.add(this.onStorageChange);
this.setupStorageInParent = extensionStorageHelpers.callParentProcessAsync.bind(
extensionStorageHelpers,
"setupStorageInParent"
);
// Add a message listener in the child process to receive messages from the parent
// process
ppmm.addMessageListener(
"debug:storage-extensionStorage-request-child",
extensionStorageHelpers.handleParentRequest.bind(
extensionStorageHelpers
)
);
},
/** /**
* This fires when the extension changes storage data while the storage * This fires when the extension changes storage data while the storage
* inspector is open. Ensures this.hostVsStores stays up-to-date and * inspector is open. Ensures this.hostVsStores stays up-to-date and
* passes the changes on to update the client. * passes the changes on to update the client.
*/ */
onStorageChange({ addonId, changes }) { onStorageChange(changes) {
if (addonId !== this.addonId) {
return;
}
const host = this.extensionHostURL; const host = this.extensionHostURL;
const storeMap = this.hostVsStores.get(host); const storeMap = this.hostVsStores.get(host);
@ -1922,10 +1717,32 @@ StorageActors.createActor(
*/ */
async preListStores() { async preListStores() {
// Ensure the actor's target is an extension and it is enabled // Ensure the actor's target is an extension and it is enabled
if (!this.addonId || !WebExtensionPolicy.getByID(this.addonId)) { if (!this.addonId || !this.getExtensionPolicy()) {
return; return;
} }
// Subscribe a listener for event notifications from the WE storage API when
// storage local data has been changed by the extension, and keep track of the
// listener to remove it when the debugger is being disconnected.
ExtensionStorageIDB.addOnChangedListener(
this.addonId,
this.onStorageChange
);
try {
// Make sure the extension storage APIs have been loaded,
// otherwise the DevTools storage panel would not be updated
// automatically when the extension storage data is being changed
// if the parent ext-storage.js module wasn't already loaded
// (See Bug 1802929).
const { extension } = WebExtensionPolicy.getByID(this.addonId);
await extension.apiManager.asyncGetAPI("storage", extension);
} catch (e) {
console.error(
"Exception while trying to initialize webext storage API",
e
);
}
await this.populateStoresForHost(this.extensionHostURL); await this.populateStoresForHost(this.extensionHostURL);
}, },
@ -1956,7 +1773,7 @@ StorageActors.createActor(
const storeMap = new Map(); const storeMap = new Map();
this.hostVsStores.set(host, storeMap); this.hostVsStores.set(host, storeMap);
const storagePrincipal = await this.getStoragePrincipal(extension.id); const storagePrincipal = await this.getStoragePrincipal();
if (!storagePrincipal) { if (!storagePrincipal) {
// This could happen if the extension fails to be migrated to the // This could happen if the extension fails to be migrated to the
@ -1982,17 +1799,20 @@ StorageActors.createActor(
} }
}, },
async getStoragePrincipal(addonId) { async getStoragePrincipal() {
const { extension } = this.getExtensionPolicy();
const { const {
backendEnabled, backendEnabled,
storagePrincipal, storagePrincipal,
} = await this.setupStorageInParent(addonId); } = await ExtensionStorageIDB.selectBackend({ extension });
if (!backendEnabled) { if (!backendEnabled) {
// IDB backend disabled; give up. // IDB backend disabled; give up.
return null; return null;
} }
return storagePrincipal;
// Received as a StructuredCloneHolder, so we need to deserialize
return storagePrincipal.deserialize(this, true);
}, },
getValuesForHost(host, name) { getValuesForHost(host, name) {

View file

@ -188,6 +188,7 @@ function getWatcherSupportedResources(type) {
[Resources.TYPES.CACHE_STORAGE]: true, [Resources.TYPES.CACHE_STORAGE]: true,
[Resources.TYPES.COOKIE]: true, [Resources.TYPES.COOKIE]: true,
[Resources.TYPES.ERROR_MESSAGE]: true, [Resources.TYPES.ERROR_MESSAGE]: true,
[Resources.TYPES.EXTENSION_STORAGE]: true,
[Resources.TYPES.INDEXED_DB]: true, [Resources.TYPES.INDEXED_DB]: true,
[Resources.TYPES.LOCAL_STORAGE]: true, [Resources.TYPES.LOCAL_STORAGE]: true,
[Resources.TYPES.SESSION_STORAGE]: true, [Resources.TYPES.SESSION_STORAGE]: true,

View file

@ -60,13 +60,13 @@ add_setup(async function setup() {
add_task(async function test_extension_store_exists() { add_task(async function test_extension_store_exists() {
const extension = await startupExtension(getExtensionConfig()); const extension = await startupExtension(getExtensionConfig());
const { target, extensionStorage } = await openAddonStoragePanel( const { commands, extensionStorage } = await openAddonStoragePanel(
extension.id extension.id
); );
ok(extensionStorage, "Should have an extensionStorage store"); ok(extensionStorage, "Should have an extensionStorage store");
await shutdown(extension, target); await shutdown(extension, commands);
}); });
add_task( add_task(
@ -86,7 +86,7 @@ add_task(
getExtensionConfig({ background }) getExtensionConfig({ background })
); );
const { target, extensionStorage } = await openAddonStoragePanel( const { commands, extensionStorage } = await openAddonStoragePanel(
extension.id extension.id
); );
@ -97,7 +97,7 @@ add_task(
"Should have the expected extension host in the extensionStorage store" "Should have the expected extension host in the extensionStorage store"
); );
await shutdown(extension, target); await shutdown(extension, commands);
} }
); );
@ -118,7 +118,7 @@ add_task(async function test_panel_live_updates() {
getExtensionConfig({ background: extensionScriptWithMessageListener }) getExtensionConfig({ background: extensionScriptWithMessageListener })
); );
const { target, extensionStorage } = await openAddonStoragePanel( const { commands, extensionStorage } = await openAddonStoragePanel(
extension.id extension.id
); );
@ -319,7 +319,7 @@ add_task(async function test_panel_live_updates() {
"Got the expected results on populated storage.local" "Got the expected results on populated storage.local"
); );
await shutdown(extension, target); await shutdown(extension, commands);
}); });
/** /**
@ -347,7 +347,7 @@ add_task(
extension.sendMessage("storage-local-set", { a: 123 }); extension.sendMessage("storage-local-set", { a: 123 });
await extension.awaitMessage("storage-local-set:done"); await extension.awaitMessage("storage-local-set:done");
const { target, extensionStorage } = await openAddonStoragePanel( const { commands, extensionStorage } = await openAddonStoragePanel(
extension.id extension.id
); );
@ -366,7 +366,7 @@ add_task(
); );
await contentPage.close(); await contentPage.close();
await shutdown(extension, target); await shutdown(extension, commands);
} }
); );
@ -396,7 +396,7 @@ add_task(async function test_panel_data_matches_extension_with_no_pages_open() {
await extension.awaitMessage("storage-local-onChanged"); await extension.awaitMessage("storage-local-onChanged");
await contentPage.close(); await contentPage.close();
const { target, extensionStorage } = await openAddonStoragePanel( const { commands, extensionStorage } = await openAddonStoragePanel(
extension.id extension.id
); );
@ -414,7 +414,7 @@ add_task(async function test_panel_data_matches_extension_with_no_pages_open() {
"Got the expected results on populated storage.local" "Got the expected results on populated storage.local"
); );
await shutdown(extension, target); await shutdown(extension, commands);
}); });
/** /**
@ -434,7 +434,7 @@ add_task(
getExtensionConfig({ files: ext_no_bg.files }) getExtensionConfig({ files: ext_no_bg.files })
); );
const { target, extensionStorage } = await openAddonStoragePanel( const { commands, extensionStorage } = await openAddonStoragePanel(
extension.id extension.id
); );
@ -493,7 +493,7 @@ add_task(
); );
await contentPage.close(); await contentPage.close();
await shutdown(extension, target); await shutdown(extension, commands);
} }
); );
@ -516,7 +516,7 @@ add_task(
const host = await extension.awaitMessage("extension-origin"); const host = await extension.awaitMessage("extension-origin");
const { target, extensionStorage } = await openAddonStoragePanel( const { commands, extensionStorage } = await openAddonStoragePanel(
extension.id extension.id
); );
@ -588,7 +588,7 @@ add_task(
); );
} }
await shutdown(extension, target); await shutdown(extension, commands);
} }
); );
@ -610,11 +610,9 @@ add_task(
const host = await extension.awaitMessage("extension-origin"); const host = await extension.awaitMessage("extension-origin");
const { const { commands, extensionStorage } = await openAddonStoragePanel(
target, extension.id
extensionStorage, );
storageFront,
} = await openAddonStoragePanel(extension.id);
const DEFAULT_VALUE = "value"; // global in devtools/server/actors/storage.js const DEFAULT_VALUE = "value"; // global in devtools/server/actors/storage.js
let items = { let items = {
@ -624,7 +622,7 @@ add_task(
}; };
info("Adding storage items from the extension"); info("Adding storage items from the extension");
let storesUpdate = storageFront.once("stores-update"); let storesUpdate = extensionStorage.once("single-store-update");
extension.sendMessage("storage-local-set", items); extension.sendMessage("storage-local-set", items);
await extension.awaitMessage("storage-local-set:done"); await extension.awaitMessage("storage-local-set:done");
@ -637,13 +635,15 @@ add_task(
[host]: ["guid_1", "guid_2", "guid_3"], [host]: ["guid_1", "guid_2", "guid_3"],
}, },
}, },
changed: undefined,
deleted: undefined,
}, },
data, data,
"The change data from the storage actor's 'stores-update' event matches the changes made in the client." "The change data from the storage actor's 'stores-update' event matches the changes made in the client."
); );
info("Waiting for panel to edit some items"); info("Waiting for panel to edit some items");
storesUpdate = storageFront.once("stores-update"); storesUpdate = extensionStorage.once("single-store-update");
await extensionStorage.editItem({ await extensionStorage.editItem({
host, host,
field: "value", field: "value",
@ -655,11 +655,13 @@ add_task(
data = await storesUpdate; data = await storesUpdate;
Assert.deepEqual( Assert.deepEqual(
{ {
added: undefined,
changed: { changed: {
extensionStorage: { extensionStorage: {
[host]: ["guid_1"], [host]: ["guid_1"],
}, },
}, },
deleted: undefined,
}, },
data, data,
"The change data from the storage actor's 'stores-update' event matches the changes made in the client." "The change data from the storage actor's 'stores-update' event matches the changes made in the client."
@ -679,13 +681,15 @@ add_task(
); );
info("Waiting for panel to remove an item"); info("Waiting for panel to remove an item");
storesUpdate = storageFront.once("stores-update"); storesUpdate = extensionStorage.once("single-store-update");
await extensionStorage.removeItem(host, "guid_3"); await extensionStorage.removeItem(host, "guid_3");
info("Waiting for the storage actor to emit a 'stores-update' event"); info("Waiting for the storage actor to emit a 'stores-update' event");
data = await storesUpdate; data = await storesUpdate;
Assert.deepEqual( Assert.deepEqual(
{ {
added: undefined,
changed: undefined,
deleted: { deleted: {
extensionStorage: { extensionStorage: {
[host]: ["guid_3"], [host]: ["guid_3"],
@ -709,14 +713,14 @@ add_task(
); );
info("Waiting for panel to remove all items"); info("Waiting for panel to remove all items");
const storesCleared = storageFront.once("stores-cleared"); const storesCleared = extensionStorage.once("single-store-cleared");
await extensionStorage.removeAll(host); await extensionStorage.removeAll(host);
info("Waiting for the storage actor to emit a 'stores-cleared' event"); info("Waiting for the storage actor to emit a 'stores-cleared' event");
data = await storesCleared; data = await storesCleared;
Assert.deepEqual( Assert.deepEqual(
{ {
extensionStorage: { clearedHostsOrPaths: {
[host]: [], [host]: [],
}, },
}, },
@ -733,7 +737,7 @@ add_task(
`The storage items in the extension match the items in the panel` `The storage items in the extension match the items in the panel`
); );
await shutdown(extension, target); await shutdown(extension, commands);
} }
); );
@ -780,7 +784,7 @@ add_task(
await extension.awaitMessage("extension-origin"); await extension.awaitMessage("extension-origin");
const { target, extensionStorage } = await openAddonStoragePanel( const { commands, extensionStorage } = await openAddonStoragePanel(
extension.id extension.id
); );
@ -829,7 +833,7 @@ add_task(
Services.prefs.setBoolPref(LEAVE_STORAGE_PREF, false); Services.prefs.setBoolPref(LEAVE_STORAGE_PREF, false);
Services.prefs.setBoolPref(LEAVE_UUID_PREF, false); Services.prefs.setBoolPref(LEAVE_UUID_PREF, false);
await shutdown(extension, target); await shutdown(extension, commands);
} }
); );
@ -884,7 +888,7 @@ add_task(async function test_panel_live_reload_for_extension_without_bg_page() {
await contentPage.close(); await contentPage.close();
info("Opening storage panel"); info("Opening storage panel");
const { target, extensionStorage } = await openAddonStoragePanel( const { commands, extensionStorage } = await openAddonStoragePanel(
extension.id extension.id
); );
@ -915,7 +919,7 @@ add_task(async function test_panel_live_reload_for_extension_without_bg_page() {
"Got the expected results on populated storage.local" "Got the expected results on populated storage.local"
); );
await shutdown(extension, target); await shutdown(extension, commands);
}); });
/** /**
@ -953,7 +957,7 @@ add_task(
const host = await extension.awaitMessage("extension-origin"); const host = await extension.awaitMessage("extension-origin");
info("Opening storage panel"); info("Opening storage panel");
const { target, extensionStorage } = await openAddonStoragePanel( const { commands, extensionStorage } = await openAddonStoragePanel(
extension.id extension.id
); );
@ -994,7 +998,7 @@ add_task(
"Got the expected results on populated storage.local" "Got the expected results on populated storage.local"
); );
await shutdown(extension, target); await shutdown(extension, commands);
} }
); );
@ -1034,7 +1038,7 @@ add_task(
const host = await extension.awaitMessage("extension-origin"); const host = await extension.awaitMessage("extension-origin");
info("Opening storage panel"); info("Opening storage panel");
const { target, extensionStorage } = await openAddonStoragePanel( const { commands, extensionStorage } = await openAddonStoragePanel(
extension.id extension.id
); );
@ -1052,7 +1056,7 @@ add_task(
"Got the expected results on populated storage.local" "Got the expected results on populated storage.local"
); );
await shutdown(extension, target); await shutdown(extension, commands);
} }
); );

View file

@ -83,11 +83,9 @@ add_task(async function test_panel_live_reload() {
extension.sendMessage("storage-local-set", { a: 123 }); extension.sendMessage("storage-local-set", { a: 123 });
await extension.awaitMessage("storage-local-set:done"); await extension.awaitMessage("storage-local-set:done");
const { const { commands, extensionStorage } = await openAddonStoragePanel(
target, extension.id
extensionStorage, );
storageFront,
} = await openAddonStoragePanel(extension.id);
manifest = { manifest = {
...manifest, ...manifest,
@ -99,11 +97,13 @@ add_task(async function test_panel_live_reload() {
// Wait for the storage front to receive an event for the storage panel refresh // Wait for the storage front to receive an event for the storage panel refresh
// when the extension has been reloaded. // when the extension has been reloaded.
const promiseStoragePanelUpdated = new Promise(resolve => { const promiseStoragePanelUpdated = new Promise(resolve => {
storageFront.on("stores-update", function updateListener(updates) { extensionStorage.on("single-store-update", function updateListener(
updates
) {
info(`Got stores-update event: ${JSON.stringify(updates)}`); info(`Got stores-update event: ${JSON.stringify(updates)}`);
const extStorageAdded = updates.added?.extensionStorage; const extStorageAdded = updates.added?.extensionStorage;
if (host in extStorageAdded && extStorageAdded[host].length) { if (host in extStorageAdded && extStorageAdded[host].length) {
storageFront.off("stores-update", updateListener); extensionStorage.off("single-store-update", updateListener);
resolve(); resolve();
} }
}); });
@ -137,5 +137,5 @@ add_task(async function test_panel_live_reload() {
"Got the expected results on populated storage.local" "Got the expected results on populated storage.local"
); );
await shutdown(extension, target); await shutdown(extension, commands);
}); });

View file

@ -20,21 +20,6 @@ const {
CommandsFactory, CommandsFactory,
} = require("resource://devtools/shared/commands/commands-factory.js"); } = require("resource://devtools/shared/commands/commands-factory.js");
/**
* Set up the equivalent of an `about:debugging` toolbox for a given extension, minus
* the toolbox.
*
* @param {String} id - The id for the extension to be targeted by the toolbox.
* @return {Object} Resolves with the web extension actor front and target objects when
* the debugger has been connected to the extension.
*/
async function setupExtensionDebugging(id) {
const commands = await CommandsFactory.forAddon(id);
const target = await commands.descriptorFront.getTarget();
return { front: commands.descriptorFront, target };
}
exports.setupExtensionDebugging = setupExtensionDebugging;
/** /**
* Loads and starts up a test extension given the provided extension configuration. * Loads and starts up a test extension given the provided extension configuration.
* *
@ -52,21 +37,32 @@ async function startupExtension(extConfig) {
exports.startupExtension = startupExtension; exports.startupExtension = startupExtension;
/** /**
* Initializes the extensionStorage actor for a target extension. This is effectively * Initializes the extensionStorage actor for a given extension. This is effectively
* what happens when the addon storage panel is opened in the browser. * what happens when the addon storage panel is opened in the browser.
* *
* @param {String} - id, The addon id * @param {String} - id, The addon id
* @return {Object} - Resolves with the web extension actor target and extensionStorage * @return {Object} - Resolves with the DevTools "commands" objact and the extensionStorage
* store objects when the panel has been opened. * resource/front.
*/ */
async function openAddonStoragePanel(id) { async function openAddonStoragePanel(id) {
const { target } = await setupExtensionDebugging(id); const commands = await CommandsFactory.forAddon(id);
await commands.targetCommand.startListening();
const storageFront = await target.getFront("storage"); // Fetch the EXTENSION_STORAGE resource.
const stores = await storageFront.listStores(); // Unfortunately, we can't use resourceCommand.waitForNextResource as it would destroy
const extensionStorage = stores.extensionStorage || null; // the actor by immediately unwatching for the resource type.
const extensionStorage = await new Promise(resolve => {
commands.resourceCommand.watchResources(
[commands.resourceCommand.TYPES.EXTENSION_STORAGE],
{
onAvailable(resources) {
resolve(resources[0]);
},
}
);
});
return { target, extensionStorage, storageFront }; return { commands, extensionStorage };
} }
exports.openAddonStoragePanel = openAddonStoragePanel; exports.openAddonStoragePanel = openAddonStoragePanel;
@ -170,11 +166,11 @@ exports.extensionScriptWithMessageListener = extensionScriptWithMessageListener;
* Shutdown procedure common to all tasks. * Shutdown procedure common to all tasks.
* *
* @param {Object} extension - The test extension * @param {Object} extension - The test extension
* @param {Object} target - The web extension actor targeted by the DevTools client * @param {Object} commands - The web extension commands used by the DevTools to interact with the backend
*/ */
async function shutdown(extension, target) { async function shutdown(extension, commands) {
if (target) { if (commands) {
await target.destroy(); await commands.destroy();
} }
await extension.unload(); await extension.unload();
} }

View file

@ -1372,6 +1372,11 @@ loader.lazyRequireGetter(
ResourceCommand.TYPES.COOKIE, ResourceCommand.TYPES.COOKIE,
"resource://devtools/shared/commands/resource/transformers/storage-cookie.js" "resource://devtools/shared/commands/resource/transformers/storage-cookie.js"
); );
loader.lazyRequireGetter(
ResourceTransformers,
ResourceCommand.TYPES.EXTENSION_STORAGE,
"resource://devtools/shared/commands/resource/transformers/storage-extension.js"
);
loader.lazyRequireGetter( loader.lazyRequireGetter(
ResourceTransformers, ResourceTransformers,
ResourceCommand.TYPES.INDEXED_DB, ResourceCommand.TYPES.INDEXED_DB,

View file

@ -8,6 +8,7 @@ DevToolsModules(
"network-events.js", "network-events.js",
"storage-cache.js", "storage-cache.js",
"storage-cookie.js", "storage-cookie.js",
"storage-extension.js",
"storage-indexed-db.js", "storage-indexed-db.js",
"storage-local-storage.js", "storage-local-storage.js",
"storage-session-storage.js", "storage-session-storage.js",

View file

@ -0,0 +1,26 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {
TYPES: { EXTENSION_STORAGE },
} = require("resource://devtools/shared/commands/resource/resource-command.js");
const { Front, types } = require("resource://devtools/shared/protocol.js");
module.exports = function({ resource, watcherFront, targetFront }) {
if (!(resource instanceof Front) && watcherFront) {
const { innerWindowId } = resource;
// it's safe to instantiate the front now, so we do it.
resource = types.getType("extensionStorage").read(resource, targetFront);
resource.resourceType = EXTENSION_STORAGE;
resource.resourceId = `${EXTENSION_STORAGE}-${targetFront.browsingContextID}`;
resource.resourceKey = "extensionStorage";
resource.innerWindowId = innerWindowId;
}
return resource;
};

View file

@ -227,11 +227,12 @@ const Types = (exports.__TypesForTests = [
}, },
{ {
types: [ types: [
"Cache",
"cookies", "cookies",
"localStorage", "localStorage",
"sessionStorage", "extensionStorage",
"Cache",
"indexedDB", "indexedDB",
"sessionStorage",
"storage", "storage",
], ],
spec: "devtools/shared/specs/storage", spec: "devtools/shared/specs/storage",