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
117 lines
3.4 KiB
JavaScript
117 lines
3.4 KiB
JavaScript
'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 };
|