/* * This file is part of Adblock Plus , * Copyright (C) 2006-2017 eyeo GmbH * * Adblock Plus is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * Adblock Plus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Adblock Plus. If not, see . */ "use strict"; let {EventEmitter} = require("events"); const MESSAGE_NAME = "AdblockPlus:Message"; const RESPONSE_NAME = "AdblockPlus:Response"; function isPromise(value) { // value instanceof Promise won't work - there can be different Promise // classes (e.g. in different contexts) and there can also be promise-like // classes (e.g. Task). return (value && typeof value.then == "function"); } function sendMessage(messageManager, messageName, payload, callbackID) { let request = {messageName, payload, callbackID}; if (messageManager instanceof Ci.nsIMessageSender) { messageManager.sendAsyncMessage(MESSAGE_NAME, request); return 1; } else if (messageManager instanceof Ci.nsIMessageBroadcaster) { messageManager.broadcastAsyncMessage(MESSAGE_NAME, request); return messageManager.childCount; } else { Cu.reportError("Unexpected message manager, impossible to send message"); return 0; } } function sendSyncMessage(messageManager, messageName, payload) { let request = {messageName, payload}; let responses = messageManager.sendRpcMessage(MESSAGE_NAME, request); let processor = new ResponseProcessor(messageName); for (let response of responses) processor.add(response); return processor.value; } function ResponseProcessor(messageName) { this.value = undefined; this.add = function(response) { if (typeof response == "undefined") return; if (typeof this.value == "undefined") this.value = response; else Cu.reportError("Got multiple responses to message '" + messageName + "', only first response was accepted."); }; } function getSender(origin) { if (origin instanceof Ci.nsIDOMXULElement) origin = origin.messageManager; if (origin instanceof Ci.nsIMessageSender) return new LightWeightPort(origin); else return null; } /** * Lightweight communication port allowing only sending messages. * @param {nsIMessageManager} messageManager * @constructor */ function LightWeightPort(messageManager) { this._messageManager = messageManager; } LightWeightPort.prototype = { /** * @see Port#emit */ emit: function(messageName, payload) { sendMessage(this._messageManager, messageName, payload); }, /** * @see Port#emitSync */ emitSync: function(messageName, payload) { return sendSyncMessage(this._messageManager, messageName, payload); } }; /** * Communication port wrapping the message manager API to send and receive * messages. * @param {nsIMessageManager} messageManager * @constructor */ function Port(messageManager) { this._messageManager = messageManager; this._eventEmitter = new EventEmitter(); this._responseCallbacks = new Map(); this._responseCallbackCounter = 0; this._handleRequest = this._handleRequest.bind(this); this._handleResponse = this._handleResponse.bind(this); this._messageManager.addMessageListener(MESSAGE_NAME, this._handleRequest); this._messageManager.addMessageListener(RESPONSE_NAME, this._handleResponse); } Port.prototype = { /** * Disables the port and makes it stop listening to incoming messages. */ disconnect: function() { this._messageManager.removeMessageListener(MESSAGE_NAME, this._handleRequest); this._messageManager.removeMessageListener(RESPONSE_NAME, this._handleResponse); }, _sendResponse: function(sender, callbackID, payload) { if (!sender || typeof callbackID == "undefined") return; let response = {callbackID, payload}; sender._messageManager.sendAsyncMessage(RESPONSE_NAME, response); }, _handleRequest: function(message) { let sender = getSender(message.target); let {callbackID, messageName, payload} = message.data; let result = this._dispatch(messageName, payload, sender); if (isPromise(result)) { // This is a promise - asynchronous response if (message.sync) { Cu.reportError("Asynchronous response to the synchronous message '" + messageName + "' is not possible"); return undefined; } result.then(result => { this._sendResponse(sender, callbackID, result) }, e => { Cu.reportError(e); this._sendResponse(sender, callbackID, undefined); }); } else this._sendResponse(sender, callbackID, result); return result; }, _handleResponse: function(message) { let {callbackID, payload} = message.data; let callbackData = this._responseCallbacks.get(callbackID); if (!callbackData) return; let [callback, processor, expectedResponses] = callbackData; try { processor.add(payload); } catch (e) { Cu.reportError(e); } callbackData[2] = --expectedResponses; if (expectedResponses <= 0) { this._responseCallbacks.delete(callbackID); callback(processor.value); } }, _dispatch: function(messageName, payload, sender) { let callbacks = this._eventEmitter.listeners(messageName); let processor = new ResponseProcessor(messageName); for (let callback of callbacks) { try { processor.add(callback(payload, sender)); } catch (e) { Cu.reportError(e); } } return processor.value; }, /** * Function to be called when a particular message is received * @callback Port~messageHandler * @param payload data attached to the message if any * @param {LightWeightPort} sender object that can be used to communicate with * the sender of the message, could be null * @return the handler can return undefined (no response), a value (response * to be sent to sender immediately) or a promise (asynchronous * response). */ /** * Adds a handler for the specified message. * @param {string} messageName message that would trigger the callback * @param {Port~messageHandler} callback */ on: function(messageName, callback) { this._eventEmitter.on(messageName, callback); }, /** * Removes a handler for the specified message. * @param {string} messageName message that would trigger the callback * @param {Port~messageHandler} callback */ off: function(messageName, callback) { this._eventEmitter.off(messageName, callback); }, /** * Sends a message. * @param {string} messageName message identifier * @param [payload] data to attach to the message */ emit: function(messageName, payload) { sendMessage(this._messageManager, messageName, payload, undefined); }, /** * Sends a message and expects a response. * @param {string} messageName message identifier * @param [payload] data to attach to the message * @return {Promise} promise that will be resolved with the response */ emitWithResponse: function(messageName, payload) { let callbackID = ++this._responseCallbackCounter; let expectedResponses = sendMessage( this._messageManager, messageName, payload, callbackID); return new Promise((resolve, reject) => { this._responseCallbacks.set(callbackID, [resolve, new ResponseProcessor(messageName), expectedResponses]); }); }, /** * Sends a synchonous message (DO NOT USE unless absolutely unavoidable). * @param {string} messageName message identifier * @param [payload] data to attach to the message * @return response returned by the handler */ emitSync: function(messageName, payload) { return sendSyncMessage(this._messageManager, messageName, payload); } }; exports.Port = Port; let messageManager; try { // Child messageManager = require("messageManager"); } catch (e) { // Parent messageManager = Cc["@mozilla.org/parentprocessmessagemanager;1"] .getService(Ci.nsIMessageListenerManager); } let port = new Port(messageManager); onShutdown.add(() => port.disconnect()); exports.port = port;