feat(debugger): integrate NX Tcl Remote Debugger into NX Postprocessor
build_and_puplish.yml / build_and_publish (release) Successful in 35s

Embed the NX Tcl Remote Debugger into the NX Postprocessor extension.
Add a new debugging client, protocol, and adapter logic to
drive attach/launch, breakpoints, stepping, and evaluation
via the runtime adapter.

- Introduced a client debugger with a new adapter and protocol
- Wired attach/launch, breakpoints, stepping, and evaluation
- Updated docs and licensing to reflect the embedded debugger
This commit is contained in:
Christoph Brandau
2026-08-28 22:36:18 +02:00
parent 07ccd2d26a
commit c3d5116885
12 changed files with 1135 additions and 10 deletions
+116
View File
@@ -0,0 +1,116 @@
'use strict';
const net = require('net');
const { EventEmitter } = require('events');
function encodeHex(value) {
return Buffer.from(String(value ?? ''), 'utf8').toString('hex') || '-';
}
function decodeHex(value) {
if (!value || value === '-') return '';
return Buffer.from(value, 'hex').toString('utf8');
}
class RuntimeConnection extends EventEmitter {
constructor() {
super();
this.socket = null;
this.buffer = '';
this.nextRequestId = 1;
this.pending = new Map();
this.closed = false;
}
connect(host, port) {
return new Promise((resolve, reject) => {
const socket = net.createConnection({ host, port });
const onError = (error) => {
socket.destroy();
reject(error);
};
socket.once('error', onError);
socket.once('connect', () => {
socket.removeListener('error', onError);
this.socket = socket;
this.closed = false;
socket.setEncoding('utf8');
socket.on('data', (chunk) => this.onData(chunk));
socket.on('error', (error) => this.emit('runtimeError', error));
socket.on('close', () => this.onClose());
resolve();
});
});
}
request(command, fields = [], timeoutMs = 30000) {
if (!this.socket || this.closed) {
return Promise.reject(new Error('NX runtime is not connected'));
}
const id = this.nextRequestId++;
const normalized = fields.map((field) => String(field ?? ''));
if (normalized.some((field) => field.includes('\t') || field.includes('\n'))) {
return Promise.reject(new Error('protocol fields must be hex-encoded before transmission'));
}
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
this.pending.delete(id);
reject(new Error(`NX ${command} request timed out`));
}, timeoutMs);
this.pending.set(id, { resolve, reject, timer, command });
this.socket.write(['REQ', id, command, ...normalized].join('\t') + '\n');
});
}
close() {
this.closed = true;
if (this.socket) this.socket.destroy();
this.socket = null;
}
onData(chunk) {
this.buffer += chunk;
let newline;
while ((newline = this.buffer.indexOf('\n')) >= 0) {
const line = this.buffer.slice(0, newline).replace(/\r$/, '');
this.buffer = this.buffer.slice(newline + 1);
if (line) this.onLine(line);
}
}
onLine(line) {
const fields = line.split('\t');
if (fields[0] === 'RES') {
const id = Number(fields[1]);
const pending = this.pending.get(id);
if (!pending) return;
this.pending.delete(id);
clearTimeout(pending.timer);
if (fields[2] === 'OK') {
pending.resolve({ command: fields[3], fields: fields.slice(4) });
} else {
pending.reject(new Error(decodeHex(fields[4]) || `NX ${pending.command} failed`));
}
return;
}
if (fields[0] === 'EVENT') {
this.emit('event', fields[1], fields.slice(2));
return;
}
this.emit('runtimeError', new Error(`unrecognized NX message: ${line}`));
}
onClose() {
if (this.closed) return;
this.closed = true;
for (const pending of this.pending.values()) {
clearTimeout(pending.timer);
pending.reject(new Error('NX runtime disconnected'));
}
this.pending.clear();
this.emit('close');
}
}
module.exports = { RuntimeConnection, encodeHex, decodeHex };