mirror of
https://github.com/danbulant/node-x11
synced 2026-06-14 20:21:40 +00:00
initial big import. Working base to add new requests
This commit is contained in:
parent
180edaebf7
commit
14592502c0
12 changed files with 1187 additions and 16 deletions
25
README.md
25
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
|
||||
|
|
|
|||
11
lib/x11/auth.js
Normal file
11
lib/x11/auth.js
Normal 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
99
lib/x11/corereqs.js
Normal 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
208
lib/x11/handshake.js
Normal 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
261
lib/x11/hexy.js
Normal 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
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
var core = require('./xcore');
|
||||
module.exports.createClient = core.createClient;
|
||||
46
lib/x11/unpackbuffer.js
Normal file
46
lib/x11/unpackbuffer.js
Normal 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
299
lib/x11/unpackstream.js
Normal 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
199
lib/x11/xcore.js
Normal 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
19
lib/x11/xerrors.js
Normal 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
22
lib/x11/xutil.js
Normal 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
12
test/createwindow.js
Normal 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);
|
||||
});
|
||||
Loading…
Reference in a new issue