--- a/browser/components/extensions/ext-tabs.js
+++ b/browser/components/extensions/ext-tabs.js
@@ -648,30 +648,30 @@ extensions.registerSchemaAPI("tabs", nul
let message = {
options,
width: browser.clientWidth,
height: browser.clientHeight,
};
return context.sendMessage(browser.messageManager, "Extension:Capture",
- message, recipient);
+ message, {recipient});
},
detectLanguage: function(tabId) {
let tab = tabId !== null ? TabManager.getTab(tabId) : TabManager.activeTab;
if (!tab) {
return Promise.reject({message: `Invalid tab ID: ${tabId}`});
}
let browser = tab.linkedBrowser;
let recipient = {innerWindowID: browser.innerWindowID};
return context.sendMessage(browser.messageManager, "Extension:DetectLanguage",
- {}, recipient);
+ {}, {recipient});
},
_execute: function(tabId, details, kind, method) {
let tab = tabId !== null ? TabManager.getTab(tabId) : TabManager.activeTab;
let mm = tab.linkedBrowser.messageManager;
let options = {
js: [],
@@ -719,17 +719,17 @@ extensions.registerSchemaAPI("tabs", nul
options.match_about_blank = details.matchAboutBlank;
}
if (details.runAt !== null) {
options.run_at = details.runAt;
} else {
options.run_at = "document_idle";
}
- return context.sendMessage(mm, "Extension:Execute", {options}, recipient);
+ return context.sendMessage(mm, "Extension:Execute", {options}, {recipient});
},
executeScript: function(tabId, details) {
return self.tabs._execute(tabId, details, "js", "executeScript");
},
insertCSS: function(tabId, details) {
return self.tabs._execute(tabId, details, "css", "insertCSS");
--- a/browser/components/extensions/test/browser/browser_ext_tabs_sendMessage.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_sendMessage.js
@@ -10,58 +10,72 @@ add_task(function* tabsSendMessageReply(
"content_scripts": [{
"matches": ["http://example.com/"],
"js": ["content-script.js"],
"run_at": "document_start",
}],
},
background: function() {
+ let firstTab;
let promiseResponse = new Promise(resolve => {
browser.runtime.onMessage.addListener((msg, sender, respond) => {
if (msg == "content-script-ready") {
let tabId = sender.tab.id;
browser.tabs.sendMessage(tabId, "respond-never", response => {
- browser.test.fail("Got unexpected response callback");
+ browser.test.fail(`Got unexpected response callback: ${response}`);
browser.test.notifyFail("sendMessage");
});
Promise.all([
promiseResponse,
+
browser.tabs.sendMessage(tabId, "respond-now"),
+ browser.tabs.sendMessage(tabId, "respond-now-2"),
new Promise(resolve => browser.tabs.sendMessage(tabId, "respond-soon", resolve)),
browser.tabs.sendMessage(tabId, "respond-promise"),
browser.tabs.sendMessage(tabId, "respond-never"),
+
browser.tabs.sendMessage(tabId, "respond-error").catch(error => Promise.resolve({error})),
browser.tabs.sendMessage(tabId, "throw-error").catch(error => Promise.resolve({error})),
- ]).then(([response, respondNow, respondSoon, respondPromise, respondNever, respondError, throwError]) => {
+
+ browser.tabs.sendMessage(firstTab, "no-listener").catch(error => Promise.resolve({error})),
+ ]).then(([response, respondNow, respondNow2, respondSoon, respondPromise, respondNever, respondError, throwError, noListener]) => {
browser.test.assertEq("expected-response", response, "Content script got the expected response");
browser.test.assertEq("respond-now", respondNow, "Got the expected immediate response");
+ browser.test.assertEq("respond-now-2", respondNow2, "Got the expected immediate response from the second listener");
browser.test.assertEq("respond-soon", respondSoon, "Got the expected delayed response");
browser.test.assertEq("respond-promise", respondPromise, "Got the expected promise response");
browser.test.assertEq(undefined, respondNever, "Got the expected no-response resolution");
browser.test.assertEq("respond-error", respondError.error.message, "Got the expected error response");
browser.test.assertEq("throw-error", throwError.error.message, "Got the expected thrown error response");
+ browser.test.assertEq("Could not establish connection. Receiving end does not exist.",
+ noListener.error.message,
+ "Got the expected no listener response");
+
return browser.tabs.remove(tabId);
}).then(() => {
browser.test.notifyPass("sendMessage");
});
return Promise.resolve("expected-response");
} else if (msg[0] == "got-response") {
resolve(msg[1]);
}
});
});
- browser.tabs.create({url: "http://example.com/"});
+ browser.tabs.query({currentWindow: true, active: true}).then(tabs => {
+ firstTab = tabs[0].id;
+ browser.tabs.create({url: "http://example.com/"});
+ });
},
files: {
"content-script.js": function() {
browser.runtime.onMessage.addListener((msg, sender, respond) => {
if (msg == "respond-now") {
respond(msg);
} else if (msg == "respond-soon") {
@@ -72,16 +86,23 @@ add_task(function* tabsSendMessageReply(
} else if (msg == "respond-never") {
return;
} else if (msg == "respond-error") {
return Promise.reject(new Error(msg));
} else if (msg == "throw-error") {
throw new Error(msg);
}
});
+ browser.runtime.onMessage.addListener((msg, sender, respond) => {
+ if (msg == "respond-now") {
+ respond("hello");
+ } else if (msg == "respond-now-2") {
+ respond(msg);
+ }
+ });
browser.runtime.sendMessage("content-script-ready").then(response => {
browser.runtime.sendMessage(["got-response", response]);
});
},
},
});
yield extension.startup();
@@ -102,17 +123,17 @@ add_task(function* tabsSendMessageNoExce
let exception;
try {
browser.tabs.sendMessage(tab.id, "message");
browser.tabs.sendMessage(tab.id + 100, "message");
} catch (e) {
exception = e;
}
- browser.test.assertEq(undefined, exception, "no exception should be raised on tabs.sendMessage to unexistent tabs");
+ browser.test.assertEq(undefined, exception, "no exception should be raised on tabs.sendMessage to nonexistent tabs");
browser.tabs.remove(tab.id, function() {
browser.test.notifyPass("tabs.sendMessage");
});
});
},
});
yield Promise.all([
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -86,17 +86,16 @@ ExtensionManagement.registerSchema("chro
ExtensionManagement.registerSchema("chrome://extensions/content/schemas/test.json");
ExtensionManagement.registerSchema("chrome://extensions/content/schemas/web_navigation.json");
ExtensionManagement.registerSchema("chrome://extensions/content/schemas/web_request.json");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
BaseContext,
LocaleData,
- MessageBroker,
Messenger,
injectAPI,
instanceOf,
extend,
flushJarCache,
} = ExtensionUtils;
const LOGGER_ID_BASE = "addons.webextension.";
@@ -211,22 +210,16 @@ var Management = {
this.emitter.emit(hook, ...args);
},
off(hook, callback) {
this.emitter.off(hook, callback);
},
};
-// A MessageBroker that's used to send and receive messages for
-// extension pages (which run in the chrome process).
-var globalBroker = new MessageBroker([Services.mm, Services.ppmm]);
-
-var gContextId = 0;
-
// An extension page is an execution context for any extension content
// that runs in the chrome process. It's used for background pages
// (type="background"), popups (type="popup"), and any extension
// content loaded into browser tabs (type="tab").
//
// |params| is an object with the following properties:
// |type| is one of "background", "popup", or "tab".
// |contentWindow| is the DOM window the content runs in.
@@ -238,57 +231,45 @@ ExtensionPage = class extends BaseContex
super();
let {type, contentWindow, uri} = params;
this.extension = extension;
this.type = type;
this.contentWindow = contentWindow || null;
this.uri = uri || extension.baseURI;
this.incognito = params.incognito || false;
- this.contextId = gContextId++;
this.unloaded = false;
// This is the MessageSender property passed to extension.
// It can be augmented by the "page-open" hook.
let sender = {id: extension.uuid};
if (uri) {
sender.url = uri.spec;
}
let delegate = {
getSender() {},
};
Management.emit("page-load", this, params, sender, delegate);
// Properties in |filter| must match those in the |recipient|
// parameter of sendMessage.
let filter = {extensionId: extension.id};
- this.messenger = new Messenger(this, globalBroker, sender, filter, delegate);
+ this.messenger = new Messenger(this, [Services.mm, Services.ppmm], sender, filter, delegate);
this.extension.views.add(this);
}
get cloneScope() {
return this.contentWindow;
}
get principal() {
return this.contentWindow.document.nodePrincipal;
}
- // A wrapper around MessageChannel.sendMessage which adds the extension ID
- // to the recipient object, and ensures replies are not processed after the
- // context has been unloaded.
- sendMessage(target, messageName, data, recipient = {}, sender = {}) {
- recipient.extensionId = this.extension.id;
- sender.extensionId = this.extension.id;
- sender.contextId = this.contextId;
-
- return MessageChannel.sendMessage(target, messageName, data, recipient, sender);
- }
-
// Called when the extension shuts down.
shutdown() {
Management.emit("page-shutdown", this);
this.unload();
}
// This method is called when an extension page navigates away or
// its tab is closed.
@@ -297,26 +278,21 @@ ExtensionPage = class extends BaseContex
// multiple times for tab pages closed by the "page-unload" handlers
// triggered below.
if (this.unloaded) {
return;
}
this.unloaded = true;
- MessageChannel.abortResponses({
- extensionId: this.extension.id,
- contextId: this.contextId,
- });
+ super.unload();
Management.emit("page-unload", this);
this.extension.views.delete(this);
-
- super.unload();
}
};
// For extensions that have called setUninstallURL(), send an event
// so the browser can display the URL.
let UninstallObserver = {
init: function() {
AddonManager.addAddonListener(this);
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -38,17 +38,16 @@ XPCOMUtils.defineLazyModuleGetter(this,
XPCOMUtils.defineLazyModuleGetter(this, "WebNavigationFrames",
"resource://gre/modules/WebNavigationFrames.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
runSafeSyncWithoutClone,
BaseContext,
LocaleData,
- MessageBroker,
Messenger,
injectAPI,
flushJarCache,
detectLanguage,
promiseDocumentReady,
} = ExtensionUtils;
function isWhenBeforeOrSame(when1, when2) {
@@ -320,23 +319,22 @@ class ExtensionContext extends BaseConte
let delegate = {
getSender(context, target, sender) {
// Nothing to do here.
},
};
let url = contentWindow.location.href;
- let broker = ExtensionContent.getBroker(mm);
// The |sender| parameter is passed directly to the extension.
let sender = {id: this.extension.uuid, frameId, url};
// Properties in |filter| must match those in the |recipient|
// parameter of sendMessage.
let filter = {extensionId, frameId};
- this.messenger = new Messenger(this, broker, sender, filter, delegate);
+ this.messenger = new Messenger(this, [mm], sender, filter, delegate);
this.chromeObj = Cu.createObjectIn(this.sandbox, {defineAs: "browser"});
// Sandboxes don't get Xrays for some weird compatibility
// reason. However, we waive here anyway in case that changes.
Cu.waiveXrays(this.sandbox).chrome = this.chromeObj;
injectAPI(api(this), this.chromeObj);
@@ -734,18 +732,16 @@ class ExtensionGlobal {
this.global = global;
MessageChannel.addListener(global, "Extension:Capture", this);
MessageChannel.addListener(global, "Extension:DetectLanguage", this);
MessageChannel.addListener(global, "Extension:Execute", this);
MessageChannel.addListener(global, "WebNavigation:GetFrame", this);
MessageChannel.addListener(global, "WebNavigation:GetAllFrames", this);
- this.broker = new MessageBroker([global]);
-
this.windowId = global.content
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.outerWindowID;
global.sendAsyncMessage("Extension:TopWindowID", {windowId: this.windowId});
}
@@ -853,15 +849,11 @@ this.ExtensionContent = {
init(global) {
this.globals.set(global, new ExtensionGlobal(global));
},
uninit(global) {
this.globals.get(global).uninit();
this.globals.delete(global);
},
-
- getBroker(messageManager) {
- return this.globals.get(messageManager).broker;
- },
};
ExtensionManager.init();
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -15,16 +15,18 @@ Cu.import("resource://gre/modules/XPCOMU
Cu.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LanguageDetector",
"resource:///modules/translation/LanguageDetector.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Locale",
"resource://gre/modules/Locale.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
+ "resource://gre/modules/MessageChannel.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
"resource://gre/modules/Preferences.jsm");
function filterStack(error) {
return String(error.stack).replace(/(^.*(Task\.jsm|Promise-backend\.js).*\n)+/gm, "<Promise Chain>\n");
}
// Run a function and report exceptions.
@@ -120,21 +122,24 @@ DefaultWeakMap.prototype = {
class SpreadArgs extends Array {
constructor(args) {
super();
this.push(...args);
}
}
+let gContextId = 0;
+
class BaseContext {
constructor() {
this.onClose = new Set();
this.checkedLastError = false;
this._lastError = null;
+ this.contextId = ++gContextId;
}
get cloneScope() {
throw new Error("Not implemented");
}
get principal() {
throw new Error("Not implemented");
@@ -165,16 +170,32 @@ class BaseContext {
callOnClose(obj) {
this.onClose.add(obj);
}
forgetOnClose(obj) {
this.onClose.delete(obj);
}
+ /**
+ * A wrapper around MessageChannel.sendMessage which adds the extension ID
+ * to the recipient object, and ensures replies are not processed after the
+ * context has been unloaded.
+ */
+ sendMessage(target, messageName, data, options = {}) {
+ options.recipient = options.recipient || {};
+ options.sender = options.sender || {};
+
+ options.recipient.extensionId = this.extension.id;
+ options.sender.extensionId = this.extension.id;
+ options.sender.contextId = this.contextId;
+
+ return MessageChannel.sendMessage(target, messageName, data, options);
+ }
+
get lastError() {
this.checkedLastError = true;
return this._lastError;
}
set lastError(val) {
this.checkedLastError = false;
this._lastError = val;
@@ -276,16 +297,21 @@ class BaseContext {
value => {
runSafeSyncWithoutClone(reject, this.normalizeError(value));
});
});
}
}
unload() {
+ MessageChannel.abortResponses({
+ extensionId: this.extension.id,
+ contextId: this.contextId,
+ });
+
for (let obj of this.onClose) {
obj.close();
}
}
}
function LocaleData(data) {
this.defaultLocale = data.defaultLocale;
@@ -665,111 +691,17 @@ function promiseDocumentReady(doc) {
}, true);
});
}
/*
* Messaging primitives.
*/
-var nextBrokerId = 1;
-
-var MESSAGES = [
- "Extension:Message",
- "Extension:Connect",
-];
-
-// Receives messages from multiple message managers and directs them
-// to a set of listeners. On the child side: one broker per frame
-// script. On the parent side: one broker total, covering both the
-// global MM and the ppmm. Message must be tagged with a recipient,
-// which is an object with properties. Listeners can filter for
-// messages that have a certain value for a particular property in the
-// recipient. (If a message doesn't specify the given property, it's
-// considered a match.)
-function MessageBroker(messageManagers) {
- this.messageManagers = messageManagers;
- for (let mm of this.messageManagers) {
- for (let message of MESSAGES) {
- mm.addMessageListener(message, this);
- }
- }
-
- this.listeners = {message: [], connect: []};
-}
-
-MessageBroker.prototype = {
- uninit() {
- for (let mm of this.messageManagers) {
- for (let message of MESSAGES) {
- mm.removeMessageListener(message, this);
- }
- }
-
- this.listeners = null;
- },
-
- makeId() {
- return nextBrokerId++;
- },
-
- addListener(type, listener, filter) {
- this.listeners[type].push({filter, listener});
- },
-
- removeListener(type, listener) {
- for (let i = 0; i < this.listeners[type].length; i++) {
- if (this.listeners[type][i].listener == listener) {
- this.listeners[type].splice(i, 1);
- return;
- }
- }
- },
-
- runListeners(type, target, data) {
- let listeners = [];
- for (let {listener, filter} of this.listeners[type]) {
- let pass = true;
- for (let prop in filter) {
- if (prop in data.recipient && filter[prop] != data.recipient[prop]) {
- pass = false;
- break;
- }
- }
-
- // Save up the list of listeners to call in case they modify the
- // set of listeners.
- if (pass) {
- listeners.push(listener);
- }
- }
-
- for (let listener of listeners) {
- listener(type, target, data.message, data.sender, data.recipient);
- }
- },
-
- receiveMessage({name, data, target}) {
- switch (name) {
- case "Extension:Message":
- this.runListeners("message", target, data);
- break;
-
- case "Extension:Connect":
- this.runListeners("connect", target, data);
- break;
- }
- },
-
- sendMessage(messageManager, type, message, sender, recipient) {
- let data = {message, sender, recipient};
- let names = {message: "Extension:Message", connect: "Extension:Connect"};
- messageManager.sendAsyncMessage(names[type], data);
- },
-};
+var nextPortId = 1;
// Abstraction for a Port object in the extension API. Each port has a unique ID.
function Port(context, messageManager, name, id, sender) {
this.context = context;
this.messageManager = messageManager;
this.name = name;
this.id = id;
this.listenerName = `Extension:Port-${this.id}`;
@@ -872,138 +804,134 @@ function getMessageManager(target) {
}
return target;
}
// Each extension scope gets its own Messenger object. It handles the
// basics of sendMessage, onMessage, connect, and onConnect.
//
// |context| is the extension scope.
-// |broker| is a MessageBroker used to receive and send messages.
+// |messageManagers| is an array of MessageManagers used to receive messages.
// |sender| is an object describing the sender (usually giving its extension id, tabId, etc.)
// |filter| is a recipient filter to apply to incoming messages from the broker.
// |delegate| is an object that must implement a few methods:
// getSender(context, messageManagerTarget, sender): returns a MessageSender
// See https://developer.chrome.com/extensions/runtime#type-MessageSender.
-function Messenger(context, broker, sender, filter, delegate) {
+function Messenger(context, messageManagers, sender, filter, delegate) {
this.context = context;
- this.broker = broker;
+ this.messageManagers = messageManagers;
this.sender = sender;
this.filter = filter;
this.delegate = delegate;
}
Messenger.prototype = {
- sendMessage(messageManager, msg, recipient, responseCallback) {
- let id = this.broker.makeId();
- let replyName = `Extension:Reply-${id}`;
- recipient.messageId = id;
- this.broker.sendMessage(messageManager, "message", msg, this.sender, recipient);
+ _sendMessage(messageManager, message, data, recipient) {
+ let options = {
+ recipient,
+ sender: this.sender,
+ responseType: MessageChannel.RESPONSE_FIRST,
+ };
- let promise = new Promise((resolve, reject) => {
- let onClose;
- let listener = ({data: response}) => {
- messageManager.removeMessageListener(replyName, listener);
- this.context.forgetOnClose(onClose);
+ return this.context.sendMessage(messageManager, message, data, options);
+ },
- if (response.gotData) {
- resolve(response.data);
- } else if (response.error) {
- reject(response.error);
- } else if (!responseCallback) {
- // As a special case, we don't call the callback variant if we
- // receive no response, but the promise needs to resolve or
- // reject in either case.
- resolve();
+ sendMessage(messageManager, msg, recipient, responseCallback) {
+ let promise = this._sendMessage(messageManager, "Extension:Message", msg, recipient)
+ .catch(error => {
+ if (error.result == MessageChannel.RESULT_NO_HANDLER) {
+ return Promise.reject({message: "Could not establish connection. Receiving end does not exist."});
+ } else if (error.result == MessageChannel.RESULT_NO_RESPONSE) {
+ if (responseCallback) {
+ // As a special case, we don't call the callback variant if we
+ // receive no response. So return a promise which will never
+ // resolve.
+ return new Promise(() => {});
+ }
+ } else {
+ return Promise.reject({message: error.message});
}
- };
- onClose = {
- close() {
- messageManager.removeMessageListener(replyName, listener);
- },
- };
-
- messageManager.addMessageListener(replyName, listener);
- this.context.callOnClose(onClose);
- });
+ });
return this.context.wrapPromise(promise, responseCallback);
},
onMessage(name) {
return new SingletonEventManager(this.context, name, callback => {
- let listener = (type, target, message, sender, recipient) => {
- message = Cu.cloneInto(message, this.context.cloneScope);
- if (this.delegate) {
- this.delegate.getSender(this.context, target, sender);
- }
- sender = Cu.cloneInto(sender, this.context.cloneScope);
+ let listener = {
+ messageFilter: {},
+ listenerInfo: this.filter,
+
+ receiveMessage: ({target, data: message, sender, recipient}) => {
+ if (this.delegate) {
+ this.delegate.getSender(this.context, target, sender);
+ }
- let mm = getMessageManager(target);
- let replyName = `Extension:Reply-${recipient.messageId}`;
+ let sendResponse;
+ let response = undefined;
+ let promise = new Promise(resolve => {
+ sendResponse = value => {
+ resolve(value);
+ response = promise;
+ };
+ });
- new Promise((resolve, reject) => {
- let sendResponse = Cu.exportFunction(resolve, this.context.cloneScope);
+ message = Cu.cloneInto(message, this.context.cloneScope);
+ sender = Cu.cloneInto(sender, this.context.cloneScope);
+ sendResponse = Cu.exportFunction(sendResponse, this.context.cloneScope);
// Note: We intentionally do not use runSafe here so that any
// errors are propagated to the message sender.
let result = callback(message, sender, sendResponse);
if (result instanceof Promise) {
- resolve(result);
- } else if (result !== true) {
- reject();
+ return result;
+ } else if (result === true) {
+ return promise;
}
- }).then(
- data => {
- mm.sendAsyncMessage(replyName, {data, gotData: true});
- },
- error => {
- if (error) {
- // The result needs to be structured-clonable, which
- // ordinary Error objects are not.
- try {
- error = {message: String(error.message), stack: String(error.stack)};
- } catch (e) {
- error = {message: String(error)};
- }
- }
- mm.sendAsyncMessage(replyName, {error, gotData: false});
- });
+ return response;
+ },
};
- this.broker.addListener("message", listener, this.filter);
+ MessageChannel.addListener(this.messageManagers, "Extension:Message", listener);
return () => {
- this.broker.removeListener("message", listener);
+ MessageChannel.removeListener(this.messageManagers, "Extension:Message", listener);
};
}).api();
},
connect(messageManager, name, recipient) {
- let portId = this.broker.makeId();
+ let portId = nextPortId++;
let port = new Port(this.context, messageManager, name, portId, null);
let msg = {name, portId};
- this.broker.sendMessage(messageManager, "connect", msg, this.sender, recipient);
+ // TODO: Disconnect the port if no response?
+ this._sendMessage(messageManager, "Extension:Connect", msg, recipient);
return port.api();
},
onConnect(name) {
- return new EventManager(this.context, name, fire => {
- let listener = (type, target, message, sender, recipient) => {
- let {name, portId} = message;
- let mm = getMessageManager(target);
- if (this.delegate) {
- this.delegate.getSender(this.context, target, sender);
- }
- let port = new Port(this.context, mm, name, portId, sender);
- fire.withoutClone(port.api());
+ return new SingletonEventManager(this.context, name, callback => {
+ let listener = {
+ messageFilter: {},
+ listenerInfo: this.filter,
+
+ receiveMessage: ({target, data: message, sender, recipient}) => {
+ let {name, portId} = message;
+ let mm = getMessageManager(target);
+ if (this.delegate) {
+ this.delegate.getSender(this.context, target, sender);
+ }
+ let port = new Port(this.context, mm, name, portId, sender);
+ runSafeSyncWithoutClone(callback, port.api());
+ return true;
+ },
};
- this.broker.addListener("connect", listener, this.filter);
+ MessageChannel.addListener(this.messageManagers, "Extension:Connect", listener);
return () => {
- this.broker.removeListener("connect", listener);
+ MessageChannel.removeListener(this.messageManagers, "Extension:Connect", listener);
};
}).api();
},
};
function flushJarCache(jarFile) {
Services.obs.notifyObservers(jarFile, "flush-cache-entry", null);
}
@@ -1051,14 +979,13 @@ this.ExtensionUtils = {
runSafe,
runSafeSync,
runSafeSyncWithoutClone,
runSafeWithoutClone,
BaseContext,
DefaultWeakMap,
EventManager,
LocaleData,
- MessageBroker,
Messenger,
PlatformInfo,
SingletonEventManager,
SpreadArgs,
};
--- a/toolkit/components/extensions/MessageChannel.jsm
+++ b/toolkit/components/extensions/MessageChannel.jsm
@@ -56,17 +56,17 @@
* filter, and an optional sender tag to identify itself:
*
* let data = { touchWith: "pencil" };
* let sender = { extensionID, contextID };
* let recipient = { innerWindowID: tab.linkedBrowser.innerWindowID, extensionID };
*
* MessageChannel.sendMessage(
* tab.linkedBrowser.messageManager, "ContentScript:TouchContent",
- * data, recipient, sender
+ * data, {recipient, sender}
* ).then(result => {
* alert(result.touchResult);
* });
*
* Since the lifetimes of message senders and receivers may not always
* match, either side of the message channel may cancel pending
* responses which match its sender or recipient tags.
*
@@ -147,58 +147,50 @@ class FilteringMessageManager {
/**
* Receives a message from our message manager, maps it to a handler, and
* passes the result to our message callback.
*/
receiveMessage({data, target}) {
let handlers = Array.from(this.getHandlers(data.messageName, data.recipient));
- let result = {};
- if (handlers.length == 0) {
- result.error = {result: MessageChannel.RESULT_NO_HANDLER,
- message: "No matching message handler"};
- } else if (handlers.length > 1) {
- result.error = {result: MessageChannel.RESULT_MULTIPLE_HANDLERS,
- message: `Multiple matching handlers for ${data.messageName}`};
- } else {
- result.handler = handlers[0];
- }
-
data.target = target;
- this.callback(result, data);
+ this.callback({handlers}, data);
}
/**
* Iterates over all handlers for the given message name. If `recipient`
* is provided, only iterates over handlers whose filters match it.
*
* @param {string|number} messageName
* The message for which to return handlers.
* @param {object} recipient
* The recipient data on which to filter handlers.
*/
* getHandlers(messageName, recipient) {
let handlers = this.handlers.get(messageName) || new Set();
for (let handler of handlers) {
- if (MessageChannel.matchesFilter(handler.messageFilter, recipient)) {
+ if (MessageChannel.matchesFilter(handler.messageFilter, recipient) &&
+ MessageChannel.matchesInfo(handler.listenerInfo || {}, recipient)) {
yield handler;
}
}
}
/**
* Registers a handler for the given message.
*
* @param {string} messageName
* The internal message name for which to register the handler.
* @param {object} handler
* An opaque handler object. The object must have a `messageFilter`
- * property on which to filter messages. Final dispatching is handled
- * by the message callback passed to the constructor.
+ * property on which to filter messages.
+ *
+ * Final dispatching is handled by the message callback passed to
+ * the constructor.
*/
addHandler(messageName, handler) {
if (!this.handlers.has(messageName)) {
this.handlers.set(messageName, new Set());
}
this.handlers.get(messageName).add(handler);
}
@@ -293,23 +285,54 @@ this.MessageChannel = {
this.pendingResponses = new Set();
},
RESULT_SUCCESS: 0,
RESULT_DISCONNECTED: 1,
RESULT_NO_HANDLER: 2,
RESULT_MULTIPLE_HANDLERS: 3,
RESULT_ERROR: 4,
+ RESULT_NO_RESPONSE: 5,
REASON_DISCONNECTED: {
result: this.RESULT_DISCONNECTED,
message: "Message manager disconnected",
},
/**
+ * Specifies that only a single listener matching the specified
+ * recipient tag may be listening for the given message, at the other
+ * end of the target message manager.
+ *
+ * If no matching listeners exist, a RESULT_NO_HANDLER error will be
+ * returned. If multiple matching listeners exist, a
+ * RESULT_MULTIPLE_HANDLERS error will be returned.
+ */
+ RESPONSE_SINGLE: 0,
+
+ /**
+ * If multiple message managers matching the specified recipient tag
+ * are listening for a message, all listeners are notified, but only
+ * the first response or error is returned.
+ *
+ * If no matching listeners exist, a RESULT_NO_HANDLER error will be
+ * returned. If no listeners return a response, a RESULT_NO_RESPONSE
+ * error will be returned.
+ */
+ RESPONSE_FIRST: 1,
+
+ /**
+ * If multiple message managers matching the specified recipient tag
+ * are listening for a message, all listeners are notified, and all
+ * responses are returned as an array, once all listeners have
+ * replied.
+ */
+ RESPONSE_ALL: 2,
+
+ /**
* Returns true if the given `data` object matches the given `filter`
* object. The objects match if every property of `filter` is present
* in `data`, and the values in both objects are strictly equal.
*
* @param {object} filter
* The filter object to match against.
* @param {object} data
* The data object being matched.
@@ -317,20 +340,36 @@ this.MessageChannel = {
*/
matchesFilter(filter, data) {
return Object.keys(filter).every(key => {
return key in data && data[key] === filter[key];
});
},
/**
+ * Returns true if every property which is present in both the `info`
+ * and `data` object has the same value in both objects.
+ *
+ * @param {object} info
+ * The info object to match against.
+ * @param {object} data
+ * The data object being matched.
+ * @returns {bool} True if the objects match.
+ */
+ matchesInfo(info, data) {
+ return Object.keys(info).every(key => {
+ return !(key in data) || data[key] === info[key];
+ });
+ },
+
+ /**
* Adds a message listener to the given message manager.
*
- * @param {nsIMessageSender} target
- * The message manager on which to listen.
+ * @param {nsIMessageSender|[nsIMessageSender]} targets
+ * The message managers on which to listen.
* @param {string|number} messageName
* The name of the message to listen for.
* @param {MessageReceiver} handler
* The handler to dispatch to. Must be an object with the following
* properties:
*
* receiveMessage:
* A method which is called for each message received by the
@@ -363,63 +402,84 @@ this.MessageChannel = {
* resolution or rejection value of which will likewise be
* returned to the message sender.
*
* messageFilter:
* An object containing arbitrary properties on which to filter
* received messages. Messages will only be dispatched to this
* object if the `recipient` object passed to `sendMessage`
* matches this filter, as determined by `matchesFilter`.
+ *
+ * listenerInfo:
+ * An object containing arbitrary properties describing the
+ * listener. This filters messages in a similar manner to
+ * `messageFilter`, but only except that properties are only
+ * required to match if they are present in both the
+ * `listenerInfo` and the `recipient` object.
*/
- addListener(target, messageName, handler) {
- this.messageManagers.get(target).addHandler(messageName, handler);
+ addListener(targets, messageName, handler) {
+ for (let target of [].concat(targets)) {
+ this.messageManagers.get(target).addHandler(messageName, handler);
+ }
},
/**
* Removes a message listener from the given message manager.
*
* @param {nsIMessageSender} target
- * The message manager on which to stop listening.
+ * @param {nsIMessageSender|[nsIMessageSender]} targets
+ * The message managers on which to stop listening.
* @param {string|number} messageName
* The name of the message to stop listening for.
* @param {MessageReceiver} handler
* The handler to stop dispatching to.
*/
- removeListener(target, messageName, handler) {
- this.messageManagers.get(target).removeListener(messageName, handler);
+ removeListener(targets, messageName, handler) {
+ for (let target of [].concat(targets)) {
+ this.messageManagers.get(target).removeHandler(messageName, handler);
+ }
},
/**
* Sends a message via the given message manager. Returns a promise which
* resolves or rejects with the return value of the message receiver.
*
* The promise also rejects if there is no matching listener, or the other
* side of the message manager disconnects before the response is received.
*
* @param {nsIMessageSender} target
* The message manager on which to send the message.
* @param {string} messageName
* The name of the message to send, as passed to `addListener`.
* @param {object} data
* A structured-clone-compatible object to send to the message
* recipient.
- * @param {object} [recipient]
+ * @param {object} [options]
+ * An object containing any of the following properties:
+ * @param {object} [options.recipient]
* A structured-clone-compatible object to identify the message
* recipient. The object must match the `messageFilter` defined by
* recipients in order for the message to be received.
- * @param {object} [sender]
+ * @param {object} [options.sender]
* A structured-clone-compatible object to identify the message
* sender. This object may also be used as a filter to prematurely
* abort responses when the sender is being destroyed.
* @see `abortResponses`.
+ * @param {integer} [options.responseType=RESPONSE_SINGLE]
+ * Specifies the type of response expected. See the `RESPONSE_*`
+ * contents for details.
* @returns Promise
*/
- sendMessage(target, messageName, data, recipient = {}, sender = {}) {
+ sendMessage(target, messageName, data, options = {}) {
+ let sender = options.sender || {};
+ let recipient = options.recipient || {};
+ let responseType = options.responseType;
+
let channelId = gChannelId++;
- let message = {messageName, channelId, sender, recipient, data};
+ let message = {messageName, channelId, sender, recipient, data, responseType};
let deferred = PromiseUtils.defer();
deferred.messageFilter = {};
deferred.sender = recipient;
deferred.messageManager = target;
this._addPendingResponse(deferred);
@@ -433,46 +493,80 @@ this.MessageChannel = {
broker.removeHandler(channelId, deferred);
};
deferred.promise.then(cleanup, cleanup);
target.sendAsyncMessage(MESSAGE_MESSAGE, message);
return deferred.promise;
},
+ _callHandlers(handlers, data) {
+ let responseType = data.responseType || this.RESPONSE_SINGLE;
+
+ // At least one handler is required for all response types but
+ // RESPONSE_ALL.
+ if (handlers.length == 0 && responseType != this.RESPONSE_ALL) {
+ return Promise.reject({result: MessageChannel.RESULT_NO_HANDLER,
+ message: "No matching message handler"});
+ }
+
+ if (responseType == this.RESPONSE_SINGLE) {
+ if (handlers.length > 1) {
+ return Promise.reject({result: MessageChannel.RESULT_MULTIPLE_HANDLERS,
+ message: `Multiple matching handlers for ${data.messageName}`});
+ }
+
+ return new Promise(resolve => {
+ resolve(handlers[0].receiveMessage(data));
+ });
+ }
+
+ let responses = handlers.map(handler => handler.receiveMessage(data))
+ .filter(response => response !== undefined);
+
+ switch (responseType) {
+ case this.RESPONSE_FIRST:
+ if (responses.length == 0) {
+ return Promise.reject({result: MessageChannel.RESULT_NO_RESPONSE,
+ message: "No handler returned a response"});
+ }
+
+ return Promise.race(responses);
+
+ case this.RESPONSE_ALL:
+ return Promise.all(responses);
+ }
+ return Promise.reject({message: "Invalid response type"});
+ },
+
/**
* Handles dispatching message callbacks from the message brokers to their
* appropriate `MessageReceivers`, and routing the responses back to the
* original senders.
*
* Each handler object is a `MessageReceiver` object as passed to
* `addListener`.
*/
- _handleMessage({handler, error}, data) {
+ _handleMessage({handlers}, data) {
// The target passed to `receiveMessage` is sometimes a message manager
// owner instead of a message manager, so make sure to convert it to a
// message manager first if necessary.
let {target} = data;
if (!(target instanceof Ci.nsIMessageSender)) {
target = target.messageManager;
}
let deferred = {
sender: data.sender,
messageManager: target,
};
deferred.promise = new Promise((resolve, reject) => {
deferred.reject = reject;
- if (handler) {
- let result = handler.receiveMessage(data);
- resolve(result);
- } else {
- reject(error);
- }
+ this._callHandlers(handlers, data).then(resolve, reject);
}).then(
value => {
let response = {
result: this.RESULT_SUCCESS,
messageName: data.channelId,
recipient: {},
value,
};
@@ -508,25 +602,27 @@ this.MessageChannel = {
},
/**
* Handles message callbacks from the response brokers.
*
* Each handler object is a deferred object created by `sendMessage`, and
* should be resolved or rejected based on the contents of the response.
*/
- _handleResponse({handler, error}, data) {
- if (error) {
- // If we have an error at this point, we have handler to report it to,
- // so just log it.
- Cu.reportError(error.message);
+ _handleResponse({handlers}, data) {
+ // If we have an error at this point, we have handler to report it to,
+ // so just log it.
+ if (handlers.length == 0) {
+ Cu.reportError(`No matching message response handler for ${data.messageName}`);
+ } else if (handlers.length > 1) {
+ Cu.reportError(`Multiple matching response handlers for ${data.messageName}`);
} else if (data.result === this.RESULT_SUCCESS) {
- handler.resolve(data.value);
+ handlers[0].resolve(data.value);
} else {
- handler.reject(data.error);
+ handlers[0].reject(data.error);
}
},
/**
* Adds a pending response to the the `pendingResponses` list.
*
* The response object must be a deferred promise with the following
* properties:
--- a/toolkit/components/extensions/ext-webNavigation.js
+++ b/toolkit/components/extensions/ext-webNavigation.js
@@ -85,31 +85,31 @@ extensions.registerSchemaAPI("webNavigat
let tab = TabManager.getTab(details.tabId);
if (!tab) {
return Promise.reject({message: `No tab found with tabId: ${details.tabId}`});
}
let {innerWindowID, messageManager} = tab.linkedBrowser;
let recipient = {innerWindowID};
- return context.sendMessage(messageManager, "WebNavigation:GetAllFrames", {}, recipient)
+ return context.sendMessage(messageManager, "WebNavigation:GetAllFrames", {}, {recipient})
.then((results) => results.map(convertGetFrameResult.bind(null, details.tabId)));
},
getFrame(details) {
let tab = TabManager.getTab(details.tabId);
if (!tab) {
return Promise.reject({message: `No tab found with tabId: ${details.tabId}`});
}
let recipient = {
innerWindowID: tab.linkedBrowser.innerWindowID,
};
let mm = tab.linkedBrowser.messageManager;
- return context.sendMessage(mm, "WebNavigation:GetFrame", {options: details}, recipient)
+ return context.sendMessage(mm, "WebNavigation:GetFrame", {options: details}, {recipient})
.then((result) => {
return result ?
convertGetFrameResult(details.tabId, result) :
Promise.reject({message: `No frame found with frameId: ${details.frameId}`});
});
},
},
};
--- a/toolkit/components/extensions/test/mochitest/mochitest.ini
+++ b/toolkit/components/extensions/test/mochitest/mochitest.ini
@@ -41,16 +41,17 @@ skip-if = buildapp == 'b2g' # runat != d
[test_ext_permission_xhr.html]
skip-if = buildapp == 'b2g' # JavaScript error: jar:remoteopenfile:///data/local/tmp/generated-extension.xpi!/content.js, line 46: NS_ERROR_ILLEGAL_VALUE:
[test_ext_runtime_connect.html]
skip-if = buildapp == 'b2g' # port.sender.tab is undefined on b2g.
[test_ext_runtime_connect2.html]
skip-if = buildapp == 'b2g' # port.sender.tab is undefined on b2g.
[test_ext_runtime_disconnect.html]
[test_ext_runtime_getPlatformInfo.html]
+[test_ext_runtime_sendMessage.html]
[test_ext_sandbox_var.html]
[test_ext_sendmessage_reply.html]
skip-if = buildapp == 'b2g' # sender.tab is undefined on b2g.
[test_ext_sendmessage_reply2.html]
skip-if = buildapp == 'b2g' # sender.tab is undefined on b2g.
[test_ext_sendmessage_doublereply.html]
skip-if = buildapp == 'b2g' # sender.tab is undefined on b2g.
[test_ext_storage.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_sendMessage.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* tabsSendMessageReply() {
+ function background() {
+ browser.runtime.onMessage.addListener((msg, sender, respond) => {
+ if (msg == "respond-now") {
+ respond(msg);
+ } else if (msg == "respond-soon") {
+ setTimeout(() => { respond(msg); }, 0);
+ return true;
+ } else if (msg == "respond-promise") {
+ return Promise.resolve(msg);
+ } else if (msg == "respond-never") {
+ return;
+ } else if (msg == "respond-error") {
+ return Promise.reject(new Error(msg));
+ } else if (msg == "throw-error") {
+ throw new Error(msg);
+ }
+ });
+
+ browser.runtime.onMessage.addListener((msg, sender, respond) => {
+ if (msg == "respond-now") {
+ respond("hello");
+ } else if (msg == "respond-now-2") {
+ respond(msg);
+ }
+ });
+
+ browser.runtime.sendMessage("respond-never", response => {
+ browser.test.fail(`Got unexpected response callback: ${response}`);
+ browser.test.notifyFail("sendMessage");
+ });
+
+ Promise.all([
+ browser.runtime.sendMessage("respond-now"),
+ browser.runtime.sendMessage("respond-now-2"),
+ new Promise(resolve => browser.runtime.sendMessage("respond-soon", resolve)),
+ browser.runtime.sendMessage("respond-promise"),
+ browser.runtime.sendMessage("respond-never"),
+
+ browser.runtime.sendMessage("respond-error").catch(error => Promise.resolve({error})),
+ browser.runtime.sendMessage("throw-error").catch(error => Promise.resolve({error})),
+ ]).then(([respondNow, respondNow2, respondSoon, respondPromise, respondNever, respondError, throwError]) => {
+ browser.test.assertEq("respond-now", respondNow, "Got the expected immediate response");
+ browser.test.assertEq("respond-now-2", respondNow2, "Got the expected immediate response from the second listener");
+ browser.test.assertEq("respond-soon", respondSoon, "Got the expected delayed response");
+ browser.test.assertEq("respond-promise", respondPromise, "Got the expected promise response");
+ browser.test.assertEq(undefined, respondNever, "Got the expected no-response resolution");
+
+ browser.test.assertEq("respond-error", respondError.error.message, "Got the expected error response");
+ browser.test.assertEq("throw-error", throwError.error.message, "Got the expected thrown error response");
+
+ browser.test.notifyPass("sendMessage");
+ }).catch(e => {
+ browser.test.fail(`Error: ${e} :: ${e.stack}`);
+ browser.test.notifyFail("sendMessage");
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: `(${background})()`
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("sendMessage");
+ yield extension.unload();
+});
+</script>
+
+</body>
+</html>