feat(debugger): integrate NX Tcl Remote Debugger into NX Postprocessor
build_and_puplish.yml / build_and_publish (release) Successful in 35s
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:
@@ -9,6 +9,7 @@ vite.config.js
|
|||||||
.prettierrc.json
|
.prettierrc.json
|
||||||
esbuild.js
|
esbuild.js
|
||||||
.gitea
|
.gitea
|
||||||
|
**/.ruff_cache/**
|
||||||
.venv/**
|
.venv/**
|
||||||
.nox/**
|
.nox/**
|
||||||
server/.venv/**
|
server/.venv/**
|
||||||
@@ -18,4 +19,7 @@ server/.nox/**
|
|||||||
**/requirements.in
|
**/requirements.in
|
||||||
**/server/src/_debug_server.py
|
**/server/src/_debug_server.py
|
||||||
server/noxfile.py
|
server/noxfile.py
|
||||||
|
server/.claude/**
|
||||||
|
server/tests/**
|
||||||
client/**
|
client/**
|
||||||
|
dist/**/*.map
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
- Integrate the NX Tcl Remote Debugger directly into NX Postprocessor Support
|
||||||
|
- Add `nx-tcl` attach configurations and breakpoint support for TCL and DEF files
|
||||||
|
- Support breakpoints, stepping, stack frames, variables, watches, evaluation, logpoints, hit conditions, and Tcl error stops
|
||||||
|
- Add generation-safe stack and variable references so delayed VS Code requests cannot address stale NX frames
|
||||||
|
- Add command-buffer breakpoint handling for `LIB_GE_command_buffer_edit_*` source bodies
|
||||||
|
- Extend the README with launch configuration, path mapping, and debugger usage
|
||||||
- Prevent truncated TCL inlay hints and add configurable parameter hint modes
|
- Prevent truncated TCL inlay hints and add configurable parameter hint modes
|
||||||
- Add inlay hints for built-in NX procedures, variadic arguments, and visible ranges
|
- Add inlay hints for built-in NX procedures, variadic arguments, and visible ranges
|
||||||
- Add inlay hint documentation and navigation to custom procedure definitions
|
- Add inlay hint documentation and navigation to custom procedure definitions
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# NX Postprocessor Support
|
# NX Postprocessor Support
|
||||||
|
|
||||||
A comprehensive VS Code extension providing language support for NX CAM postprocessor development, including CDL, TCL, and DEF files.
|
A comprehensive VS Code extension providing language support and remote debugging for NX CAM postprocessor development, including CDL, TCL, and DEF files.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -10,6 +10,7 @@ A comprehensive VS Code extension providing language support for NX CAM postproc
|
|||||||
- **Intelligent Code Analysis** - Linting and error detection for postprocessor code
|
- **Intelligent Code Analysis** - Linting and error detection for postprocessor code
|
||||||
- **Auto-completion** - Context-aware code completion for faster development
|
- **Auto-completion** - Context-aware code completion for faster development
|
||||||
- **Signature Help** - Shows parameters and documentation for custom and NX procedures
|
- **Signature Help** - Shows parameters and documentation for custom and NX procedures
|
||||||
|
- **NX Tcl Remote Debugger** - Breakpoints, stepping, call stack, scopes, variables, watches, evaluation, logpoints, hit conditions, and Tcl error stops directly in a running NX Post process
|
||||||
|
|
||||||
## Supported File Types
|
## Supported File Types
|
||||||
|
|
||||||
@@ -24,6 +25,10 @@ A comprehensive VS Code extension providing language support for NX CAM postproc
|
|||||||
3. Open any `.cdl`, `.tcl`, or `.def` file
|
3. Open any `.cdl`, `.tcl`, or `.def` file
|
||||||
4. The extension will automatically activate and provide language support
|
4. The extension will automatically activate and provide language support
|
||||||
|
|
||||||
|
The former standalone `NX Tcl Remote Debugger` extension is no longer required. Disable or
|
||||||
|
uninstall `local-nx.nx-tcl-debug` before using the integrated debugger because both extensions
|
||||||
|
register the same `nx-tcl` debug type.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
The extension can be configured through VS Code settings:
|
The extension can be configured through VS Code settings:
|
||||||
@@ -38,6 +43,49 @@ TCL files default to unlimited inlay hint length so that VS Code does not
|
|||||||
truncate later parameter names on a line. An explicit user setting for
|
truncate later parameter names on a line. An explicit user setting for
|
||||||
`editor.inlayHints.maximumLength` still takes precedence.
|
`editor.inlayHints.maximumLength` still takes precedence.
|
||||||
|
|
||||||
|
## NX Tcl Remote Debugger
|
||||||
|
|
||||||
|
### Add a VS Code attach configuration
|
||||||
|
|
||||||
|
Create `.vscode/launch.json` through **Run and Debug: create a launch.json file** and select
|
||||||
|
**NX Tcl: Attach to NX Post**, or use this configuration:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "nx-tcl",
|
||||||
|
"request": "attach",
|
||||||
|
"name": "Attach to NX Post Tcl",
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 4711,
|
||||||
|
"connectTimeout": 120000,
|
||||||
|
"stopOnEntry": false,
|
||||||
|
"breakOnError": true,
|
||||||
|
"localRoot": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When VS Code and NX see the source through different roots, set `remoteRoot` to the root used by
|
||||||
|
NX and keep `localRoot` as the corresponding workspace root.
|
||||||
|
|
||||||
|
### Start debugging
|
||||||
|
|
||||||
|
1. Set breakpoints on executable Tcl or DEF commands.
|
||||||
|
2. Start **Attach to NX Post Tcl** in VS Code before running the postprocessor.
|
||||||
|
3. Start postprocessing in NX.
|
||||||
|
4. Use Continue, Step Over, Step Into, Step Out, Pause, Variables, Watch, and the Debug Console as
|
||||||
|
with a normal source debugger.
|
||||||
|
|
||||||
|
Breakpoints remain active and stop at every real invocation. Use a hit condition such as `1` for
|
||||||
|
a one-time stop. Blank lines, comments, declarations, and multiline Tcl commands may be relocated
|
||||||
|
to the nearest executable command; VS Code shows the resolved line. Dynamic
|
||||||
|
`LIB_GE_command_buffer_edit_*` bodies are mapped back to their original source and owning Tcl
|
||||||
|
procedure.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Simply open any supported file type and enjoy:
|
Simply open any supported file type and enjoy:
|
||||||
@@ -48,6 +96,7 @@ Simply open any supported file type and enjoy:
|
|||||||
- Code formatting (Format Document command)
|
- Code formatting (Format Document command)
|
||||||
- Hover information
|
- Hover information
|
||||||
- Signature help while entering procedure arguments
|
- Signature help while entering procedure arguments
|
||||||
|
- Remote NX Tcl debugging with breakpoints and full stepping
|
||||||
|
|
||||||
## Development and debugging
|
## Development and debugging
|
||||||
|
|
||||||
@@ -56,6 +105,7 @@ Install the root and client dependencies before the first debug session:
|
|||||||
```powershell
|
```powershell
|
||||||
npm install
|
npm install
|
||||||
npm install --prefix client
|
npm install --prefix client
|
||||||
|
npm run test:debugger
|
||||||
```
|
```
|
||||||
|
|
||||||
Use one of the checked-in VS Code launch configurations:
|
Use one of the checked-in VS Code launch configurations:
|
||||||
@@ -78,4 +128,5 @@ This extension is actively maintained. For issues or feature requests, please vi
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
AGPL-3.0 License - see LICENSE file for details.
|
AGPL-3.0 License - see LICENSE file for details. The embedded NX Tcl Remote Debugger adapter
|
||||||
|
retains its MIT notice in `debugger/NX_TCL_DEBUGGER_LICENSE.txt`.
|
||||||
|
|||||||
@@ -0,0 +1,650 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const { RuntimeConnection, encodeHex, decodeHex } = require('./protocol');
|
||||||
|
|
||||||
|
const THREAD_ID = 1;
|
||||||
|
|
||||||
|
class NxDebugAdapter {
|
||||||
|
constructor(input = process.stdin, output = process.stdout) {
|
||||||
|
this.input = input;
|
||||||
|
this.output = output;
|
||||||
|
this.inputBuffer = Buffer.alloc(0);
|
||||||
|
this.sequence = 1;
|
||||||
|
this.runtime = null;
|
||||||
|
this.terminated = false;
|
||||||
|
this.explicitDisconnect = false;
|
||||||
|
this.localRoot = '';
|
||||||
|
this.remoteRoot = '';
|
||||||
|
this.nextBreakpointId = 1;
|
||||||
|
// VS Code can expose the same physical file through more than one source
|
||||||
|
// identity (for example, drive mappings or differently cased workspace
|
||||||
|
// roots). SETBPS replaces all breakpoints for a runtime path, so retain
|
||||||
|
// each DAP source group and send their union to NX.
|
||||||
|
this.sourceBreakpointGroups = new Map();
|
||||||
|
this.pendingStartRequest = null;
|
||||||
|
this.resumeInProgress = 0;
|
||||||
|
this.pendingStoppedEvents = [];
|
||||||
|
this.isPaused = false;
|
||||||
|
// DAP frame/variable IDs can outlive a stop in the VS Code UI. Runtime
|
||||||
|
// levels and references, however, are valid only for the current Tcl
|
||||||
|
// pause. Give every stop adapter-owned IDs so a late request can never
|
||||||
|
// accidentally address a same-numbered frame from a later pause.
|
||||||
|
this.nextFrameId = 1;
|
||||||
|
this.frameReferences = new Map();
|
||||||
|
this.runtimeToFrameReference = new Map();
|
||||||
|
this.nextVariablesReference = 1;
|
||||||
|
this.variableReferences = new Map();
|
||||||
|
this.runtimeToVariableReference = new Map();
|
||||||
|
|
||||||
|
input.on('data', (chunk) => this.onData(chunk));
|
||||||
|
input.on('end', () => this.dispose());
|
||||||
|
input.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
onData(chunk) {
|
||||||
|
this.inputBuffer = Buffer.concat([this.inputBuffer, chunk]);
|
||||||
|
while (true) {
|
||||||
|
const headerEnd = this.inputBuffer.indexOf('\r\n\r\n');
|
||||||
|
if (headerEnd < 0) return;
|
||||||
|
const header = this.inputBuffer.subarray(0, headerEnd).toString('ascii');
|
||||||
|
const match = /Content-Length:\s*(\d+)/i.exec(header);
|
||||||
|
if (!match) throw new Error('missing DAP Content-Length header');
|
||||||
|
const contentLength = Number(match[1]);
|
||||||
|
const messageEnd = headerEnd + 4 + contentLength;
|
||||||
|
if (this.inputBuffer.length < messageEnd) return;
|
||||||
|
const payload = this.inputBuffer.subarray(headerEnd + 4, messageEnd).toString('utf8');
|
||||||
|
this.inputBuffer = this.inputBuffer.subarray(messageEnd);
|
||||||
|
const message = JSON.parse(payload);
|
||||||
|
if (message.type === 'request') {
|
||||||
|
this.handleRequest(message).catch((error) => this.sendErrorResponse(message, error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
send(message) {
|
||||||
|
message.seq = this.sequence++;
|
||||||
|
const json = JSON.stringify(message);
|
||||||
|
this.output.write(`Content-Length: ${Buffer.byteLength(json, 'utf8')}\r\n\r\n${json}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendResponse(request, body = {}) {
|
||||||
|
this.send({
|
||||||
|
type: 'response',
|
||||||
|
request_seq: request.seq,
|
||||||
|
success: true,
|
||||||
|
command: request.command,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sendErrorResponse(request, error) {
|
||||||
|
this.send({
|
||||||
|
type: 'response',
|
||||||
|
request_seq: request.seq,
|
||||||
|
success: false,
|
||||||
|
command: request.command,
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sendEvent(event, body = {}) {
|
||||||
|
this.send({ type: 'event', event, body });
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleRequest(request) {
|
||||||
|
const args = request.arguments || {};
|
||||||
|
switch (request.command) {
|
||||||
|
case 'initialize':
|
||||||
|
this.sendResponse(request, {
|
||||||
|
supportsConfigurationDoneRequest: true,
|
||||||
|
supportsConditionalBreakpoints: true,
|
||||||
|
supportsHitConditionalBreakpoints: true,
|
||||||
|
supportsLogPoints: true,
|
||||||
|
supportsEvaluateForHovers: true,
|
||||||
|
supportsSetVariable: true,
|
||||||
|
supportsExceptionFilterOptions: true,
|
||||||
|
exceptionBreakpointFilters: [
|
||||||
|
{ filter: 'tclError', label: 'Tcl errors', description: 'Break when a Tcl command returns TCL_ERROR' },
|
||||||
|
],
|
||||||
|
supportsTerminateRequest: true,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'attach':
|
||||||
|
case 'launch':
|
||||||
|
await this.attach(args);
|
||||||
|
this.pendingStartRequest = request;
|
||||||
|
this.sendEvent('initialized');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'configurationDone':
|
||||||
|
await this.runtime.request('CONFIGDONE');
|
||||||
|
this.sendResponse(request);
|
||||||
|
if (this.pendingStartRequest) {
|
||||||
|
this.sendResponse(this.pendingStartRequest);
|
||||||
|
this.pendingStartRequest = null;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'setBreakpoints':
|
||||||
|
await this.setBreakpoints(request, args);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'setExceptionBreakpoints': {
|
||||||
|
const filters = args.filters || [];
|
||||||
|
await this.runtime.request('SETEXCEPTIONS', filters);
|
||||||
|
this.sendResponse(request, {
|
||||||
|
breakpoints: filters.map(() => ({ verified: true })),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'threads':
|
||||||
|
this.sendResponse(request, { threads: [{ id: THREAD_ID, name: 'NX Post Tcl' }] });
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'stackTrace':
|
||||||
|
await this.stackTrace(request);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'scopes':
|
||||||
|
await this.scopes(request, args);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'variables':
|
||||||
|
await this.variables(request, args);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'evaluate':
|
||||||
|
await this.evaluate(request, args);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'setVariable':
|
||||||
|
await this.setVariable(request, args);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'continue':
|
||||||
|
await this.resume(request, 'CONTINUE', { allThreadsContinued: true });
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'next':
|
||||||
|
await this.resume(request, 'NEXT');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'stepIn':
|
||||||
|
await this.resume(request, 'STEPIN');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'stepOut':
|
||||||
|
await this.resume(request, 'STEPOUT');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'pause':
|
||||||
|
await this.runtime.request('PAUSE');
|
||||||
|
this.sendResponse(request);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'disconnect':
|
||||||
|
case 'terminate':
|
||||||
|
await this.disconnect(request);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
this.sendResponse(request);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async attach(args) {
|
||||||
|
const host = args.host || '127.0.0.1';
|
||||||
|
const port = Number(args.port || 4711);
|
||||||
|
const timeout = Number(args.connectTimeout || 120000);
|
||||||
|
this.localRoot = args.localRoot ? path.resolve(args.localRoot) : '';
|
||||||
|
this.remoteRoot = args.remoteRoot ? this.normalizePath(args.remoteRoot) : '';
|
||||||
|
this.sourceBreakpointGroups.clear();
|
||||||
|
this.isPaused = false;
|
||||||
|
this.resetStoppedReferences(true);
|
||||||
|
|
||||||
|
this.runtime = new RuntimeConnection();
|
||||||
|
this.runtime.on('event', (name, fields) => this.onRuntimeEvent(name, fields));
|
||||||
|
this.runtime.on('runtimeError', (error) => {
|
||||||
|
this.sendEvent('output', { category: 'stderr', output: `NX: ${error.message}\n` });
|
||||||
|
});
|
||||||
|
this.runtime.on('close', () => {
|
||||||
|
if (!this.explicitDisconnect) this.sendTerminated();
|
||||||
|
});
|
||||||
|
|
||||||
|
const started = Date.now();
|
||||||
|
let lastError;
|
||||||
|
while (Date.now() - started < timeout) {
|
||||||
|
try {
|
||||||
|
await this.runtime.connect(host, port);
|
||||||
|
lastError = null;
|
||||||
|
break;
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lastError) {
|
||||||
|
throw new Error(`could not connect to NX at ${host}:${port}: ${lastError.message}`);
|
||||||
|
}
|
||||||
|
await this.runtime.request('CONFIG', [args.stopOnEntry ? 1 : 0, args.breakOnError ? 1 : 0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setBreakpoints(request, args) {
|
||||||
|
const sourcePath = args.source && args.source.path ? args.source.path : '';
|
||||||
|
const remotePath = this.toRemotePath(sourcePath);
|
||||||
|
const sourceKey = this.breakpointSourceKey(args.source, sourcePath);
|
||||||
|
const remoteKey = this.breakpointRemoteKey(remotePath);
|
||||||
|
const requested = args.breakpoints || (args.lines || []).map((line) => ({ line }));
|
||||||
|
const records = requested.map((breakpoint) => ({
|
||||||
|
id: this.nextBreakpointId++,
|
||||||
|
line: breakpoint.line,
|
||||||
|
condition: breakpoint.condition || '',
|
||||||
|
hitCondition: breakpoint.hitCondition || '',
|
||||||
|
logMessage: breakpoint.logMessage || '',
|
||||||
|
}));
|
||||||
|
if (!this.isTclSource(sourcePath)) {
|
||||||
|
this.sourceBreakpointGroups.delete(sourceKey);
|
||||||
|
// VS Code forwards breakpoints from every language to every active debug
|
||||||
|
// session. Clear any stale runtime entry left by an older adapter, but do
|
||||||
|
// not make the NX Tcl runtime parse or instrument a Python source file.
|
||||||
|
await this.runtime.request('SETBPS', [encodeHex(remotePath), 0]);
|
||||||
|
this.sendResponse(request, {
|
||||||
|
breakpoints: records.map((breakpoint) => ({
|
||||||
|
id: breakpoint.id,
|
||||||
|
verified: false,
|
||||||
|
line: breakpoint.line,
|
||||||
|
source: args.source,
|
||||||
|
message: 'NX Tcl Remote Debugger supports breakpoints only in .tcl and .def files',
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sourceBreakpointGroups.set(sourceKey, { remotePath, remoteKey, records });
|
||||||
|
|
||||||
|
// Collapse exact duplicates shared by aliases, while remembering which
|
||||||
|
// runtime breakpoint represents every DAP breakpoint ID. This keeps hit
|
||||||
|
// counts and log points from firing twice for one Tcl command.
|
||||||
|
const mergedBreakpoints = [];
|
||||||
|
const mergedBySignature = new Map();
|
||||||
|
const representativeById = new Map();
|
||||||
|
for (const group of this.sourceBreakpointGroups.values()) {
|
||||||
|
if (group.remoteKey !== remoteKey) continue;
|
||||||
|
for (const breakpoint of group.records) {
|
||||||
|
const signature = this.breakpointSignature(breakpoint);
|
||||||
|
let representative = mergedBySignature.get(signature);
|
||||||
|
if (!representative) {
|
||||||
|
representative = breakpoint;
|
||||||
|
mergedBySignature.set(signature, representative);
|
||||||
|
mergedBreakpoints.push(representative);
|
||||||
|
}
|
||||||
|
representativeById.set(breakpoint.id, representative.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = [encodeHex(remotePath), mergedBreakpoints.length];
|
||||||
|
for (const breakpoint of mergedBreakpoints) {
|
||||||
|
fields.push(
|
||||||
|
breakpoint.id,
|
||||||
|
breakpoint.line,
|
||||||
|
encodeHex(breakpoint.condition),
|
||||||
|
encodeHex(breakpoint.hitCondition),
|
||||||
|
encodeHex(breakpoint.logMessage),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const runtimeResponse = await this.runtime.request('SETBPS', fields);
|
||||||
|
const responseFields = runtimeResponse.fields || [];
|
||||||
|
const responseCount = Number(responseFields[0] || 0);
|
||||||
|
const responseStride = responseFields.length >= 1 + responseCount * 4 ? 4 : 2;
|
||||||
|
const runtimeBreakpoints = new Map();
|
||||||
|
let responseCursor = 1;
|
||||||
|
for (let index = 0; index < responseCount; index++) {
|
||||||
|
const id = Number(responseFields[responseCursor++]);
|
||||||
|
const line = Number(responseFields[responseCursor++]);
|
||||||
|
let verified = true;
|
||||||
|
let message = '';
|
||||||
|
if (responseStride === 4) {
|
||||||
|
verified = responseFields[responseCursor++] === '1';
|
||||||
|
message = decodeHex(responseFields[responseCursor++]);
|
||||||
|
}
|
||||||
|
runtimeBreakpoints.set(id, { line, verified, message });
|
||||||
|
}
|
||||||
|
this.sendResponse(request, {
|
||||||
|
breakpoints: records.map((breakpoint) => {
|
||||||
|
const representativeId = representativeById.get(breakpoint.id);
|
||||||
|
const runtimeBreakpoint = runtimeBreakpoints.get(representativeId);
|
||||||
|
const result = {
|
||||||
|
id: breakpoint.id,
|
||||||
|
verified: runtimeBreakpoint ? runtimeBreakpoint.verified : false,
|
||||||
|
line: runtimeBreakpoint && runtimeBreakpoint.line ? runtimeBreakpoint.line : breakpoint.line,
|
||||||
|
source: args.source,
|
||||||
|
};
|
||||||
|
if (runtimeBreakpoint && runtimeBreakpoint.message) result.message = runtimeBreakpoint.message;
|
||||||
|
return result;
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async stackTrace(request) {
|
||||||
|
if (!this.isPaused) {
|
||||||
|
this.sendResponse(request, { stackFrames: [], totalFrames: 0 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { fields } = await this.runtime.request('STACK');
|
||||||
|
const count = Number(fields[0] || 0);
|
||||||
|
const stackFrames = [];
|
||||||
|
let cursor = 1;
|
||||||
|
for (let index = 0; index < count; index++) {
|
||||||
|
const runtimeId = Number(fields[cursor++]);
|
||||||
|
const name = decodeHex(fields[cursor++]);
|
||||||
|
const remoteFile = decodeHex(fields[cursor++]);
|
||||||
|
const line = Number(fields[cursor++]);
|
||||||
|
const column = Number(fields[cursor++]);
|
||||||
|
const localFile = this.toLocalPath(remoteFile);
|
||||||
|
const frame = {
|
||||||
|
id: this.toAdapterFrameId(runtimeId),
|
||||||
|
name: name || '<Tcl>',
|
||||||
|
line: line || 1,
|
||||||
|
column: column || 1,
|
||||||
|
};
|
||||||
|
if (localFile) frame.source = { name: path.basename(localFile), path: localFile };
|
||||||
|
stackFrames.push(frame);
|
||||||
|
}
|
||||||
|
this.sendResponse(request, { stackFrames, totalFrames: stackFrames.length });
|
||||||
|
}
|
||||||
|
|
||||||
|
async scopes(request, args) {
|
||||||
|
const runtimeFrameId = this.frameReferences.get(Number(args.frameId));
|
||||||
|
if (!this.isPaused || runtimeFrameId === undefined) {
|
||||||
|
this.sendResponse(request, { scopes: [] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { fields } = await this.runtime.request('SCOPES', [runtimeFrameId]);
|
||||||
|
const count = Number(fields[0] || 0);
|
||||||
|
const scopes = [];
|
||||||
|
let cursor = 1;
|
||||||
|
for (let index = 0; index < count; index++) {
|
||||||
|
const variablesReference = this.toAdapterVariablesReference(Number(fields[cursor++]));
|
||||||
|
const name = decodeHex(fields[cursor++]);
|
||||||
|
cursor++; // wire label: expensive
|
||||||
|
const expensive = fields[cursor++] === '1';
|
||||||
|
scopes.push({ name, variablesReference, expensive });
|
||||||
|
}
|
||||||
|
this.sendResponse(request, { scopes });
|
||||||
|
}
|
||||||
|
|
||||||
|
async variables(request, args) {
|
||||||
|
const runtimeReference = this.variableReferences.get(Number(args.variablesReference));
|
||||||
|
if (!this.isPaused || runtimeReference === undefined) {
|
||||||
|
this.sendResponse(request, { variables: [] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { fields } = await this.runtime.request('VARIABLES', [
|
||||||
|
runtimeReference,
|
||||||
|
args.start || 0,
|
||||||
|
args.count || 0,
|
||||||
|
encodeHex(args.filter || ''),
|
||||||
|
]);
|
||||||
|
const count = Number(fields[0] || 0);
|
||||||
|
const variables = [];
|
||||||
|
let cursor = 1;
|
||||||
|
for (let index = 0; index < count; index++) {
|
||||||
|
variables.push({
|
||||||
|
name: decodeHex(fields[cursor++]),
|
||||||
|
value: decodeHex(fields[cursor++]),
|
||||||
|
type: decodeHex(fields[cursor++]),
|
||||||
|
variablesReference: this.toAdapterVariablesReference(Number(fields[cursor++])),
|
||||||
|
indexedVariables: Number(fields[cursor++]) || undefined,
|
||||||
|
namedVariables: Number(fields[cursor++]) || undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.sendResponse(request, { variables });
|
||||||
|
}
|
||||||
|
|
||||||
|
async evaluate(request, args) {
|
||||||
|
// VS Code can refresh Watch/Hover expressions after Continue but before
|
||||||
|
// it has processed the next stopped event. Do not forward that transient
|
||||||
|
// request to the running NX interpreter (which correctly rejects it).
|
||||||
|
if (!this.isPaused) {
|
||||||
|
this.sendResponse(request, {
|
||||||
|
result: '<NX is running; evaluation is available while paused>',
|
||||||
|
type: 'unavailable',
|
||||||
|
variablesReference: 0,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let runtimeFrameId = 0;
|
||||||
|
if (Number(args.frameId) > 0) {
|
||||||
|
runtimeFrameId = this.frameReferences.get(Number(args.frameId));
|
||||||
|
if (runtimeFrameId === undefined) {
|
||||||
|
this.sendResponse(request, {
|
||||||
|
result: '<stack frame is no longer available>',
|
||||||
|
type: 'unavailable',
|
||||||
|
variablesReference: 0,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const { fields } = await this.runtime.request('EVALUATE', [runtimeFrameId, encodeHex(args.expression || '')]);
|
||||||
|
this.sendResponse(request, {
|
||||||
|
result: decodeHex(fields[0]),
|
||||||
|
type: decodeHex(fields[1]),
|
||||||
|
variablesReference: this.toAdapterVariablesReference(Number(fields[2])),
|
||||||
|
indexedVariables: Number(fields[3]) || undefined,
|
||||||
|
namedVariables: Number(fields[4]) || undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async setVariable(request, args) {
|
||||||
|
const runtimeReference = this.variableReferences.get(Number(args.variablesReference));
|
||||||
|
if (!this.isPaused || runtimeReference === undefined) {
|
||||||
|
throw new Error('variable is no longer available');
|
||||||
|
}
|
||||||
|
const { fields } = await this.runtime.request('SETVARIABLE', [
|
||||||
|
runtimeReference,
|
||||||
|
encodeHex(args.name),
|
||||||
|
encodeHex(args.value),
|
||||||
|
]);
|
||||||
|
this.sendResponse(request, {
|
||||||
|
value: decodeHex(fields[0]),
|
||||||
|
type: decodeHex(fields[1]),
|
||||||
|
variablesReference: this.toAdapterVariablesReference(Number(fields[2])),
|
||||||
|
indexedVariables: Number(fields[3]) || undefined,
|
||||||
|
namedVariables: Number(fields[4]) || undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async resume(request, command, responseBody = {}) {
|
||||||
|
this.isPaused = false;
|
||||||
|
this.resetStoppedReferences();
|
||||||
|
this.resumeInProgress++;
|
||||||
|
try {
|
||||||
|
await this.runtime.request(command);
|
||||||
|
// The runtime can hit the next breakpoint immediately and place its
|
||||||
|
// STOPPED event in the same TCP packet as this response. Always publish
|
||||||
|
// the DAP resume transition before releasing such a queued stop.
|
||||||
|
this.sendResponse(request, responseBody);
|
||||||
|
this.sendEvent('continued', { threadId: THREAD_ID, allThreadsContinued: true });
|
||||||
|
} finally {
|
||||||
|
this.resumeInProgress--;
|
||||||
|
if (this.resumeInProgress === 0) this.flushStoppedEvents();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async disconnect(request) {
|
||||||
|
this.explicitDisconnect = true;
|
||||||
|
this.isPaused = false;
|
||||||
|
this.resetStoppedReferences();
|
||||||
|
if (this.runtime && !this.runtime.closed) {
|
||||||
|
try {
|
||||||
|
await this.runtime.request('DISCONNECT', [], 2000);
|
||||||
|
} catch (_) {
|
||||||
|
// The runtime may close immediately after acknowledging detach.
|
||||||
|
}
|
||||||
|
this.runtime.close();
|
||||||
|
}
|
||||||
|
this.sendResponse(request);
|
||||||
|
this.sendTerminated();
|
||||||
|
}
|
||||||
|
|
||||||
|
onRuntimeEvent(name, fields) {
|
||||||
|
switch (name) {
|
||||||
|
case 'HELLO':
|
||||||
|
this.sendEvent('output', {
|
||||||
|
category: 'console',
|
||||||
|
output: `NX runtime ${decodeHex(fields[0])}, Tcl ${decodeHex(fields[1])}, PID ${fields[2]}\n`,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'STOPPED':
|
||||||
|
this.isPaused = true;
|
||||||
|
this.resetStoppedReferences();
|
||||||
|
this.publishStoppedEvent({
|
||||||
|
reason: decodeHex(fields[0]) || 'pause',
|
||||||
|
threadId: THREAD_ID,
|
||||||
|
allThreadsStopped: true,
|
||||||
|
text: decodeHex(fields[3]) || undefined,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'OUTPUT':
|
||||||
|
this.sendEvent('output', { category: 'stdout', output: decodeHex(fields[0]) });
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.sendEvent('output', { category: 'console', output: `NX event ${name}\n` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
publishStoppedEvent(body) {
|
||||||
|
if (this.resumeInProgress > 0) {
|
||||||
|
this.pendingStoppedEvents.push(body);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.sendEvent('stopped', body);
|
||||||
|
}
|
||||||
|
|
||||||
|
flushStoppedEvents() {
|
||||||
|
const queued = this.pendingStoppedEvents;
|
||||||
|
this.pendingStoppedEvents = [];
|
||||||
|
for (const body of queued) this.sendEvent('stopped', body);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetStoppedReferences(resetCounters = false) {
|
||||||
|
this.frameReferences.clear();
|
||||||
|
this.runtimeToFrameReference.clear();
|
||||||
|
this.variableReferences.clear();
|
||||||
|
this.runtimeToVariableReference.clear();
|
||||||
|
if (resetCounters) {
|
||||||
|
this.nextFrameId = 1;
|
||||||
|
this.nextVariablesReference = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toAdapterFrameId(runtimeId) {
|
||||||
|
const normalized = Number(runtimeId);
|
||||||
|
if (!Number.isFinite(normalized) || normalized <= 0) return 0;
|
||||||
|
let adapterId = this.runtimeToFrameReference.get(normalized);
|
||||||
|
if (adapterId !== undefined) return adapterId;
|
||||||
|
adapterId = this.nextFrameId++;
|
||||||
|
this.runtimeToFrameReference.set(normalized, adapterId);
|
||||||
|
this.frameReferences.set(adapterId, normalized);
|
||||||
|
return adapterId;
|
||||||
|
}
|
||||||
|
|
||||||
|
toAdapterVariablesReference(runtimeReference) {
|
||||||
|
const normalized = Number(runtimeReference);
|
||||||
|
if (!Number.isFinite(normalized) || normalized <= 0) return 0;
|
||||||
|
let adapterReference = this.runtimeToVariableReference.get(normalized);
|
||||||
|
if (adapterReference !== undefined) return adapterReference;
|
||||||
|
adapterReference = this.nextVariablesReference++;
|
||||||
|
this.runtimeToVariableReference.set(normalized, adapterReference);
|
||||||
|
this.variableReferences.set(adapterReference, normalized);
|
||||||
|
return adapterReference;
|
||||||
|
}
|
||||||
|
|
||||||
|
isTclSource(sourcePath) {
|
||||||
|
const extension = path.extname(String(sourcePath || '')).toLowerCase();
|
||||||
|
return extension === '.tcl' || extension === '.def';
|
||||||
|
}
|
||||||
|
|
||||||
|
breakpointSourceKey(source = {}, sourcePath = '') {
|
||||||
|
if (source && Number(source.sourceReference) > 0) {
|
||||||
|
return `reference:${source.sourceReference}`;
|
||||||
|
}
|
||||||
|
// Deliberately preserve spelling and case here: they are precisely what
|
||||||
|
// distinguish two VS Code source identities that map to the same runtime
|
||||||
|
// path. Runtime comparison is handled separately below.
|
||||||
|
return `path:${String(sourcePath || '')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
breakpointRemoteKey(remotePath) {
|
||||||
|
let normalized = this.normalizePath(remotePath);
|
||||||
|
if (process.platform === 'win32' || /^[A-Za-z]:\//.test(normalized) || normalized.startsWith('//')) {
|
||||||
|
normalized = normalized.toLowerCase();
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
breakpointSignature(breakpoint) {
|
||||||
|
return JSON.stringify([
|
||||||
|
Number(breakpoint.line),
|
||||||
|
breakpoint.condition || '',
|
||||||
|
breakpoint.hitCondition || '',
|
||||||
|
breakpoint.logMessage || '',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizePath(value) {
|
||||||
|
const normalized = String(value || '').replace(/\\/g, '/');
|
||||||
|
if (normalized === '/' || /^[A-Za-z]:\/$/.test(normalized)) return normalized;
|
||||||
|
return normalized.replace(/\/+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
samePathPrefix(candidate, prefix) {
|
||||||
|
let normalizedCandidate = this.normalizePath(candidate);
|
||||||
|
let normalizedPrefix = this.normalizePath(prefix);
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
normalizedCandidate = normalizedCandidate.toLowerCase();
|
||||||
|
normalizedPrefix = normalizedPrefix.toLowerCase();
|
||||||
|
}
|
||||||
|
if (normalizedCandidate === normalizedPrefix) return true;
|
||||||
|
const boundaryPrefix = normalizedPrefix.endsWith('/') ? normalizedPrefix : `${normalizedPrefix}/`;
|
||||||
|
return normalizedCandidate.startsWith(boundaryPrefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
toRemotePath(localPath) {
|
||||||
|
const normalized = this.normalizePath(path.resolve(localPath || '.'));
|
||||||
|
const localRoot = this.normalizePath(this.localRoot);
|
||||||
|
if (localRoot && this.remoteRoot && this.samePathPrefix(normalized, localRoot)) {
|
||||||
|
return this.remoteRoot + normalized.slice(localRoot.length);
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
toLocalPath(remotePath) {
|
||||||
|
const normalized = this.normalizePath(remotePath);
|
||||||
|
if (this.remoteRoot && this.localRoot && this.samePathPrefix(normalized, this.remoteRoot)) {
|
||||||
|
const suffix = normalized.slice(this.remoteRoot.length).replace(/^\//, '');
|
||||||
|
return path.join(this.localRoot, ...suffix.split('/'));
|
||||||
|
}
|
||||||
|
return process.platform === 'win32' ? normalized.replace(/\//g, '\\') : normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendTerminated() {
|
||||||
|
if (this.terminated) return;
|
||||||
|
this.terminated = true;
|
||||||
|
this.sendEvent('terminated');
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
if (this.runtime) this.runtime.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
new NxDebugAdapter();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { NxDebugAdapter, THREAD_ID };
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { PassThrough } = require('node:stream');
|
||||||
|
const { NxDebugAdapter } = require('./debugAdapter');
|
||||||
|
|
||||||
|
function frameMessage(message) {
|
||||||
|
const json = JSON.stringify(message);
|
||||||
|
return `Content-Length: ${Buffer.byteLength(json, 'utf8')}\r\n\r\n${json}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class NxInlineDebugAdapter {
|
||||||
|
constructor() {
|
||||||
|
this.input = new PassThrough();
|
||||||
|
this.output = new PassThrough();
|
||||||
|
this.outputBuffer = Buffer.alloc(0);
|
||||||
|
this.listeners = new Set();
|
||||||
|
this.disposed = false;
|
||||||
|
|
||||||
|
this.onDidSendMessage = (listener, thisArgs, disposables) => {
|
||||||
|
const registration = { listener, thisArgs };
|
||||||
|
this.listeners.add(registration);
|
||||||
|
const disposable = {
|
||||||
|
dispose: () => this.listeners.delete(registration),
|
||||||
|
};
|
||||||
|
if (Array.isArray(disposables)) disposables.push(disposable);
|
||||||
|
return disposable;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.output.on('data', (chunk) => this.onOutput(chunk));
|
||||||
|
this.adapter = new NxDebugAdapter(this.input, this.output);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMessage(message) {
|
||||||
|
if (!this.disposed) this.input.write(frameMessage(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
onOutput(chunk) {
|
||||||
|
this.outputBuffer = Buffer.concat([this.outputBuffer, chunk]);
|
||||||
|
while (true) {
|
||||||
|
const headerEnd = this.outputBuffer.indexOf('\r\n\r\n');
|
||||||
|
if (headerEnd < 0) return;
|
||||||
|
const header = this.outputBuffer.subarray(0, headerEnd).toString('ascii');
|
||||||
|
const match = /Content-Length:\s*(\d+)/i.exec(header);
|
||||||
|
if (!match) throw new Error('missing DAP Content-Length header from NX adapter');
|
||||||
|
const contentLength = Number(match[1]);
|
||||||
|
const messageEnd = headerEnd + 4 + contentLength;
|
||||||
|
if (this.outputBuffer.length < messageEnd) return;
|
||||||
|
|
||||||
|
const payload = this.outputBuffer.subarray(headerEnd + 4, messageEnd).toString('utf8');
|
||||||
|
this.outputBuffer = this.outputBuffer.subarray(messageEnd);
|
||||||
|
const message = JSON.parse(payload);
|
||||||
|
for (const { listener, thisArgs } of [...this.listeners]) {
|
||||||
|
listener.call(thisArgs, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
if (this.disposed) return;
|
||||||
|
this.disposed = true;
|
||||||
|
this.adapter.dispose();
|
||||||
|
this.input.end();
|
||||||
|
this.output.destroy();
|
||||||
|
this.listeners.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { NxInlineDebugAdapter, frameMessage };
|
||||||
@@ -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 };
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import * as vscode from "vscode"
|
||||||
|
|
||||||
|
// The adapter is shared with the standalone NX Tcl Remote Debugger. It is
|
||||||
|
// bundled by esbuild into this extension, so no child process or additional
|
||||||
|
// VS Code extension is required.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const { NxInlineDebugAdapter } = require("./inlineAdapter")
|
||||||
|
|
||||||
|
class NxDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory {
|
||||||
|
createDebugAdapterDescriptor(): vscode.ProviderResult<vscode.DebugAdapterDescriptor> {
|
||||||
|
return new vscode.DebugAdapterInlineImplementation(new NxInlineDebugAdapter())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveDebugConfiguration(
|
||||||
|
folder: vscode.WorkspaceFolder | undefined,
|
||||||
|
config: vscode.DebugConfiguration
|
||||||
|
): vscode.DebugConfiguration {
|
||||||
|
const resolved = { ...config }
|
||||||
|
if (!resolved.type) resolved.type = "nx-tcl"
|
||||||
|
if (!resolved.request) resolved.request = "attach"
|
||||||
|
if (!resolved.name) resolved.name = "Attach to NX Post Tcl"
|
||||||
|
if (!resolved.host) resolved.host = "127.0.0.1"
|
||||||
|
if (!resolved.port) resolved.port = 4711
|
||||||
|
if (!resolved.connectTimeout) resolved.connectTimeout = 120000
|
||||||
|
if (resolved.stopOnEntry === undefined) resolved.stopOnEntry = false
|
||||||
|
if (resolved.breakOnError === undefined) resolved.breakOnError = true
|
||||||
|
if (!resolved.localRoot && folder) resolved.localRoot = folder.uri.fsPath
|
||||||
|
return resolved
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerNxTclDebugger(context: vscode.ExtensionContext): void {
|
||||||
|
const factory = new NxDebugAdapterFactory()
|
||||||
|
context.subscriptions.push(
|
||||||
|
vscode.debug.registerDebugAdapterDescriptorFactory("nx-tcl", factory),
|
||||||
|
vscode.debug.registerDebugConfigurationProvider("nx-tcl", {
|
||||||
|
resolveDebugConfiguration
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { resolveDebugConfiguration }
|
||||||
@@ -29,10 +29,13 @@ import { checkIfConfigurationChanged, getInterpreterFromSetting } from "./common
|
|||||||
import { loadServerDefaults } from "./common/setup"
|
import { loadServerDefaults } from "./common/setup"
|
||||||
import { getLSClientTraceLevel } from "./common/utilities"
|
import { getLSClientTraceLevel } from "./common/utilities"
|
||||||
import { createOutputChannel, onDidChangeConfiguration, registerCommand } from "./common/vscodeapi"
|
import { createOutputChannel, onDidChangeConfiguration, registerCommand } from "./common/vscodeapi"
|
||||||
|
import { registerNxTclDebugger } from "./debugger/register"
|
||||||
|
|
||||||
let client: LanguageClient | undefined
|
let client: LanguageClient | undefined
|
||||||
|
|
||||||
export async function activate(context: vscode.ExtensionContext) {
|
export async function activate(context: vscode.ExtensionContext) {
|
||||||
|
registerNxTclDebugger(context)
|
||||||
|
|
||||||
// This is required to get server name and module. This should be
|
// This is required to get server name and module. This should be
|
||||||
// the first thing that we do in this extension.
|
// the first thing that we do in this extension.
|
||||||
const serverInfo = loadServerDefaults()
|
const serverInfo = loadServerDefaults()
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 NX Tcl Remote Debugger contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "nx-post-support",
|
"name": "nx-post-support",
|
||||||
"version": "2025.9.200",
|
"version": "2026.8.201",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "nx-post-support",
|
"name": "nx-post-support",
|
||||||
"version": "2025.9.200",
|
"version": "2026.8.201",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/vscode": "^1.96.0",
|
"@types/vscode": "^1.96.0",
|
||||||
"@vscode/vsce": "^3.2.1",
|
"@vscode/vsce": "^3.2.1",
|
||||||
|
|||||||
+106
-5
@@ -1,10 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "nx-post-support",
|
"name": "nx-post-support",
|
||||||
"displayName": "NX Postprocessor Support",
|
"displayName": "NX Postprocessor Support",
|
||||||
"description": "VS Code extension for NX CAM postprocessor development with syntax highlighting, formatting, linting, and auto-completion for CDL, TCL, and DEF files",
|
"description": "VS Code extension for NX CAM postprocessor development with language support and remote Tcl debugging for CDL, TCL, and DEF files",
|
||||||
"version": "2026.8.200",
|
"version": "2026.8.201",
|
||||||
"publisher": "Christoph",
|
"publisher": "Christoph",
|
||||||
"icon": "images/nx-1.png",
|
"icon": "images/nx-1.png",
|
||||||
|
"activationEvents": [
|
||||||
|
"onLanguage:tcl",
|
||||||
|
"onLanguage:cdl",
|
||||||
|
"onLanguage:def",
|
||||||
|
"onDebug"
|
||||||
|
],
|
||||||
"extensionDependencies": [
|
"extensionDependencies": [
|
||||||
"ms-python.python"
|
"ms-python.python"
|
||||||
],
|
],
|
||||||
@@ -23,15 +29,109 @@
|
|||||||
"UDE",
|
"UDE",
|
||||||
"Postprocessor",
|
"Postprocessor",
|
||||||
"NX CAM",
|
"NX CAM",
|
||||||
"Siemens NX"
|
"Siemens NX",
|
||||||
|
"debugger",
|
||||||
|
"remote debugging"
|
||||||
],
|
],
|
||||||
"engines": {
|
"engines": {
|
||||||
"vscode": "^1.96.0"
|
"vscode": "^1.96.0"
|
||||||
},
|
},
|
||||||
"categories": [
|
"categories": [
|
||||||
"Programming Languages"
|
"Programming Languages",
|
||||||
|
"Debuggers"
|
||||||
],
|
],
|
||||||
"contributes": {
|
"contributes": {
|
||||||
|
"breakpoints": [
|
||||||
|
{
|
||||||
|
"language": "tcl"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"language": "def"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"debuggers": [
|
||||||
|
{
|
||||||
|
"type": "nx-tcl",
|
||||||
|
"label": "NX Tcl Remote Debugger",
|
||||||
|
"languages": [
|
||||||
|
"tcl",
|
||||||
|
"def"
|
||||||
|
],
|
||||||
|
"configurationAttributes": {
|
||||||
|
"attach": {
|
||||||
|
"required": [
|
||||||
|
"host",
|
||||||
|
"port"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"host": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "127.0.0.1",
|
||||||
|
"description": "Host on which the NX Tcl runtime is listening."
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 4711,
|
||||||
|
"description": "TCP port of the NX Tcl runtime."
|
||||||
|
},
|
||||||
|
"connectTimeout": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 120000,
|
||||||
|
"description": "How long VS Code retries until NX starts the runtime, in milliseconds."
|
||||||
|
},
|
||||||
|
"stopOnEntry": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Stop on the first traced Tcl command."
|
||||||
|
},
|
||||||
|
"breakOnError": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Stop when a traced Tcl command returns TCL_ERROR."
|
||||||
|
},
|
||||||
|
"localRoot": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Local source root opened in VS Code."
|
||||||
|
},
|
||||||
|
"remoteRoot": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Corresponding source root on the NX host when it differs from localRoot."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"initialConfigurations": [
|
||||||
|
{
|
||||||
|
"type": "nx-tcl",
|
||||||
|
"request": "attach",
|
||||||
|
"name": "Attach to NX Post Tcl",
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 4711,
|
||||||
|
"connectTimeout": 120000,
|
||||||
|
"stopOnEntry": false,
|
||||||
|
"breakOnError": true,
|
||||||
|
"localRoot": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configurationSnippets": [
|
||||||
|
{
|
||||||
|
"label": "NX Tcl: Attach to NX Post",
|
||||||
|
"description": "Attach VS Code to an NX runtime embedded in NX Post.",
|
||||||
|
"body": {
|
||||||
|
"type": "nx-tcl",
|
||||||
|
"request": "attach",
|
||||||
|
"name": "Attach to NX Post Tcl",
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 4711,
|
||||||
|
"connectTimeout": 120000,
|
||||||
|
"stopOnEntry": false,
|
||||||
|
"breakOnError": true,
|
||||||
|
"localRoot": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"languages": [
|
"languages": [
|
||||||
{
|
{
|
||||||
"id": "tcl",
|
"id": "tcl",
|
||||||
@@ -151,7 +251,8 @@
|
|||||||
"compile": "node esbuild.js --production",
|
"compile": "node esbuild.js --production",
|
||||||
"compile:debug": "node esbuild.js",
|
"compile:debug": "node esbuild.js",
|
||||||
"watch": "node esbuild.js --watch",
|
"watch": "node esbuild.js --watch",
|
||||||
"package": "node esbuild.js --production"
|
"package": "node esbuild.js --production",
|
||||||
|
"test:debugger": "node --test test/debugger.test.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/vscode": "^1.96.0",
|
"@types/vscode": "^1.96.0",
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
"use strict"
|
||||||
|
|
||||||
|
const test = require("node:test")
|
||||||
|
const assert = require("node:assert/strict")
|
||||||
|
const { PassThrough } = require("node:stream")
|
||||||
|
const { NxDebugAdapter } = require("../client/src/debugger/debugAdapter")
|
||||||
|
const manifest = require("../package.json")
|
||||||
|
|
||||||
|
function dapMessage(message) {
|
||||||
|
const json = JSON.stringify(message)
|
||||||
|
return `Content-Length: ${Buffer.byteLength(json, "utf8")}\r\n\r\n${json}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function readDapMessage(stream) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let buffer = Buffer.alloc(0)
|
||||||
|
const timer = setTimeout(() => reject(new Error("DAP response timed out")), 2000)
|
||||||
|
const onData = (chunk) => {
|
||||||
|
buffer = Buffer.concat([buffer, chunk])
|
||||||
|
const headerEnd = buffer.indexOf("\r\n\r\n")
|
||||||
|
if (headerEnd < 0) return
|
||||||
|
const header = buffer.subarray(0, headerEnd).toString("ascii")
|
||||||
|
const match = /Content-Length:\s*(\d+)/i.exec(header)
|
||||||
|
if (!match) return
|
||||||
|
const length = Number(match[1])
|
||||||
|
if (buffer.length < headerEnd + 4 + length) return
|
||||||
|
clearTimeout(timer)
|
||||||
|
stream.removeListener("data", onData)
|
||||||
|
resolve(JSON.parse(buffer.subarray(headerEnd + 4, headerEnd + 4 + length)))
|
||||||
|
}
|
||||||
|
stream.on("data", onData)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
test("integrated manifest contributes the NX Tcl debugger", () => {
|
||||||
|
const debuggerContribution = manifest.contributes.debuggers.find(
|
||||||
|
(entry) => entry.type === "nx-tcl"
|
||||||
|
)
|
||||||
|
assert.ok(debuggerContribution)
|
||||||
|
assert.deepEqual(debuggerContribution.languages, ["tcl", "def"])
|
||||||
|
assert.deepEqual(
|
||||||
|
manifest.contributes.breakpoints.map((entry) => entry.language),
|
||||||
|
["tcl", "def"]
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("integrated adapter answers a DAP initialize request", async () => {
|
||||||
|
const input = new PassThrough()
|
||||||
|
const output = new PassThrough()
|
||||||
|
const adapter = new NxDebugAdapter(input, output)
|
||||||
|
const responsePromise = readDapMessage(output)
|
||||||
|
|
||||||
|
input.write(
|
||||||
|
dapMessage({ seq: 1, type: "request", command: "initialize", arguments: {} })
|
||||||
|
)
|
||||||
|
const response = await responsePromise
|
||||||
|
adapter.dispose()
|
||||||
|
|
||||||
|
assert.equal(response.success, true)
|
||||||
|
assert.equal(response.command, "initialize")
|
||||||
|
assert.equal(response.body.supportsConfigurationDoneRequest, true)
|
||||||
|
assert.equal(response.body.supportsSetVariable, true)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user