diff --git a/README.md b/README.md index 49958bd..4ae59ee 100644 --- a/README.md +++ b/README.md @@ -3,25 +3,18 @@ # status -soon to be released at stage 2) ( see [roadmap.txt](node-x11/blob/master/roadmap.txt) ) +stage 2) ( see [roadmap.txt](node-x11/blob/master/roadmap.txt) ) +next todo: dispatch replies and errors, decode all evnt types # example - var x = require('x11'); - - var s = x.createConnection().defaultScreen(); - var wnd = s.createWindow(10, 10, 100, 100); - // adding event callback also selects event on server - wnd - .on('expose', function(exposeevent) - { - this.drawString(10, 50, 'Hello'); - }) - .on('keypress', function(keyevent) - { - process.exit(0); - }); - + var X = require('x11').createClient(); + X.on('connect', function(display) { + var root = display.screen[0].root; + var wid = X.AllocID(); + X.CreateWindow(wid, root, 10, 10, 400, 300, 1, 1, 0, { backgroundPixel: 0, eventMask: 0x00000040 }); + X.MapWindow(wid); + }); # Protocol documentation diff --git a/lib/x11/auth.js b/lib/x11/auth.js new file mode 100644 index 0000000..6831e15 --- /dev/null +++ b/lib/x11/auth.js @@ -0,0 +1,11 @@ +// TODO: http://en.wikipedia.org/wiki/X_Window_authorization + +module.exports = function( cb ) +{ + // empty yet + var authType = ''; + var authData = ''; + cb( authType, authData ); +} + +// TODO: rewrite to allow negotiation of auth type with server diff --git a/lib/x11/corereqs.js b/lib/x11/corereqs.js new file mode 100644 index 0000000..0a6b1aa --- /dev/null +++ b/lib/x11/corereqs.js @@ -0,0 +1,99 @@ +var valueMask = { + backgroundPixmap: 0x00000001, + backgroundPixel : 0x00000002, + borderPixmap : 0x00000004, + borderPixel : 0x00000008, + bitGrawity : 0x00000010, + winGravity : 0x00000020, + backingStore : 0x00000040, + backingPlanes : 0x00000080, + backingPixel : 0x00000100, + overrideRedirect: 0x00000200, + saveUnder : 0x00000400, + eventMask : 0x00000800, + doNotPropagateMask: 0x00001000, + colormap : 0x00002000, + cursor : 0x00004000 +}; + +var valueMaskNames = {}; +for (var m in valueMask) { + valueMaskNames[valueMask[m]] = m; +} + +/* + +the way requests are described here + +- outgoing request + + 1) as function + client.CreateWindow( params, params ) -> + req = reqs.CreateWindow[0]( param, param ); + pack_stream.pack(req[0], req[1]); + + 2) as array: [format, [opcode, request_length, additional known params]] + + client.MapWindow[0](id) -> + req = reqs.MwpWindow; + req[1].push(id); + pack_stream.pack( req[0], req[1] ); + +- reply + +*/ + +module.exports = { + CreateWindow: [ + // create request packet - function OR format string + function(id, parentId, x, y, width, height, borderWidth, class, visual, values) { + + // TODO: ??? there is depth field in xproto, but xlib just sets it to zero + var depth = 0; + + var packetLength = 8 + Object.keys(values).length; + // TODO: should be CCSLLssSSSSLL - x,y are signed + var format = 'CCSLLSSSSSSLL'; + + // create bitmask + var bitmask = 0; + // TODO: slice from function arguments? + var args = [1, depth, packetLength, id, parentId, x, y, width, height, borderWidth, class, visual]; + + // TODO: the code is a little bit mess + // additional values need to be packed in the following way: + // bitmask (bytes #24 to #31 in the packet) - 32 bit indicating what adittional arguments we supply + // values list (bytes #32 .. #32+4*num_values) in order of corresponding bits + + var masksList = []; + for (var v in values) + { + var valueBit = valueMask[v]; + if (!valueBit) + { + throw new Error('CreateWindow: incorrect value param ' + v); + } + masksList.push(valueBit); + bitmask |= valueBit; + format += 'L'; + } + // values packed in order of corresponding bit + masksList.sort(); + // set bits to indicate additional values we are sending in this request + args.push(bitmask); + // add values in the order of the bits + // TODO: maybe it's better just to scan all 32 bits anstead of sorting parameters we are actually have? + for (m in masksList) + { + valueName = valueMaskNames[masksList[m]]; + args.push( values[valueName] ); + } + return [format, args]; + } + + ], + MapWindow: [ + // 8 - opcode, 2 - length + [ 'CxSL', [8, 2] ] + ] +} diff --git a/lib/x11/handshake.js b/lib/x11/handshake.js new file mode 100644 index 0000000..0a56de2 --- /dev/null +++ b/lib/x11/handshake.js @@ -0,0 +1,208 @@ +var getAuthString = require('./auth'); +var xutil = require('./xutil'); + +function readVisuals(bl, visuals, n_visuals, cb) +{ + if (n_visuals == 0) + { + cb(); + return; + } + + var visual = {}; + bl.unpackTo( visual, + [ + 'L vid', + 'C class', + 'C bits_per_rgb', + 'S map_ent', + 'L red_mask', + 'L green_mask', + 'L blue_mask', + 'xxxx' + ], + function() { + var vid = visual.vid; + // delete visual.vid; + visuals[visual.vid] = visual; + if (Object.keys(visuals).length == n_visuals) + cb() + else + readVisuals(bl, visuals, n_visuals, cb); + }); +} + +function readDepths(bl, display, depths, n_depths, cb) +{ + if (n_depths == 0) + { + cb(); + return; + } + + bl.unpack( 'CxSxxxx', function(res) { + var dep = res[0]; + var n_visuals = res[1]; + var visuals = {}; + readVisuals(bl, visuals, n_visuals, function() + { + depths[dep] = visuals; + if (Object.keys(depths).length == n_depths) + cb(); + else + readDepths(bl, display, depths, n_depths, cb); + }); + }); +} + +function readScreens(bl, display, cbDisplayReady) +{ + // for (i=0; i < display.screen_num; ++i) + { + var scr = {}; + bl.unpackTo( scr, + [ + 'L root', + 'L default_colormap', + 'L white_pixel', + 'L black_pixel', + 'L input_masks', + 'S pixel_with', + 'S pixel_height', + 'S mm_width', + 'S mm_height', + 'S min_installed_maps', + 'S max_installed_maps', + 'L root_visual', + 'C root_depth', + 'C backing_stores', + 'C root_depth', + 'C num_depths' + ], + function () { + var depths = {}; + readDepths(bl, display, depths, scr.num_depths, function() { + + scr.depths = depths; + delete scr.num_depths; + display.screen.push(scr); + + if (display.screen.length == display.screen_num) + { + delete display.screen_num; + cbDisplayReady(display); + return; + } else { + readScreens(bl, display, cbDisplayReady); + } + }); + }); + } +} + +function readServerHello(bl, cb) +{ + +bl.unpack('C', function(res) { + + if (res[0] == 0) + { + // conection time error + // unpack error (? TODO) + var err = new Error; + cb(err); // TODO: detect that this is error on xcore side + // TODO: do we need to close stream from our side? + // TODO: api to close source stream via attached unpackstream + return; + } + + var display = {}; + bl.unpackTo( + display, + [ + 'x', + 'S major', + 'S minor', + 'S xlen', + 'L release', + 'L resource_base', + 'L resource_mask', + 'L motion_buffer_size', + 'S vlen', + 'S max_request_length', + 'C screen_num', + 'C format_num', + 'C image_byte_order', + 'C bitmap_bit_order', + 'C bitmap_scanline_unit', + 'C bitmap_scanline_pad', + 'C min_keycode', + 'C max_keycode', + 'xxxx' + ], + + function() + { + var pvlen = xutil.padded_length(display.vlen); + + // setup data to generate resource id + // TODO: cleaunup code here + var mask = display.resource_mask; + display.rsrc_shift = 0; + while (!( (mask >> display.rsrc_shift) & 1) ) + display.rsrc_shift++; + display.rsrc_id = 0; + + bl.get(pvlen, function(vendor) + { + display.vendor = vendor.toString().substr(0, display.vlen); // utf8 by default? + + display.format = {}; + for (i=0; i < display.format_num; ++i) + { + bl.unpack('CCCxxxxx', function(fmt) { + var depth = fmt[0]; + display.format[depth] = {}; + display.format[depth].bits_per_pixel = fmt[1]; + display.format[depth].scanline_pad = fmt[2]; + if (Object.keys(display.format).length == display.format_num) + { + delete display.format_num; + display.screen = []; + readScreens(bl, display, cb); + } + }); + } + }); + } + ); +}); + +} + +function writeClientHello(stream) +{ + getAuthString( function( authType, authData ) { + authType = xutil.padded_string( authType ); + authData = xutil.padded_string( authData ); + var byte_order = 'l'.charCodeAt(0); // TODO: byteorder!!! + var protocol_major = 11; // TODO: config? env? + var protocol_minor = 0; + stream.pack( + 'CxSSSSxxaa', + [ + byte_order, + protocol_major, + protocol_minor, + authType.length, + authData.length, + authType, + authData + ] + ); + stream.flush(); + }); +} + +module.exports.readServerHello = readServerHello; +module.exports.writeClientHello = writeClientHello; \ No newline at end of file diff --git a/lib/x11/hexy.js b/lib/x11/hexy.js new file mode 100644 index 0000000..e807e21 --- /dev/null +++ b/lib/x11/hexy.js @@ -0,0 +1,261 @@ +//= hexy.js -- utility to create hex dumps +// +// `hexy` is a javascript (node) library that's easy to use to create hex +// dumps from within node. It contains a number of options to configure +// how the hex dumb will end up looking. +// +// It should create a pleasant looking hex dumb by default: +// +// var hexy = require('hexy.js'), +// b = new Buffer("\000\001\003\005\037\012\011bcdefghijklmnopqrstuvwxyz0123456789") +// +// console.log(hexy.hexy(b)) +// +// results in this dump: +// +// 0000000: 00 01 03 05 1f 0a 09 62 63 64 65 66 67 68 69 6a .......b cdefghij +// 0000010: 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 77 78 79 7a klmnopqr stuvwxyz +// 0000020: 30 31 32 33 34 35 36 37 38 39 01234567 89 +// +// but it's also possible to configure: +// +// * Line numbering +// * Line width +// * Format +// * Case of hex decimals +// * Presence of the ASCII annotation in the right column. +// +// This mean you can do exciting dumps like: +// +// 0000000: 0001 0305 1f0a 0962 .... ...b +// 0000008: 6364 6566 6768 696a cdef ghij +// 0000010: 6b6c 6d6e 6f70 7172 klmn opqr +// 0000018: 7374 7576 7778 797a stuv wxyz +// 0000020: 3031 3233 3435 3637 0123 4567 +// 0000028: 3839 89 +// +// or even: +// +// 0000000: 00 01 03 05 1f 0a 09 62 63 64 65 66 67 68 69 6a +// 0000010: 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 77 78 79 7a +// 0000020: 30 31 32 33 34 35 36 37 38 39 +// +// with hexy! +// +// Formatting options are configured by passing a `format` object to the `hexy` function: +// +// var format = {} +// format.width = width // how many bytes per line, default 16 +// format.numbering = n // ["hex_bytes" | "none"], default "none" +// format.format = f // ["fours"|"twos"|"none"], how many nibbles per group +// // default "fours" +// format.caps = c // ["lower"|"upper"], default lower +// format.annotate=a // ["ascii"|"none"], ascii annotation at end of line? +// // default "ascii" +// format.prefix=p // something pretty to put in front of each line +// // default "" +// format.indent=i // number of spaces to indent +// // default 0 +// +// console.log(hexy.hexy(buffer, format)) +// +// In case you're really nerdy, you'll have noticed that the defaults correspond +// to how `xxd` formats it's output. +// +// +//== Installing +// +// Either use `npm`: +// +// npm install hexy +// +// This will install the lib which you'll be able to use like so: +// +// var hexy = require("hexy.js"), +// buf = // get Buffer from somewhere, +// str = hexy.hexy(buf) +// +// It will also install `hexy.js` into your path in case you're totally fed up +// with using `xxd`. +// +// +// If you don't like `npm`, grab the source from github: +// +// http://github.com/a2800276/hexy.js +// +//== TODOS +// +// The current version only pretty prints Buffers. Which probably means it +// can only be used from within node. What's more important what it +// doesn't support: Strings (which would be nice for the sake of +// completeness) and Streams/series of Buffers which would be nice so you +// don't have to collect the whole things you want to pretty print in +// memory. `hexy` is probably most useful for debugging and getting binary +// protocol stuff working, so that's probably not an too much of an issue. +// +//== History +// +// This is a fairly straightforward port of `hexy.rb` which does more or less the +// same thing. You can find it here: +// +// http://github.com/a2800276/hexy +// +// in case these sorts of things interest you. +// +//== Mail +// +// In case you discover bugs, spelling errors, offer suggestions for +// improvements or would like to help out with the project, you can contact +// me directly (tim@kuriositaet.de). + +var hexy = function (buffer, config) { + config = config || {} + var h = new Hexy(buffer, config) + return h.toString() +} + +var Hexy = function (buffer, config) { + var self = this + + self.buffer = buffer // magic string conversion here? + self.width = config.width || 16 + self.numbering = config.numbering == "none" ? "none" : "hex_bytes" + self.groupSpacing = config.groupSpacing || 0 + + switch (config.format) { + case "none": + case "twos": + self.format = config.format + break + default: + self.format = "fours" + } + + self.caps = config.caps == "upper" ? "upper" : "lower" + self.annotate = config.annotate == "none" ? "none" : "ascii" + self.prefix = config.prefix || "" + self.indent = config.indent || 0 + + for (var i = 0; i!=self.indent; ++i) { + self.prefix = " "+prefix + } + + var pos = 0 + + this.toString = function () { + var str = "" + + //split up into line of max `self.width` + var line_arr = lines() + + //lines().forEach(function(hex_raw, i){ + for (var i = 0; i!= line_arr.length; ++i) { + var hex_raw = line_arr[i], + hex = hex_raw[0], + raw = hex_raw[1] + //insert spaces every `self.format.twos` or fours + var howMany = hex.length + if (self.format === "fours") { + howMany = 4 + } else if (self.format === "twos") { + howMany = 2 + } + + var hex_formatted = "" + var middle = Math.floor(self.width / 2)-1 + var groupSpaces = (new Array(self.groupSpacing+1)).join(' '); + for (var j=0; j 0 ? groupSpaces : " ") + } + str += self.prefix + + if (self.numbering === "hex_bytes") { + str += pad(i*self.width, 8) // padding... + str += ": " + } + + var padlen = 0 + switch(self.format) { + case "fours": + padlen = self.width*2 + self.width/2 + break + case "twos": + padlen = self.width*3 + 2 + break + default: + padlen = self * 2 + } + + str += rpad(hex_formatted, padlen) + if (self.annotate === "ascii") { + str+=" " + str+=raw.replace(/[\000-\040\177-\377]/g, ".") + } + str += "\n" + } + return str + } + + var lines = function() { + var hex_raw = [] + + + for (var i = 0; i= buffer.length ? buffer.length : i+self.width, + slice = buffer.slice(begin, end), + hex = self.caps === "upper" ? hexu(slice) : hexl(slice), + raw = slice.toString('ascii') + + hex_raw.push([hex,raw]) + } + return hex_raw + + } + + var hexl = function (buffer) { + var str = "" + for (var i=0; i!=buffer.length; ++i) { + str += pad(buffer[i], 2) + } + return str + } + var hexu = function (buffer) { + return hexl(buffer).toUpperCase() + } + + var pad = function(b, len) { + var s = b.toString(16) + + while (s.length < len) { + s = "0" + s + } + return s + } + var rpad = function(s, len) { + while(s.length < len) { + s += " " + } + return s + } + +} +/* +var fs = require('fs'), + file = process.argv[2] + + +var data = fs.readFileSync(file) +//console.log(hexy(data)) +var format = {} +//format.format = "fours" +format.caps = "upper" +format.annotate = "none" +//format.numbering = "none" +format.width = 8 +console.log(hexy(data, format)) +console.log("doen") +*/ + +exports.hexy = hexy \ No newline at end of file diff --git a/lib/x11/index.js b/lib/x11/index.js index e69de29..f8e488d 100644 --- a/lib/x11/index.js +++ b/lib/x11/index.js @@ -0,0 +1,2 @@ +var core = require('./xcore'); +module.exports.createClient = core.createClient; \ No newline at end of file diff --git a/lib/x11/unpackbuffer.js b/lib/x11/unpackbuffer.js new file mode 100644 index 0000000..3d2a4e5 --- /dev/null +++ b/lib/x11/unpackbuffer.js @@ -0,0 +1,46 @@ +// unpack for static buffer + +// TODO: use as fallback only if v0.5+ fuffer is not available +// TODO: remove duplicate code +var argument_length = {}; +argument_length.C = 1; +argument_length.S = 2; +argument_length.s = 2; +argument_length.L = 4; +argument_length.x = 1; + +module.exports.addUnpack = function(Buffer) +{ + Buffer.prototype.unpack = function(format) + { + var data = []; + var offset = 0; + var current_arg = 0; + while (current_arg < format.length) + { + var arg = format[current_arg]; + switch (arg) { + case 'C': + data.push(this[offset++]); + break; + case 'S': + var b1 = this[offset++]; + var b2 = this[offset++]; + this.data.push(b2*256+b1); + break; + case 'L': + var b1 = this[offset++]; + var b2 = this[offset++]; + var b3 = this[offset++]; + var b4 = this[offset++]; + data.push(((b4*256+b3)*256 + b2)*256 + b1); + break; + case 'x': + offset++; + break; + } + current_arg++; + } + return data; + } +} \ No newline at end of file diff --git a/lib/x11/unpackstream.js b/lib/x11/unpackstream.js new file mode 100644 index 0000000..a37dc9b --- /dev/null +++ b/lib/x11/unpackstream.js @@ -0,0 +1,299 @@ +var Buffer = require('buffer').Buffer; +var EventEmitter = require('events').EventEmitter; +var util = require('util'); +var xutil = require('./xutil'); + +var argument_length = {}; +argument_length.C = 1; +argument_length.S = 2; +argument_length.s = 2; +argument_length.L = 4; +argument_length.x = 1; + +function ReadFormatRequest(format, callback) +{ + this.format = format; + this.current_arg = 0; + this.data = []; + this.callback = callback; +} + +function ReadFixedRequest(length, callback) +{ + this.length = length; + this.callback = callback; + this.data = new Buffer(length); + this.received_bytes = 0; +} + +ReadFixedRequest.prototype.execute = function(bufferlist, aa, bb, cc, dd) +{ + // TODO: this is a brute force version + // replace with Buffer.slice calls + var to_receive = this.length - this.received_bytes; + for(var i=0 ; i < to_receive; ++i) + { + if (bufferlist.length == 0) + return false; + this.data[this.received_bytes++] = bufferlist.getbyte(); + } + this.callback(this.data); + return true; +} + +ReadFormatRequest.prototype.execute = function(bufferlist, tag1, tag2) +{ + while (this.current_arg < this.format.length) + { + var arg = this.format[this.current_arg]; + if (bufferlist.length < argument_length[arg]) + return false; // need to wait for more data to prcess this argument + + // TODO: measure Buffer.readIntXXX performance and use them if faster + // note: 4 and 2-byte values may cross chunk border & split. need to handle this correctly + // maybe best approach is to wait all data required for format and then process fixed buffer + // TODO: byte order!!! + switch (arg) { + case 'C': + this.data.push(bufferlist.getbyte()); + break; + case 'S': + var b1 = bufferlist.getbyte(); + var b2 = bufferlist.getbyte(); + this.data.push(b2*256+b1); + break; + case 'L': + var b1 = bufferlist.getbyte(); + var b2 = bufferlist.getbyte(); + var b3 = bufferlist.getbyte(); + var b4 = bufferlist.getbyte(); + this.data.push(((b4*256+b3)*256 + b2)*256 + b1); + break; + case 'x': + bufferlist.getbyte(); + break; + } + this.current_arg++; + } + this.callback(this.data); + return true; +} + +function UnpackStream() +{ + EventEmitter.call(this); + + this.readlist = []; + this.length = 0; + this.offset = 0; + this.read_queue = []; + this.write_queue = []; + this.write_length = 0; +} +util.inherits(UnpackStream, EventEmitter); + +UnpackStream.prototype.write = function(buf) +{ + this.readlist.push(buf); + this.length += buf.length; + this.resume(); +} + +UnpackStream.prototype.pipe = function(stream) +{ + // TODO: ondrain & pause + this.on('data', function(data) + { + stream.write(data); + }); +} + +UnpackStream.prototype.unpack = function(format, callback) +{ + this.read_queue.push(new ReadFormatRequest(format, callback)); + this.resume(); +} + +UnpackStream.prototype.unpackTo = function(destination, names_formats, callback) +{ + var names = []; + var format = ''; + + for (var i=0; i < names_formats.length; ++i) + { + var off = 0; + while(off < names_formats[i].length && names_formats[i][off] == 'x') + { + format += 'x'; + off++; + } + + if (off < names_formats[i].length) + { + format += names_formats[i][off]; + var name = names_formats[i].substr(off+2); + names.push(name); + } + } + + this.unpack(format, function(data) { + if (data.length != names.length) + throw 'Number of arguments mismatch, ' + names.length + ' fields and ' + data.length + ' arguments'; + for (var fld = 0; fld < data.length; ++fld) + { + destination[names[fld]] = data[fld]; + } + callback(destination); + }); +} + +UnpackStream.prototype.get = function(length, callback) +{ + this.read_queue.push(new ReadFixedRequest(length, callback)); + this.resume(); +} + +UnpackStream.prototype.resume = function() +{ + if (this.resumed) + return; + this.resumed = true; + // process all read requests until enough data in the buffer + while(this.read_queue[0].execute(this)) + { + this.read_queue.shift(); + if (this.read_queue.length == 0) + return; + } + this.resumed = false; +} + +UnpackStream.prototype.getbyte = function() +{ + var res = 0; + var b = this.readlist[0]; + if (this.offset + 1 < b.length) + { + res = b[this.offset]; + this.offset++; + this.length--; + + } else { + + // last byte in current buffer, shift read list + res = b[this.offset]; + this.readlist.shift(); + this.length--; + this.offset = 0; + } + return res; +} + +/* + +// write padded string +// at the moment replaced with pack('p', [ 'padded_string' ]) +UnpackStream.prototype.pstr = function(str) +{ + var len = xutil.padded_length(str.length); + if (len == 0) + return; // nothing to write + var buf = new Buffer(len); + buf.write(str, 'binary'); + this.write_queue.push(buf); +} +*/ + +// TODO: measure node 0.5+ buffer serialisers performance +UnpackStream.prototype.pack = function(format, arguments) +{ + var packetlength = 0; + + var arg = 0; + for (var i = 0; i < format.length; ++i) + { + var f = format[i]; + if (f == 'x') + { + packetlength++; + } else if (f == 'p') { + packetlength += xutil.padded_length(arguments[arg++].length); + } else if (f == 'a') { + packetlength += arguments[arg++].length; + } else { + // this is a fixed-length format, get length from argument_length table + packetlength += argument_length[f]; + arg++; + } + } + + var buf = new Buffer(packetlength); + for (var i=0; i < packetlength; ++i) + buf[i] = 255; + + var offset = 0; + var arg = 0; + for (var i = 0; i < format.length; ++i) + { + switch(format[i]) + { + case 'x': + buf[offset++] = 0; + break; + case 'C': + var n = arguments[arg++]; + buf[offset++] = n; + break; + case 'S': + var n = arguments[arg++]; + buf[offset++] = n & 0xff; + buf[offset++] = (n >> 8) & 0xff; + break; + case 'L': + var n = arguments[arg++]; + buf[offset++] = n & 0xff; + buf[offset++] = (n >> 8) & 0xff; + buf[offset++] = (n >> 16) & 0xff; + buf[offset++] = (n >> 24) & 0xff; + break; + case 'a': // string + var str = arguments[arg++]; + // TODO: buffer.write could be faster + for (var c = 0; c < str.length; ++c) + buf[offset++] = str[c]; + break; + case 'p': // padded string + var str = arguments[arg++]; + var len = padded(str); + // TODO: buffer.write could be faster + var c = 0; + for (; c < str.length; ++c) + buf[offset++] = str[c]; + for (; c < len; ++c) + buf[offset++] = 0; + break; + } + } + this.write_queue.push(buf); + this.write_length += buf.length; + return this; +} + +UnpackStream.prototype.flush = function(stream) +{ + // TODO: measure performance benefit of + // creating and writing one big concatenated buffer + + // TODO: check write result + // pause/resume streaming + + for (var i=0; i < this.write_queue.length; ++i) + { + //stream.write(this.write_queue[i]) + this.emit('data', this.write_queue[i]); + } + this.write_queue = []; + this.write_length = 0; +} + +module.exports = UnpackStream; diff --git a/lib/x11/xcore.js b/lib/x11/xcore.js new file mode 100644 index 0000000..78b1ae4 --- /dev/null +++ b/lib/x11/xcore.js @@ -0,0 +1,199 @@ +var util = require('util'); // util.inherits +var net = require('net'); + +var handshake = require('./handshake'); +//var xevents = require('./xevents'); + +var EventEmitter = require('events').EventEmitter; +var PackStream = require('./unpackstream'); +var coreRequestsTemplate = require('./corereqs'); +//var hexy = require('./hexy').hexy; + +var Buffer = require('buffer').Buffer; +// add 'unpack' method for buffer +require('./unpackbuffer').addUnpack(Buffer); + +var xerrors = require('./xerrors'); +var coreRequests = require('./corereqs'); + +function XClient(stream) +{ + EventEmitter.call(this); + this.stream = stream; + + this.core_requests = {}; + this.ext_requests = {}; + + 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); + + pack_stream.on('data', function( data ) { + //console.error(hexy(data, {prefix: 'from packer '})); + stream.write(data); + }); + stream.on('data', function( data ) { + //console.error(hexy(data, {prefix: 'to unpacker '})); + pack_stream.write(data); + }); + + this.pack_stream = pack_stream; + + this.rcrc_id = 0; // generated for each new resource + this.seq_num = 1; // incremented in each request. (even if we don't expect reply) + + // in/out packets indexed by sequence ID + this.requests = {}; + this.replies = {}; + this.events = {}; + + this.importRequestsFromTemplates(this, coreRequests); + this.startHandshake(); + + // import available extentions + // TODO: lazy import on first call? + /* + this.ext = {}; + this.ListExtensions( function(err, extentionsList ) { + for (ext in extentionsList) { + var extRequests = require('./ext/' + extentionsList[ext]); + // TODO: need to call QueryExtention to get [major opcode, first event, first error] + importRequestsFromTemplates(this, extRequests); + } + } + */ + // init comon extentions + +} +util.inherits(XClient, EventEmitter); + +XClient.prototype.importRequestsFromTemplates = function(target, reqs) +{ + var client = this; + for (r in reqs) + { + // r is request name + target[r] = (function(reqName) { + var reqFunc = function req_proxy() { + var args = Array.prototype.slice.call(req_proxy.arguments); + // TODO: setup last argument to be reply/error callback + // var callback = args.length > 0 ? null : args[args.length - 1]; + + // 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') + { + // call template with input arguments (not including callback which is last argument TODO currently with callback. won't hurt) + //reqPack = reqTemplate.call(args); + reqPack = reqTemplate.apply(this, req_proxy.arguments); + var format = reqPack[0]; + var requestArguments = reqPack[1]; + client.pack_stream.pack(format, requestArguments); + client.pack_stream.flush(); + } else if (templateType == 'Array'){ + var format = reqTemplate[0]; + var requestArguments = reqTemplate[1]; + for (a in args) + requestArguments.push(args[a]); + client.pack_stream.pack(format, requestArguments); + client.pack_stream.flush(); + } else { + throw 'unknown request format - ' + templateType; + } + } + return reqFunc; + })(r); + } +} + +XClient.prototype.AllocID = function() +{ + // TODO: handle overflow (XCMiscGetXIDRange from XC_MISC ext) + // TODO: unused id buffer + this.display.rsrc_id++; + return (this.display.rsrc_id << this.display.rsrc_shift) + this.display.resource_base; +} + +XClient.prototype.expectReplyHeader = function() +{ + var client = this; + client.pack_stream.unpack( + 'CCSL', function(res) { + var type = res[0]; + var seq_num = res[2]; + if (type == 0) + { + var error_code = res[1]; + // unpack error packet (32 bytes for all error types, 8 of them in CCSL header) + client.pack_stream.get(24, function(buf) { + // TODO: dispatch, use sequence number + console.error('error!!!!' + xerrors.errorText[error_code]); + client.expectReplyHeader(); + } ); + return; + } else if (type > 1) + { + client.pack_stream.get(24, function(buf) { + // TODO: dispatch, use sequence number + console.error('event!!!! ' + type); + client.expectReplyHeader(); + } ); + return; + } + var opt_data = res[1]; + var length_total = res[3]; // in 4-bytes units, _including_ this header + var bodylength = (length_total-2)*4; // length of the data in bytes + client.pack_stream.get( bodylength, function( data ) { + // TODO: decode and dispatch, use sequence number + console.error('reply data!!!'); + + // wait for new packet from server + client.expectReplyHeader(); + }); + } + ); +} + +XClient.prototype.startHandshake = function() +{ + var client = this; + + handshake.writeClientHello(this.pack_stream); + handshake.readServerHello(this.pack_stream, function(display) + { + // TODO: readServerHello can set erro state in display + // emit error in that case + client.expectReplyHeader(); + client.display = display; + client.emit('connect', display); + }); +} + +module.exports.createClient = function() +{ + // TODO: parse $DISPLAY + + // open stream + // TODO: better platform matching + var platform = process.platform; + var stream; + if (platform == 'cygwin') + stream = net.createConnection(6000); + else + stream = net.createConnection('/tmp/.X11-unix/X0'); + + return new XClient(stream); +} diff --git a/lib/x11/xerrors.js b/lib/x11/xerrors.js new file mode 100644 index 0000000..fda672d --- /dev/null +++ b/lib/x11/xerrors.js @@ -0,0 +1,19 @@ +module.exports.errorText = { + 1: 'Bad request', + 2: 'Bad param value', + 3: 'Bad window', + 4: 'Bad pixmap', + 5: 'Bad atom', + 6: 'Bad cursor', + 7: 'Bad font', + 8: 'Bad match', + 9: 'Bad drawable', + 10: 'Bad access', + 11: 'Bad alloc', + 12: 'Bad colormap', + 13: 'Bad GContext', + 14: 'Bad ID choice', + 15: 'Bad name', + 16: 'Bad length', + 17: 'Bad implementation' +}; diff --git a/lib/x11/xutil.js b/lib/x11/xutil.js new file mode 100644 index 0000000..b401faf --- /dev/null +++ b/lib/x11/xutil.js @@ -0,0 +1,22 @@ +function padded_length(len) +{ + var rem = len % 4; + var padded_length = len; + if (rem) + padded_length = len + 4 - rem; + return padded_length; +} + +function padded_string(str) +{ + if (str.length == 0); + return ''; + + var len = padded_length(str.len); + var res = str; + for (var i=0; i < len; ++i) + res += String.fromCharCode(0); +} + +module.exports.padded_length = padded_length; +module.exports.padded_string = padded_string; diff --git a/test/createwindow.js b/test/createwindow.js new file mode 100644 index 0000000..ccf484e --- /dev/null +++ b/test/createwindow.js @@ -0,0 +1,12 @@ +var x11 = require('../lib/x11'); + +var xclient = x11.createClient(); + +xclient.on('connect', function(display) { + var X = this; + var root = display.screen[0].root; + var wid = X.AllocID(); + + X.CreateWindow(wid, root, 10, 10, 400, 300, 1, 1, 0, { backgroundPixel: 0, eventMask: 0x00000040 }); + X.MapWindow(wid); +}); \ No newline at end of file