UNPKG

@cliqz-oss/firefox-client

Version:
248 lines (210 loc) 7.01 kB
var net = require("net"), events = require("events"), extend = require("./extend"); var colors = require("colors"); module.exports = Client; // this is very unfortunate! and temporary. we can't // rely on 'type' property to signify an event, and we // need to write clients for each actor to handle differences // in actor protocols var unsolicitedEvents = { "tabNavigated": "tabNavigated", "styleApplied": "styleApplied", "propertyChange": "propertyChange", "networkEventUpdate": "networkEventUpdate", "networkEvent": "networkEvent", "propertyChange": "propertyChange", "newMutations": "newMutations", "appOpen": "appOpen", "appClose": "appClose", "appInstall": "appInstall", "appUninstall": "appUninstall", "frameUpdate": "frameUpdate", "tabListChanged": "tabListChanged" }; /** * a Client object handles connecting with a Firefox remote debugging * server instance (e.g. a Firefox instance), plus sending and receiving * packets on that conection using the Firefox remote debugging protocol. * * Important methods: * connect - Create the connection to the server. * makeRequest - Make a request to the server with a JSON message, * and a callback to call with the response. * * Important events: * 'message' - An unsolicited (e.g. not a response to a prior request) * packet has been received. These packets usually describe events. */ function Client(options) { this.options = options || {}; this.incoming = new Buffer(""); this._pendingRequests = []; this._activeRequests = {}; } Client.prototype = extend(events.EventEmitter.prototype, { connect: function(port, host, cb) { this.client = net.createConnection({ port: port, host: host }); this.client.on("connect", cb); this.client.on("data", this.onData.bind(this)); this.client.on("error", this.onError.bind(this)); this.client.on("end", this.onEnd.bind(this)); this.client.on("timeout", this.onTimeout.bind(this)); }, disconnect: function() { if (this.client) { this.client.end(); } }, /** * Set a request to be sent to an actor on the server. If the actor * is already handling a request, queue this request until the actor * has responded to the previous request. * * @param {object} request * Message to be JSON-ified and sent to server. * @param {function} callback * Function that's called with the response from the server. */ makeRequest: function(request, callback) { this.log("request: " + JSON.stringify(request).green); if (!request.to) { var type = request.type || ""; throw new Error(type + " request packet has no destination."); } this._pendingRequests.push({ to: request.to, message: request, callback: callback }); this._flushRequests(); }, /** * Activate (send) any pending requests to actors that don't have an * active request. */ _flushRequests: function() { this._pendingRequests = this._pendingRequests.filter(function(request) { // only one active request per actor at a time if (this._activeRequests[request.to]) { return true; } // no active requests for this actor, so activate this one this.sendMessage(request.message); this.expectReply(request.to, request.callback); // remove from pending requests return false; }.bind(this)); }, /** * Send a JSON message over the connection to the server. */ sendMessage: function(message) { if (!message.to) { throw new Error("No actor specified in request"); } if (!this.client) { throw new Error("Not connected, connect() before sending requests"); } var str = JSON.stringify(message); // message is preceded by byteLength(message): str = (new Buffer(str).length) + ":" + str; this.client.write(str); }, /** * Arrange to hand the next reply from |actor| to |handler|. */ expectReply: function(actor, handler) { if (this._activeRequests[actor]) { throw Error("clashing handlers for next reply from " + uneval(actor)); } this._activeRequests[actor] = handler; }, /** * Handler for a new message coming in. It's either an unsolicited event * from the server, or a response to a previous request from the client. */ handleMessage: function(message) { if (!message.from) { if (message.error) { throw new Error(message.message); } throw new Error("Server didn't specify an actor: " + JSON.stringify(message)); } if (!(message.type in unsolicitedEvents) && this._activeRequests[message.from]) { this.log("response: " + JSON.stringify(message).yellow); var callback = this._activeRequests[message.from]; delete this._activeRequests[message.from]; callback(message); this._flushRequests(); } else if (message.type) { // this is an unsolicited event from the server this.log("unsolicited event: ".grey + JSON.stringify(message).grey); this.emit('message', message); return; } else { throw new Error("Unexpected packet from actor " + message.from + JSON.stringify(message)); } }, /** * Called when a new data chunk is received on the connection. * Parse data into message(s) and call message handler for any full * messages that are read in. */ onData: function(data) { this.incoming = Buffer.concat([this.incoming, data]); while(this.readMessage()) {}; }, /** * Parse out and process the next message from the data read from * the connection. Returns true if a full meassage was parsed, false * otherwise. */ readMessage: function() { var sep = this.incoming.toString().indexOf(':'); if (sep < 0) { return false; } // beginning of a message is preceded by byteLength(message) + ":" var count = parseInt(this.incoming.slice(0, sep)); if (this.incoming.length - (sep + 1) < count) { this.log("no complete response yet".grey); return false; } this.incoming = this.incoming.slice(sep + 1); var packet = this.incoming.slice(0, count); this.incoming = this.incoming.slice(count); var message; try { message = JSON.parse(packet.toString()); } catch(e) { throw new Error("Couldn't parse packet from server as JSON " + e + ", message:\n" + packet); } this.handleMessage(message); return true; }, onError: function(error) { var code = error.code ? error.code : error; this.log("connection error: ".red + code.red); this.emit("error", error); }, onEnd: function() { this.log("connection closed by server".red); this.emit("end"); }, onTimeout: function() { this.log("connection timeout".red); this.emit("timeout"); }, log: function(str) { if (this.options.log) { console.log(str); } } })