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