initial big import. Working base to add new requests

This commit is contained in:
sidorares 2011-07-14 15:35:49 +10:00
parent 180edaebf7
commit 14592502c0
12 changed files with 1187 additions and 16 deletions

View file

@ -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

11
lib/x11/auth.js Normal file
View file

@ -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

99
lib/x11/corereqs.js Normal file
View file

@ -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] ]
]
}

208
lib/x11/handshake.js Normal file
View file

@ -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;

261
lib/x11/hexy.js Normal file
View file

@ -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 // <string> something pretty to put in front of each line
// // default ""
// format.indent=i // <num> 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<hex.length; j+=howMany) {
var s = hex.substr(j, howMany)
hex_formatted += s + (j/2 === middle && self.groupSpacing > 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<self.buffer.length ; i+=self.width) {
var begin = i,
end = i+self.width >= 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

View file

@ -0,0 +1,2 @@
var core = require('./xcore');
module.exports.createClient = core.createClient;

46
lib/x11/unpackbuffer.js Normal file
View file

@ -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;
}
}

299
lib/x11/unpackstream.js Normal file
View file

@ -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;

199
lib/x11/xcore.js Normal file
View file

@ -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);
}

19
lib/x11/xerrors.js Normal file
View file

@ -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'
};

22
lib/x11/xutil.js Normal file
View file

@ -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;

12
test/createwindow.js Normal file
View file

@ -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);
});