var util = Require('util'); // util.inherits var net = Require('net'); var handshake = Require('x11/core/handshake'); //var xevents = require('./xevents'); var EventEmitter = Require('events').EventEmitter; var PackStream = Require('x11/core/unpackstream'); var hexy = Require('x11/core/hexy').hexy; var Buffer = Require('buffer').Buffer; // add 'unpack' method for buffer Require('x11/core/unpackbuffer').addUnpack(Buffer); var os = require('os'); var xerrors = Require('x11/core/xerrors'); var coreRequests = Require('x11/core/corereqs'); var stdatoms = Require('x11/core/stdatoms'); var em = Require('x11/core/eventmask').eventMask; function stash () { Require('x11/core/ext/apple-wm'); Require('x11/core/ext/big-requests'); Require('x11/core/ext/composite'); Require('x11/core/ext/damage'); Require('x11/core/ext/dpms'); Require('x11/core/ext/fixes'); Require('x11/core/ext/glxconstants'); Require('x11/core/ext/glx'); Require('x11/core/ext/glxrender'); Require('x11/core/ext/randr'); Require('x11/core/ext/render'); Require('x11/core/ext/screen-saver'); Require('x11/core/ext/shape'); Require('x11/core/ext/xc-misc'); Require('x11/core/ext/xtest'); } if (process.argv.indexOf('-buildx11')!=-1) stash(); function XClient(displayNum, screenNum, options) { EventEmitter.call(this); this.options = options ? options : {}; // TODO: this is probably not used this.core_requests = {}; this.ext_requests = {}; this.displayNum = displayNum; this.screenNum = screenNum; } util.inherits(XClient, EventEmitter); XClient.prototype.init = function(stream) { this.stream = stream; this.authHost = stream.remoteAddress; // Node v0.10.x does not have stream.remoteFamily, so dig in to find it this.authFamily = stream._getpeername ? stream._getpeername().family : stream.remoteFamily; if (!this.authHost || this.authHost === '127.0.0.1' || this.authHost === '::1') { this.authHost = os.hostname(); this.authFamily = null; } var pack_stream = new PackStream(); // data received from stream is dispached to // read requests set by calls to .unpack and .unpackTo //stream.pipe(pack_stream); // pack_stream write requests are buffered and // flushed to stream as result of call to .flush // TODO: listen for drain event and flush automatically //pack_stream.pipe(stream); var client = this; pack_stream.on('data', function( data ) { //console.error(hexy(data, {prefix: 'from packer '})); //for (var i=0; i < data.length; ++i) // console.log('<<< ' + data[i]); stream.write(data); }); stream.on('data', function( data ) { //console.error(hexy(data, {prefix: 'to unpacker '})); //for (var i=0; i < data.length; ++i) // console.log('>>> ' + data[i]); pack_stream.write(data); }); stream.on('end', function() { client.emit('end'); }); this.pack_stream = pack_stream; this.rsrc_id = 0; // generated for each new resource var cli = this; if (cli.options.debug) { this.seq_num_ = 0; this.seq2stack = {}; // debug: map seq_num to stack at the moment request was issued Object.defineProperty(cli, "seq_num", { set : function(v) { cli.seq_num_ = v; var err = new Error(); Error.captureStackTrace(err, arguments.callee); err.timestamp = Date.now(); cli.seq2stack[client.seq_num] = err; }, get: function() { return cli.seq_num_; } }); } else { this.seq_num = 0; } // in/out packets indexed by sequence ID this.replies = {}; this.atoms = stdatoms; this.atom_names = (function() { var names = {}; Object.keys(stdatoms).forEach(function(key) { names[stdatoms[key]] = key; }); return names; })(); this.eventMask = em; this.event_consumers = {}; // maps window id to eventemitter TODO: bad name this.eventParsers = {}; this.errorParsers = {}; this._extensions = {}; this.importRequestsFromTemplates(this, coreRequests); this.startHandshake(); this._closing = false; this._unusedIds = []; } // TODO: close() = set 'closing' flag, watch it in replies and writeQueue, terminate if empty XClient.prototype.terminate = function() { this.stream.end(); } // GetAtomName used as cheapest non-modifying request with reply // 3 - id for shortest standard atom, "ARC" XClient.prototype.ping = function(cb) { var start = Date.now(); this.GetAtomName(3, function(err, str) { if (err) return cb(err); return cb(null, Date.now() - start); }); } XClient.prototype.close = function(cb) { var cli = this; cli.ping(function(err) { if (err) return cb(err); cli.terminate(); if (cb) cb(); }); cli._closing = true; } XClient.prototype.importRequestsFromTemplates = function(target, reqs) { var client = this; this.pending_atoms = {}; for (var r in reqs) { // r is request name target[r] = (function(reqName) { var reqFunc = function req_proxy() { if (client._closing) throw new Error('client is in closing state'); // simple overflow handling (this means that currently there is no way to have more than 65535 requests in the queue // TODO: edge cases testing if (client.seq_num == 65535) client.seq_num = 0; else client.seq_num++; // is it fast? var args = Array.prototype.slice.call(req_proxy.arguments); var callback = args.length > 0 ? args[args.length - 1] : null; if (callback && callback.constructor.name != 'Function') callback = null; // TODO: see how much we can calculate in advance (not in each request) var reqReplTemplate = reqs[reqName]; var reqTemplate = reqReplTemplate[0]; var templateType = typeof reqTemplate; if (templateType == 'object') templateType = reqTemplate.constructor.name; if (templateType == 'function') { if (reqName === 'InternAtom') { var value = req_proxy.arguments[1]; if (client.atoms[value]) { -- client.seq_num; return setImmediate(function() { callback(undefined, client.atoms[value]); }); } else { client.pending_atoms[client.seq_num] = value; } } // call template with input arguments (not including callback which is last argument TODO currently with callback. won't hurt) //reqPack = reqTemplate.call(args); var reqPack = reqTemplate.apply(this, req_proxy.arguments); var format = reqPack[0]; var requestArguments = reqPack[1]; if (callback) this.replies[this.seq_num] = [reqReplTemplate[1], callback]; client.pack_stream.pack(format, requestArguments); var b = client.pack_stream.write_queue[0]; client.pack_stream.flush(); } else if (templateType == 'Array'){ if (reqName === 'GetAtomName') { var atom = req_proxy.arguments[0]; if (client.atom_names[atom]) { -- client.seq_num; return setImmediate(function() { callback(undefined, client.atom_names[atom]); }); } else { client.pending_atoms[client.seq_num] = atom; } } var format = reqTemplate[0]; var requestArguments = []; for (var a = 0; a < reqTemplate[1].length; ++a) requestArguments.push(reqTemplate[1][a]); for (var a in args) requestArguments.push(args[a]); if (callback) this.replies[this.seq_num] = [reqReplTemplate[1], callback]; client.pack_stream.pack(format, requestArguments); client.pack_stream.flush(); } else { throw 'unknown request format - ' + templateType; } } return reqFunc; })(r); } } XClient.prototype.AllocID = function() { if (this._unusedIds.length > 0) { return this._unusedIds.pop(); } // TODO: handle overflow (XCMiscGetXIDRange from XC_MISC ext) this.display.rsrc_id++; return (this.display.rsrc_id << this.display.rsrc_shift) + this.display.resource_base; }; XClient.prototype.ReleaseID = function(id) { this._unusedIds.push(id); }; // TODO: move core events unpackers to corereqs.js XClient.prototype.unpackEvent = function(type, seq, extra, code, raw, headerBuf) { var event = {}; // TODO: constructor & base functions // Remove the most significant bit. See Chapter 1, Event Format section in X11 protocol // specification type = type & 0x7F; event.type = type; event.seq = seq; var extUnpacker = this.eventParsers[type]; if (extUnpacker) { return extUnpacker(type, seq, extra, code, raw); } if (type == 2 || type == 3 || type == 4 || type == 5 || type == 6) { // motion event var values = raw.unpack('LLLssssSC'); //event.raw = values; // TODO: use unpackTo??? event.name = [,,'KeyPress', 'KeyRelease', 'ButtonPress', 'ButtonRelease', 'MotionNotify'][type] event.time = extra; event.keycode = code; event.root = values[0]; event.wid = values[1]; event.child = values[2]; event.rootx = values[3]; event.rooty = values[4]; event.x = values[5]; event.y = values[6]; event.buttons = values[7]; event.sameScreen = values[8]; } else if (type == 7 || type == 8) { //EnterNotify || LeaveNotify event.name = type === 7 ? 'EnterNotify' : 'LeaveNotify'; var values = raw.unpack('LLLssssSC'); event.root = values[0] event.wid = values[1] event.child = values[2]; event.rootx = values[3]; event.rooty = values[4]; event.x = values[5]; event.y = values[6]; event.values = values } else if (type == 12) { // Expose var values = raw.unpack('SSSSS'); event.name = 'Expose' event.wid = extra; event.x = values[0]; event.y = values[1]; event.width = values[2]; event.height = values[3]; event.count = values[4]; // TODO: ??? } else if (type == 16) { // CreateNotify var values = raw.unpack('LssSSSc'); event.name = 'CreateNotify' event.parent = extra; event.wid = values[0]; event.x = values[1]; event.y = values[2]; event.width = values[3]; event.height = values[4]; event.borderWidth = values[5]; event.overrideRedirect = values[6] ? true : false; // x, y, width, height, border } else if (type == 17) { // destroy notify var values = raw.unpack('L'); event.name = 'DestroyNotify' event.event = extra; event.wid = values[0]; } else if (type == 18) { // UnmapNotify var values = raw.unpack('LC'); event.name = 'UnmapNotify' event.event = extra; event.wid = values[0]; event.fromConfigure = values[1] ? true : false; } else if (type == 19) { // MapNotify var values = raw.unpack('LC'); event.name = 'MapNotify' event.event = extra; event.wid = values[0]; event.overrideRedirect = values[1] ? true : false; } else if (type == 20) { var values = raw.unpack('L'); event.name = 'MapRequest' event.parent = extra; event.wid = values[0]; } else if (type == 22) { var values = raw.unpack('LLssSSSC'); event.name = 'ConfigureNotify'; event.wid = extra; // TODO rename event.wid1 = values[0]; event.aboveSibling = values[1]; event.x = values[2]; event.y = values[3]; event.width = values[4]; event.height = values[5]; event.borderWidth = values[6]; event.overrideRedirect = values[7]; } else if (type == 23) { var values = raw.unpack('LLssSSSS'); event.name = 'ConfigureRequest'; event.stackMode = code; event.parent = extra; event.wid = values[0]; event.sibling = values[1]; event.x = values[2] event.y = values[3] event.width = values[4] event.height = values[5] event.borderWidth = values[6]; // // The value-mask indicates which components were specified in // the request. The value-mask and the corresponding values are reported as given // in the request. The remaining values are filled in from the current geometry of the // window, except in the case of sibling and stack-mode, which are reported as None // and Above (respectively) if not given in the request. event.mask = values[6]; // 322, [ 12582925, 0, 0, 484, 316, 1, 12, 0 //console.log([extra, code, values]); } else if (type == 28) {// PropertyNotify event.name = 'PropertyNotify'; var values = raw.unpack('LLC'); event.wid = extra; event.atom = values[0]; event.time = values[1]; event.state = values[2]; } else if (type == 29) {// SelectionClear event.name = 'SelectionClear'; event.time = extra; var values = raw.unpack('LL'); event.owner = values[0]; event.selection = values[1]; } else if (type == 30) {// SelectionRequest event.name = 'SelectionRequest'; // TODO check this event.time = extra; var values = raw.unpack('LLLLL'); event.owner = values[0]; event.requestor = values[1]; event.selection = values[2]; event.target = values[3]; event.property = values[4]; } else if (type == 31) {// SelectionNotify event.name = 'SelectionNotify'; // TODO check this event.time = extra; var values = raw.unpack('LLLL'); event.requestor = values[0]; event.selection = values[1]; event.target = values[2]; event.property = values[3]; } else if (type == 33) {// ClientMessage event.name = 'ClientMessage'; event.format = code; event.wid = extra; event.message_type = raw.unpack('L')[0]; var format = (code === 32) ? 'LLLLL' : (code === 16) ? 'SSSSSSSSSS' : 'CCCCCCCCCCCCCCCCCCCC'; event.data = raw.unpack(format, 4); } else if (type == 34) { event.name = 'MappingNotify'; event.request = headerBuf[4]; event.firstKeyCode = headerBuf[5]; event.count = headerBuf[6]; } return event; } XClient.prototype.expectReplyHeader = function() { // TODO: move error parsers to corereqs.js var client = this; client.pack_stream.get( 8, function(headerBuf) { var res = headerBuf.unpack('CCSL'); var type = res[0]; var seq_num = res[2]; var bad_value = res[3]; if (type == 0) { var error_code = res[1]; var error = new Error(); error.error = error_code; error.seq = seq_num; if (client.options.debug) { error.longstack = client.seq2stack[error.seq] console.log(client.seq2stack[error.seq].stack); } // unpack error packet (32 bytes for all error types, 8 of them in CCSL header) client.pack_stream.get(24, function(buf) { var res = buf.unpack('SC'); error.message = xerrors.errorText[error_code]; error.badParam = bad_value; error.minorOpcode = res[0]; error.majorOpcode = res[1]; var extUnpacker = client.errorParsers[error_code]; if (extUnpacker) { extUnpacker(error, error_code, seq_num, bad_value, buf); } var handler = client.replies[seq_num]; if (handler) { var callback = handler[1]; var handled = callback(error); if (!handled) client.emit('error', error); // TODO: should we delete seq2stack and reply even if there is no handler? if (client.options.debug) delete client.seq2stack[seq_num]; delete client.replies[seq_num]; } else client.emit('error', error); client.expectReplyHeader(); } ); return; } else if (type > 1) { client.pack_stream.get(24, function(buf) { var extra = res[3]; var code = res[1]; var ev = client.unpackEvent(type, seq_num, extra, code, buf, headerBuf); // raw event 32-bytes packet (primarily for use in SendEvent); // TODO: Event::pack based on event parameters, inverse to unpackEvent ev.rawData = new Buffer(32); headerBuf.copy(ev.rawData); buf.copy(ev.rawData, 8); client.emit('event', ev); var ee = client.event_consumers[ev.wid]; if (ee) { ee.emit('event', ev); } if (ev.parent) { ee = client.event_consumers[ev.parent]; if (ee) ee.emit('child-event', ev); } client.expectReplyHeader(); } ); return; } var opt_data = res[1]; var length_total = res[3]; // in 4-bytes units, _including_ this header var bodylength = 24 + length_total*4; // 24 is rest if 32-bytes header client.pack_stream.get( bodylength, function( data ) { var handler = client.replies[seq_num]; if (handler) { var unpack = handler[0]; if (client.pending_atoms[seq_num]) { opt_data = seq_num; } var result = unpack.call(client, data, opt_data); var callback = handler[1]; callback(null, result); // TODO: add multiple replies flag and delete handler only after last reply (eg ListFontsWithInfo) delete client.replies[seq_num]; } // wait for new packet from server client.expectReplyHeader(); }); } ); } XClient.prototype.startHandshake = function() { var client = this; handshake.writeClientHello(this.pack_stream, this.displayNum, this.authHost, this.authFamily); handshake.readServerHello(this.pack_stream, function(err, display) { if (err) { client.emit('error', err); return; } client.expectReplyHeader(); client.display = display; display.client = client; client.emit('connect', display); }); } XClient.prototype.require = function(extName, callback) { var self = this; var ext = this._extensions[extName]; if (ext) { return process.nextTick(function() { callback(null, ext); }); } ext = Require('x11/core/ext/' + extName); ext.requireExt(this.display, function(err, _ext) { if (err) { return callback(err); } self._extensions[extName] = _ext; callback(null, _ext); }); }; module.exports.createClient = function(options, initCb) { if (typeof options === 'function') { initCb = options; options = {}; } if (!options) options = {}; var display = options.display; if (!display) display = (process.env.DISPLAY) ? process.env.DISPLAY : ':0'; var displayMatch = display.match(/^(?:[^:]*?\/)?(.*):(\d+)(?:.(\d+))?$/); if (!displayMatch) throw new Error("Cannot parse display"); var host = displayMatch[1]; var displayNum = displayMatch[2]; if (!displayNum) displayNum = 0; var screenNum = displayMatch[3]; if (!screenNum) screenNum = 0; // open stream var stream; var connected = false; var cbCalled = false; var socketPath; // try local socket on non-windows platforms if ( ['cygwin', 'win32', 'win64'].indexOf(process.platform) < 0 ) { if (process.platform == 'darwin' || process.platform == 'mac') { // socket path on OSX is /tmp/launch-(some id)/org.x:0 if (display[0] == '/') { socketPath = display; } } else if(!host) socketPath = '/tmp/.X11-unix/X' + displayNum; } var client = new XClient(displayNum, screenNum, options); var connectStream = function() { if (socketPath) { stream = net.createConnection(socketPath); } else { stream = net.createConnection(6000 + parseInt(displayNum), host); } stream.on('connect', function() { connected = true; client.init(stream); }); stream.on('error', function(err) { if (!connected && socketPath && err.code === 'ENOENT') { // Retry connection with TCP on localhost socketPath = null; host = 'localhost'; connectStream(); } else if (initCb && !cbCalled) { cbCalled = true; initCb(err); } else { client.emit('error', err); } }); }; connectStream(); if (initCb) { client.on('connect', function(display) { // opt-in BigReq if (!options.disableBigRequests) { client.require('big-requests', function(err, BigReq) { if (err) return initCb(err) BigReq.Enable(function(err, maxLen) { display.max_request_length = maxLen; cbCalled = true; initCb(undefined, display); }); }); } else { cbCalled = true; initCb(undefined, display); } }); } return client; }