mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-17 15:25:52 +00:00
Bug 1220154, 1249830: Handle sendMessage replies with 0 and >1 listeners correctly. r=billm
MozReview-Commit-ID: 7lE7RaJcl7n --HG-- extra : rebase_source : 424987c745840d56647600b8d9e1bf941afb1d95
This commit is contained in:
parent
5d50379e03
commit
876d0a0806
@ -653,7 +653,7 @@ extensions.registerSchemaAPI("tabs", null, (extension, context) => {
|
||||
};
|
||||
|
||||
return context.sendMessage(browser.messageManager, "Extension:Capture",
|
||||
message, recipient);
|
||||
message, {recipient});
|
||||
},
|
||||
|
||||
detectLanguage: function(tabId) {
|
||||
@ -666,7 +666,7 @@ extensions.registerSchemaAPI("tabs", null, (extension, context) => {
|
||||
let recipient = {innerWindowID: browser.innerWindowID};
|
||||
|
||||
return context.sendMessage(browser.messageManager, "Extension:DetectLanguage",
|
||||
{}, recipient);
|
||||
{}, {recipient});
|
||||
},
|
||||
|
||||
_execute: function(tabId, details, kind, method) {
|
||||
@ -724,7 +724,7 @@ extensions.registerSchemaAPI("tabs", null, (extension, context) => {
|
||||
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) {
|
||||
|
@ -15,28 +15,35 @@ add_task(function* tabsSendMessageReply() {
|
||||
},
|
||||
|
||||
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");
|
||||
@ -44,6 +51,10 @@ add_task(function* tabsSendMessageReply() {
|
||||
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");
|
||||
@ -56,7 +67,10 @@ add_task(function* tabsSendMessageReply() {
|
||||
});
|
||||
});
|
||||
|
||||
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: {
|
||||
@ -77,6 +91,13 @@ add_task(function* tabsSendMessageReply() {
|
||||
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]);
|
||||
});
|
||||
@ -107,7 +128,7 @@ add_task(function* tabsSendMessageNoExceptionOnNonExistentTab() {
|
||||
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");
|
||||
});
|
||||
|
@ -92,7 +92,6 @@ Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
||||
var {
|
||||
BaseContext,
|
||||
LocaleData,
|
||||
MessageBroker,
|
||||
Messenger,
|
||||
injectAPI,
|
||||
instanceOf,
|
||||
@ -217,12 +216,6 @@ var Management = {
|
||||
},
|
||||
};
|
||||
|
||||
// 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
|
||||
@ -244,7 +237,6 @@ ExtensionPage = class extends BaseContext {
|
||||
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.
|
||||
@ -261,7 +253,7 @@ ExtensionPage = class extends BaseContext {
|
||||
// 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);
|
||||
}
|
||||
@ -274,17 +266,6 @@ ExtensionPage = class extends BaseContext {
|
||||
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);
|
||||
@ -303,16 +284,11 @@ ExtensionPage = class extends BaseContext {
|
||||
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -43,7 +43,6 @@ var {
|
||||
runSafeSyncWithoutClone,
|
||||
BaseContext,
|
||||
LocaleData,
|
||||
MessageBroker,
|
||||
Messenger,
|
||||
injectAPI,
|
||||
flushJarCache,
|
||||
@ -325,13 +324,12 @@ class ExtensionContext extends BaseContext {
|
||||
};
|
||||
|
||||
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"});
|
||||
|
||||
@ -739,8 +737,6 @@ class ExtensionGlobal {
|
||||
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)
|
||||
@ -753,7 +749,7 @@ class ExtensionGlobal {
|
||||
this.global.sendAsyncMessage("Extension:RemoveTopWindowID", {windowId: this.windowId});
|
||||
}
|
||||
|
||||
get messageFilter() {
|
||||
get messageFilterStrict() {
|
||||
return {
|
||||
innerWindowID: windowId(this.global.content),
|
||||
};
|
||||
@ -858,10 +854,6 @@ this.ExtensionContent = {
|
||||
this.globals.get(global).uninit();
|
||||
this.globals.delete(global);
|
||||
},
|
||||
|
||||
getBroker(messageManager) {
|
||||
return this.globals.get(messageManager).broker;
|
||||
},
|
||||
};
|
||||
|
||||
ExtensionManager.init();
|
||||
|
@ -20,6 +20,8 @@ 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");
|
||||
|
||||
@ -125,11 +127,14 @@ class SpreadArgs extends Array {
|
||||
}
|
||||
}
|
||||
|
||||
let gContextId = 0;
|
||||
|
||||
class BaseContext {
|
||||
constructor() {
|
||||
this.onClose = new Set();
|
||||
this.checkedLastError = false;
|
||||
this._lastError = null;
|
||||
this.contextId = ++gContextId;
|
||||
}
|
||||
|
||||
get cloneScope() {
|
||||
@ -170,6 +175,22 @@ class BaseContext {
|
||||
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;
|
||||
@ -267,6 +288,11 @@ class BaseContext {
|
||||
}
|
||||
|
||||
unload() {
|
||||
MessageChannel.abortResponses({
|
||||
extensionId: this.extension.id,
|
||||
contextId: this.contextId,
|
||||
});
|
||||
|
||||
for (let obj of this.onClose) {
|
||||
obj.close();
|
||||
}
|
||||
@ -656,101 +682,7 @@ function promiseDocumentReady(doc) {
|
||||
* 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) {
|
||||
@ -863,128 +795,122 @@ function getMessageManager(target) {
|
||||
// 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, message, data, recipient) {
|
||||
let options = {
|
||||
recipient,
|
||||
sender: this.sender,
|
||||
responseType: MessageChannel.RESPONSE_FIRST,
|
||||
};
|
||||
|
||||
return this.context.sendMessage(messageManager, message, data, options);
|
||||
},
|
||||
|
||||
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);
|
||||
|
||||
let promise = new Promise((resolve, reject) => {
|
||||
let onClose;
|
||||
let listener = ({data: response}) => {
|
||||
messageManager.removeMessageListener(replyName, listener);
|
||||
this.context.forgetOnClose(onClose);
|
||||
|
||||
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();
|
||||
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 = {
|
||||
messageFilterPermissive: this.filter,
|
||||
|
||||
let mm = getMessageManager(target);
|
||||
let replyName = `Extension:Reply-${recipient.messageId}`;
|
||||
receiveMessage: ({target, data: message, sender, recipient}) => {
|
||||
if (this.delegate) {
|
||||
this.delegate.getSender(this.context, target, sender);
|
||||
}
|
||||
|
||||
new Promise((resolve, reject) => {
|
||||
let sendResponse = Cu.exportFunction(resolve, this.context.cloneScope);
|
||||
let sendResponse;
|
||||
let response = undefined;
|
||||
let promise = new Promise(resolve => {
|
||||
sendResponse = value => {
|
||||
resolve(value);
|
||||
response = promise;
|
||||
};
|
||||
});
|
||||
|
||||
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 = {
|
||||
messageFilterPermissive: 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();
|
||||
},
|
||||
@ -1042,7 +968,6 @@ this.ExtensionUtils = {
|
||||
DefaultWeakMap,
|
||||
EventManager,
|
||||
LocaleData,
|
||||
MessageBroker,
|
||||
Messenger,
|
||||
PlatformInfo,
|
||||
SingletonEventManager,
|
||||
|
@ -11,9 +11,9 @@
|
||||
* are no matching listeners, or the message manager disconnects before
|
||||
* a reply is received, the caller is returned an error.
|
||||
*
|
||||
* Since each message must have only one recipient, the listener end may
|
||||
* specify filters for the messages it wishes to receive, and the sender
|
||||
* end likewise may specify recipient tags to match the filters.
|
||||
* The listener end may specify filters for the messages it wishes to
|
||||
* receive, and the sender end likewise may specify recipient tags to
|
||||
* match the filters.
|
||||
*
|
||||
* The message handler on the listener side may return its response
|
||||
* value directly, or may return a promise, the resolution or rejection
|
||||
@ -34,10 +34,14 @@
|
||||
* messageManager, "ContentScript:TouchContent",
|
||||
* this);
|
||||
*
|
||||
* this.messageFilter = {
|
||||
* this.messageFilterStrict = {
|
||||
* innerWindowID: getInnerWindowID(window),
|
||||
* extensionID: extensionID,
|
||||
* };
|
||||
*
|
||||
* this.messageFilterPermissive = {
|
||||
* outerWindowID: getOuterWindowID(window),
|
||||
* };
|
||||
* },
|
||||
*
|
||||
* receiveMessage({ target, messageName, sender, recipient, data }) {
|
||||
@ -61,7 +65,7 @@
|
||||
*
|
||||
* MessageChannel.sendMessage(
|
||||
* tab.linkedBrowser.messageManager, "ContentScript:TouchContent",
|
||||
* data, recipient, sender
|
||||
* data, {recipient, sender}
|
||||
* ).then(result => {
|
||||
* alert(result.touchResult);
|
||||
* });
|
||||
@ -152,19 +156,8 @@ class FilteringMessageManager {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -179,7 +172,8 @@ class FilteringMessageManager {
|
||||
* 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.messageFilterStrict || {}, recipient) &&
|
||||
MessageChannel.matchesFilter(handler.messageFilterPermissive || {}, recipient, false)) {
|
||||
yield handler;
|
||||
}
|
||||
}
|
||||
@ -191,9 +185,12 @@ class FilteringMessageManager {
|
||||
* @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.
|
||||
* An opaque handler object. The object may have a
|
||||
* `messageFilterStrict` and/or a `messageFilterPermissive`
|
||||
* 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)) {
|
||||
@ -298,6 +295,7 @@ this.MessageChannel = {
|
||||
RESULT_NO_HANDLER: 2,
|
||||
RESULT_MULTIPLE_HANDLERS: 3,
|
||||
RESULT_ERROR: 4,
|
||||
RESULT_NO_RESPONSE: 5,
|
||||
|
||||
REASON_DISCONNECTED: {
|
||||
result: this.RESULT_DISCONNECTED,
|
||||
@ -305,27 +303,72 @@ this.MessageChannel = {
|
||||
},
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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.
|
||||
*
|
||||
* Only handlers which return a value other than `undefined` are
|
||||
* considered to have responded. Returning a Promise which evaluates
|
||||
* to `undefined` is interpreted as an explicit response.
|
||||
*
|
||||
* 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 peroperties of the `data` object match those in
|
||||
* the `filter` object. Matching is done on a strict equality basis,
|
||||
* and the behavior varies depending on the value of the `strict`
|
||||
* parameter.
|
||||
*
|
||||
* @param {object} filter
|
||||
* The filter object to match against.
|
||||
* @param {object} data
|
||||
* The data object being matched.
|
||||
* @param {boolean} [strict=false]
|
||||
* If true, all properties in the `filter` object have a
|
||||
* corresponding property in `data` with the same value. If
|
||||
* false, properties present in both objects must have the same
|
||||
* balue.
|
||||
* @returns {bool} True if the objects match.
|
||||
*/
|
||||
matchesFilter(filter, data) {
|
||||
matchesFilter(filter, data, strict = true) {
|
||||
if (strict) {
|
||||
return Object.keys(filter).every(key => {
|
||||
return key in data && data[key] === filter[key];
|
||||
});
|
||||
}
|
||||
return Object.keys(filter).every(key => {
|
||||
return key in data && data[key] === filter[key];
|
||||
return !(key in data) || data[key] === filter[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
|
||||
@ -363,28 +406,41 @@ this.MessageChannel = {
|
||||
* resolution or rejection value of which will likewise be
|
||||
* returned to the message sender.
|
||||
*
|
||||
* messageFilter:
|
||||
* messageFilterStrict:
|
||||
* 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`.
|
||||
* matches this filter, as determined by `matchesFilter` with
|
||||
* `strict=true`.
|
||||
*
|
||||
* messageFilterPermissive:
|
||||
* 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` with
|
||||
* `strict=false`.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@ -401,23 +457,32 @@ this.MessageChannel = {
|
||||
* @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]
|
||||
* recipient. The object must match the `messageFilterStrict` and
|
||||
* `messageFilterPermissive` filters defined by recipients in order
|
||||
* for the message to be received.
|
||||
* @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 || this.RESPONSE_SINGLE;
|
||||
|
||||
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;
|
||||
|
||||
@ -438,6 +503,54 @@ this.MessageChannel = {
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
_callHandlers(handlers, data) {
|
||||
let responseType = data.responseType;
|
||||
|
||||
// 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}`});
|
||||
}
|
||||
|
||||
// Note: We use `new Promise` rather than `Promise.resolve` here
|
||||
// so that errors from the handler are trapped and converted into
|
||||
// rejected promises.
|
||||
return new Promise(resolve => {
|
||||
resolve(handlers[0].receiveMessage(data));
|
||||
});
|
||||
}
|
||||
|
||||
let responses = handlers.map(handler => {
|
||||
try {
|
||||
return handler.receiveMessage(data);
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
});
|
||||
responses = responses.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
|
||||
@ -446,7 +559,7 @@ this.MessageChannel = {
|
||||
* 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.
|
||||
@ -462,12 +575,7 @@ this.MessageChannel = {
|
||||
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 = {
|
||||
@ -513,15 +621,17 @@ this.MessageChannel = {
|
||||
* 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);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -90,7 +90,7 @@ extensions.registerSchemaAPI("webNavigation", "webNavigation", (extension, conte
|
||||
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) {
|
||||
@ -104,7 +104,7 @@ extensions.registerSchemaAPI("webNavigation", "webNavigation", (extension, conte
|
||||
};
|
||||
|
||||
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) :
|
||||
|
@ -47,6 +47,7 @@ skip-if = buildapp == 'b2g' # port.sender.tab is undefined on b2g.
|
||||
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.
|
||||
|
@ -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>
|
Loading…
Reference in New Issue
Block a user