| 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" |
| }; |
| |
| /** |
| * 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); |
| } |
| } |
| }) |