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
651 lines
22 KiB
JavaScript
651 lines
22 KiB
JavaScript
'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 };
|