Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
305 changes: 305 additions & 0 deletions .github/copilot-instructions.md

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions defaultDebugConfigs.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,13 @@
"requestAvailableSourceDirectories": {
"name": "display -env java.class.path",
"successRegularExpression": ".*java\\.class\\.path\\s+=\\s+(?<directories>.*)"
},
"infoStack": {
"name": "infostack -a",
"successRegularExpression": "^\\s*\\+\\s+[^\\s]+\\s+\\[[^\\]]+\\]\\s+.+:[0-9]+",
"failRegularExpressions": [
"isdb>\\s*$"
]
}
},
"executionFinishedRegularExpressions": [
Expand Down
43 changes: 32 additions & 11 deletions src/CobolDebug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { CobolBreakpoint } from './breakpoint/CobolBreakpoint';
import { CobolStack } from './CobolStack';
import { Configuration } from './config/Configuration';
import * as fs from "fs";
import { CobolStackFrame } from './debugProcess/CobolStackFrame';

/** Scope of current variables */
const CURRENT_VARIABLES_SCOPE_NAME = "Current variables";
Expand Down Expand Up @@ -56,6 +57,8 @@ export class CobolDebugSession extends DebugSession {
private breakpointManager: BreakpointManager | undefined;
/** Indicates wheter the code is running or not */
private running: boolean = false;
/** Promise for the stack trace request */
private stackTraceRequestPromise: Promise<CobolStackFrame[]> | undefined;
/** Variables currently watched */
private watchedVariables: WatchedVariable[] = [];

Expand Down Expand Up @@ -193,17 +196,35 @@ export class CobolDebugSession extends DebugSession {
}

protected stackTraceRequest(response: DebugProtocol.StackTraceResponse, _args: DebugProtocol.StackTraceArguments): void {
response.body = {
stackFrames: [{
id: 1,
column: 1,
line: this.cobolStack.currentLineNumber,
name: "Current",
source: new Source(this.cobolStack.currentSourceName)
}],
totalFrames: 1
};
this.sendResponse(response);
// If the debug runtime is not ready or the code is running, we return an empty stack trace
if (!this.debugRuntime || this.running) {
return this.sendResponse(response);
}

// To avoid multiple simultaneous stack trace requests, we use a promise to queue them.
if (!this.stackTraceRequestPromise) {
this.stackTraceRequestPromise = this.debugRuntime.requestCallStack();
}

void this.stackTraceRequestPromise.then((frames) => {
const stackFrames = frames.map((frame, index) => {
const isCurrentFrame = index === 0;
const line = isCurrentFrame ? this.cobolStack.currentLineNumber : (frame.line !== undefined ? frame.line : 0);
const filePath = isCurrentFrame ? this.cobolStack.currentSourceName : frame.file;
const source = filePath ? new Source(filePath) : undefined;
return { id: index + 1, column: 1, line, name: `${frame.paragraph} [${frame.program}]`, source };
});
response.body = { stackFrames, totalFrames: stackFrames.length };
this.sendResponse(response);
}).catch(() => {
response.body = {
stackFrames: [{ id: 1, column: 1, line: this.cobolStack.currentLineNumber, name: "Current", source: new Source(this.cobolStack.currentSourceName) }],
totalFrames: 1
};
this.sendResponse(response);
}).finally(() => {
this.stackTraceRequestPromise = undefined;
});
}

protected nextRequest(response: DebugProtocol.NextResponse, _args: DebugProtocol.NextArguments): void {
Expand Down
13 changes: 13 additions & 0 deletions src/debugProcess/CobolStackFrame.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Represents a single frame in the COBOL call stack
*/
export interface CobolStackFrame {
/** Paragraph name where execution is paused or called from */
paragraph: string;
/** COBOL program name containing the paragraph */
program: string;
/** Source file from infostack output (file name or full path) */
file?: string;
/** Line number from infostack output */
line?: number;
}
1 change: 1 addition & 0 deletions src/debugProcess/DebugConfigs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export interface IDebugCommands {
removeVariableMonitor: ICommand
removeAllVariableMonitors: ICommand
requestAvailableSourceDirectories?: ICommand
infoStack?: ICommand
}

export interface ICommand {
Expand Down
7 changes: 7 additions & 0 deletions src/debugProcess/DebugInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { DebugPosition } from "./DebugPosition";
import { CobolBreakpoint } from "../breakpoint/CobolBreakpoint";
import { CobolParagraphBreakpoint } from "../breakpoint/CobolParagraphBreakpoint";
import { CobolMonitor } from "../monitor/CobolMonitor";
import { CobolStackFrame } from "./CobolStackFrame";

export interface DebugInterface {

Expand Down Expand Up @@ -128,4 +129,10 @@ export interface DebugInterface {
*/
sendRawCommand(command: string): void;

/**
* Requests the call stack from the external debugger.
* Returns an array of stack frames ordered from innermost (current) to outermost.
*/
requestCallStack(): Promise<CobolStackFrame[]>;

}
71 changes: 64 additions & 7 deletions src/debugProcess/ExternalDebugAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { UnmonitorAllCommand } from "./UnmonitorAllCommand";
import { DebugConfigsProvider, ICommand, IDebugCommands } from "./DebugConfigs";
import { RequestAvailableSourceDirectoriesCommand } from "./RequestAvailableSourceDirectoriesCommand";
import { FallbackDirectoriesFinder } from "./FallbackDirectoriesFinder";
import { InfoStackCommand } from "./InfoStackCommand";
import { CobolStackFrame } from "./CobolStackFrame";

/**
* Class to interact with external debugger, sending commands and parsing it's outputs.
Expand All @@ -37,12 +39,11 @@ export class ExternalDebugAdapter implements DebugInterface {
private fallbackFinder: FallbackDirectoriesFinder;

constructor(commandLineToStartProcess: string,
outputRedirector: (output: string) => void,
configFilePath: string,
traceFilePath: string,
externalPathResolver: string,
processProvider?: ProcessProvider)
{
outputRedirector: (output: string) => void,
configFilePath: string,
traceFilePath: string,
externalPathResolver: string,
processProvider?: ProcessProvider) {

// Configuration to interact with external debug process
this.configs = new DebugConfigsProvider(configFilePath);
Expand All @@ -52,7 +53,7 @@ export class ExternalDebugAdapter implements DebugInterface {
// Regular expressions to retry command execution on external debugger
const retriesText = this.configs.retriesRegularExpressions;
this.retriesRegexes = retriesText ? this.createRegExpArray(retriesText)
: undefined;
: undefined;

this.fallbackFinder = new FallbackDirectoriesFinder(this, externalPathResolver);

Expand Down Expand Up @@ -304,6 +305,61 @@ export class ExternalDebugAdapter implements DebugInterface {
}
}

/**
* Requests the call stack from the external debugger.
* @returns A promise resolving to the call stack frames.
*/
requestCallStack(): Promise<CobolStackFrame[]> {
return new Promise((resolve, reject) => {
if (!this.commands.infoStack) {
return reject("infoStack command not configured");
}
const cmd = new InfoStackCommand(this.commands.infoStack);
this.sendCommand(cmd.buildCommand(), cmd.getExpectedRegExes()).then(output => {
const frames = cmd.validateOutput(output);
this.resolveCallStackSources(frames).then(resolved => resolve(resolved)).catch(() => resolve(frames));
}).catch((error) => {
return reject(error);
});
});
}

/**
* Resolves source file names from infostack output into full paths when possible.
* Uses the same strategy already used by debug position resolution.
*/
private resolveCallStackSources(frames: CobolStackFrame[]): Promise<CobolStackFrame[]> {
return frames.reduce((chain, frame) => {
return chain.then(resolved => {
return this.resolveFrameSourcePath(frame).then(located => {
resolved.push(located);
return resolved;
}).catch(() => {
resolved.push(frame);
return resolved;
});
});
}, Promise.resolve([] as CobolStackFrame[]));
}

/**
* Resolves a frame source file name into a full file path when available.
*/
private resolveFrameSourcePath(frame: CobolStackFrame): Promise<CobolStackFrame> {
return new Promise((resolve, reject) => {
if (!frame.file || frame.file.includes("\\") || frame.file.includes("/")) {
return resolve(frame);
}

this.fallbackFinder.lookForSourceOnFallbackDirectories(frame.file!).then(fullFileName => {
if (fullFileName) {
return resolve({ ...frame, file: fullFileName });
}
return resolve(frame);
}).catch(error => reject(error));
});
}


/**
* Sends command to external COBOL debugger expecting an output that matches with the specified regular expressions
Expand All @@ -312,6 +368,7 @@ export class ExternalDebugAdapter implements DebugInterface {
* @param expectedRegexes regular expressions to match debugger output
*/
private sendCommand(command: string, expectedRegexes: RegExp[]): Promise<string> {

return new Promise((resolve, reject) => {
this.debugProcess.sendCommand({
command: command,
Expand Down
59 changes: 59 additions & 0 deletions src/debugProcess/InfoStackCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { DebugCommand } from "./DebugCommand";
import { ICommand } from "./DebugConfigs";
import { GenericDebugCommand } from "./GenericDebugCommand";
import { CobolStackFrame } from "./CobolStackFrame";

/** Regex to match each line of infostack output, e.g.:
* + M00-INICIO [SRIM00] SRIM00.CBL:51922
* + PARA [METHOD of PROGRAM] STCGER.CBL:2095
*/
const STACK_LINE_REGEX = /^\s*\+\s+([^\s]+)\s+\[([^\]]+)\]\s+(.+):([0-9]+)\s*$/gm;

/** Index of the paragraph capture group in STACK_LINE_REGEX */
const PARAGRAPH_GROUP = 1;
/** Index of the bracket content capture group in STACK_LINE_REGEX */
const BRACKET_CONTENT_GROUP = 2;
/** Index of the source capture group in STACK_LINE_REGEX */
const SOURCE_GROUP = 3;
/** Index of the line capture group in STACK_LINE_REGEX */
const LINE_GROUP = 4;

/** Regex to extract the program name from "METHOD of PROGRAM" notation */
const OF_PROGRAM_REGEX = /of\s+([\w]+)/i;

/**
* Command to request the COBOL call stack from the external debugger via 'infostack'.
* Returns frames ordered innermost first (current execution point at index 0).
*/
export class InfoStackCommand implements DebugCommand<void, CobolStackFrame[]> {

constructor(private command: ICommand) { }

buildCommand(): string {
return this.command.name;
}

getExpectedRegExes(): RegExp[] {
return new GenericDebugCommand(this.command).getExpectedRegExes("im");
}

validateOutput(output: string): CobolStackFrame[] {
const frames: CobolStackFrame[] = [];
const regex = new RegExp(STACK_LINE_REGEX.source, "gm");
let match: RegExpExecArray | null;
while ((match = regex.exec(output)) !== null) {
const paragraph = match[PARAGRAPH_GROUP];
const bracketContent = match[BRACKET_CONTENT_GROUP];
const source = match[SOURCE_GROUP].trim();
const line = Number(match[LINE_GROUP]);
const ofMatch = OF_PROGRAM_REGEX.exec(bracketContent);
const program = ofMatch ? ofMatch[PARAGRAPH_GROUP] : bracketContent.trim();
frames.push({ paragraph, program, file: source, line: line });
}

// The frames are extracted in outermost to innermost order, so we reverse them before returning.
frames.reverse();
return frames;
}

}
1 change: 1 addition & 0 deletions src/test/parser/VariableParser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class MockDebugInterface implements DebugInterface {
addParagraphBreakpoint(_breakpoint: CobolParagraphBreakpoint): Promise<string | undefined> { return Promise.resolve(undefined); }
addBreakpointOnFirstLine(_program: string): Promise<boolean> { return Promise.resolve(true); }
sendRawCommand(_cmd: string): Promise<string> { return Promise.resolve('ok'); }
requestCallStack(): Promise<import('../../debugProcess/CobolStackFrame').CobolStackFrame[]> { return Promise.resolve([]); }
onOutput() { }
onStopped() { }
onContinued() { }
Expand Down
Loading