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
3 changes: 1 addition & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,11 @@ jobs:
- macos-latest
- windows-latest
node:
- 20
- 22
code-version:
- stable
- 1.66.0
include:
- { os: ubuntu-latest, node: 22, code-version: stable }
- { os: ubuntu-latest, node: 24, code-version: stable }
runs-on: ${{ matrix.os }}
name: Test with Node ${{ matrix.node }} and VSCode ${{ matrix.code-version }} on ${{ matrix.os }}
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## Next

* Use module build tasks by default, and run programs without GDB when not debugging [PR #44](https://github.com/ocamlpro/superbol-vscode-debug/pull/44)
* Enable customization of the COBOL runtime [PR #42](https://github.com/ocamlpro/superbol-vscode-debug/pull/42)
* Show global data items in Variables panel [PR #40](https://github.com/ocamlpro/superbol-vscode-debug/pull/40)
* Fixed a bug that made the extension hang when debugged programs displayed signed numbers [PR #39](https://github.com/ocamlpro/superbol-vscode-debug/pull/39)
Expand Down
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@
"@typescript-eslint/parser": "^6.7.2",
"@vscode/debugadapter-testsupport": "^1.51.0",
"@vscode/test-cli": "^0.0.12",
"@vscode/test-electron": "^2.5.2",
"@vscode/test-electron": "^3.1.0",
"eslint": "^8.50.0",
"eslint_d": "^12.2.1",
"mocha": "^10.8.2",
Expand Down
34 changes: 25 additions & 9 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,23 @@ const COBOL_RESERVED_WORDS = ["perform", "move", "to", "set", "add", "subtract",
const superbolConsts = {
taskType: "superbol",
taskSource: "SuperBOL",
debugBuildTaskName: "build (debug)",
buildTaskName: "build module",
debugBuildTaskName: "build module (debug)",
}
const defaultDebugBuildTask = `${superbolConsts.taskSource}: ${superbolConsts.debugBuildTaskName}`;

async function checkForSuperBOLBuildTask () {
/** Name of the default build task: debug build, unless we run without debugging. */
function defaultBuildTaskName (noDebug: boolean | undefined) {
return noDebug ? superbolConsts.buildTaskName : superbolConsts.debugBuildTaskName;
}

function taskLabel (taskName: string) {
return `${superbolConsts.taskSource}: ${taskName}`;
}

async function checkForSuperBOLBuildTask (taskName: string) {
return vscode.tasks.fetchTasks({ type: superbolConsts.taskType }).then((tasks) =>
tasks.find(task => task.source == superbolConsts.taskSource &&
task.name == superbolConsts.debugBuildTaskName) != undefined
task.name == taskName) != undefined
);
}

Expand All @@ -44,7 +53,8 @@ export function deactivate() {
class GdbConfigurationProvider implements vscode.DebugConfigurationProvider {
public resolveDebugConfiguration(workspaceFolder: vscode.WorkspaceFolder | undefined, config: vscode.DebugConfiguration, _token?: vscode.CancellationToken): vscode.ProviderResult<vscode.DebugConfiguration> {
config.gdbargs = ["-q", "--interpreter=mi2"];
return checkForSuperBOLBuildTask().then(haveSuperBOLBuildTask => {
const buildTaskName = defaultBuildTaskName(config.noDebug);
return checkForSuperBOLBuildTask(buildTaskName).then(haveSuperBOLBuildTask => {
if (config.name === undefined && haveSuperBOLBuildTask) {
config.name = "SuperBOL: default debug";
Comment thread
nberth marked this conversation as resolved.
Resolved
}
Expand All @@ -58,7 +68,11 @@ class GdbConfigurationProvider implements vscode.DebugConfigurationProvider {
workspaceFolder != undefined &&
config.request != "attach" &&
config.preLaunchTask === undefined) {
config.preLaunchTask = defaultDebugBuildTask;
config.preLaunchTask = taskLabel(buildTaskName);
// Default build tasks produce modules, so we run them with cobcrun.
if (config.useCobcrun === undefined) {
config.useCobcrun = true;
}
} else if (config.preLaunchTask === "") {
delete config.preLaunchTask;
}
Expand All @@ -72,7 +86,7 @@ class GdbConfigurationProvider implements vscode.DebugConfigurationProvider {
config.group = [];
}
const libcobpath = settings.libcobPath;
if (libcobpath != undefined) {
if (libcobpath) {
if (config.env === undefined) {
config.env = { ["LD_LIBRARY_PATH"]: libcobpath };
} else {
Expand All @@ -92,12 +106,14 @@ class GdbConfigurationProvider implements vscode.DebugConfigurationProvider {

public provideDebugConfigurations(_folder: vscode.WorkspaceFolder, _token?: vscode.CancellationToken)
: vscode.ProviderResult<vscode.DebugConfiguration[]> {
return checkForSuperBOLBuildTask().then(haveSuperBOLBuildTask => {
const buildTaskName = superbolConsts.debugBuildTaskName;
return checkForSuperBOLBuildTask(buildTaskName).then(haveSuperBOLBuildTask => {
const launchConfigDefault: vscode.DebugConfiguration = {
name: "SuperBOL: debug (launch)",
type: "superbol-gdb",
request: "launch",
preLaunchTask: haveSuperBOLBuildTask ? defaultDebugBuildTask : undefined,
preLaunchTask: haveSuperBOLBuildTask ? taskLabel(buildTaskName) : undefined,
useCobcrun: haveSuperBOLBuildTask ? true : undefined,
target: "${file}",
arguments: ""
};
Expand Down
69 changes: 64 additions & 5 deletions src/gdb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import {DebugProtocol} from '@vscode/debugprotocol';
import {Breakpoint, DebuggerVariable, localizeSymbols} from './debugger';
import {MINode} from './parser.mi2';
import * as path from "path";
import {MI2} from './mi2';
import * as fs from "fs";
import {MI2, cobExecutablePath, cobcrunArgs, removePathExtension} from './mi2';
import {CoverageStatus} from './coverage';
import {accessors as settings} from './settings';
import * as log from './log';
Expand Down Expand Up @@ -64,6 +65,24 @@ export interface AttachRequestArguments extends DebugProtocol.AttachRequestArgum
sourceDirs: string[];
}

/** Module file extensions, as produced by `cobc -m`. */
const MODULE_EXTENSIONS = [".so", ".dylib", ".dll"];

/** Time (in ms) to wait for the client to start the program in a terminal. */
const RUN_IN_TERMINAL_TIMEOUT_MS = 10000;

/** Identifier of the error sent when the program cannot be run in a terminal
* (follows the launch errors 100 and 103 below). */
const RUN_IN_TERMINAL_ERROR_ID = 104;

/** Guess if the target was built as a module: there is a module file but no
* executable. */
function isModuleTarget(target: string, cwd: string): boolean {
const name = path.join(cwd, removePathExtension(target));
return !fs.existsSync(cobExecutablePath(target, cwd)) &&
MODULE_EXTENSIONS.some(ext => fs.existsSync(name + ext));
}

function initLogLevel(verbose: boolean) {
if (verbose) {
log.setLevel(log.Level.Debug);
Expand All @@ -86,19 +105,61 @@ export class GDBDebugSession extends DebugSession {
coverageStatus: CoverageStatus;
private showCoverage: boolean = true;
private readonly showDetails = settings.displayVariableAttributes;
private supportsRunInTerminal: boolean = false;

protected initializeRequest(response: DebugProtocol.InitializeResponse, _args: DebugProtocol.InitializeRequestArguments): void {
protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void {
this.supportsRunInTerminal = !!args.supportsRunInTerminalRequest;
response.body.supportsSetVariable = true;
this.sendResponse(response);
}

/** Run the program in a terminal, without GDB. */
private runWithoutDebugger(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments,
cwd: string, cobcrunPath: string): void {
const command = args.useCobcrun
? [cobcrunPath].concat(cobcrunArgs(args.target, cwd))
: [cobExecutablePath(args.target, cwd)];
const programArgs = (args.arguments ?? "").split(/\s+/).filter(a => a !== "");
const env: { [key: string]: string | null } = {};
for (const [key, value] of Object.entries(args.env ?? {})) {
env[key] = value ?? null;
}
this.runInTerminalRequest({
kind: "integrated",
title: removePathExtension(args.target),
cwd: cwd,
args: command.concat(programArgs),
env: env,
}, RUN_IN_TERMINAL_TIMEOUT_MS, (runResponse) => {
if (runResponse.success) {
this.sendResponse(response);
// The program keeps running in the terminal; the session ends here.
this.sendEvent(new TerminatedEvent());
} else {
this.sendErrorResponse(response, RUN_IN_TERMINAL_ERROR_ID, `Failed to run program: ${runResponse.message}`);
}
});
Comment thread
engboris marked this conversation as resolved.
Resolved
}

protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void {
initLogLevel(args.verbose);

const cobcrunPath = args.cobcrunPath ?? settings.cobcrunPath;
// Run in the target executables' directory, unless specified; becomes '.' if target is a module name.
let cwd = args.cwd ?? path.dirname (args.target);

if (args.useCobcrun === undefined) {
args.useCobcrun = isModuleTarget(args.target, cwd);
}

if (args.noDebug && this.supportsRunInTerminal) {
this.runWithoutDebugger(response, args, cwd, cobcrunPath);
return;
}

this.showCoverage = args.coverage;
this.started = false;
this.attached = false;
const cobcrunPath = args.cobcrunPath ?? settings.cobcrunPath;

this.miDebugger = new MI2(settings.gdbPath, args.gdbargs, args.env, args.noDebug, args.gdbtty, cobcrunPath, args.useCobcrun, args.sourceDirs);
this.miDebugger.on("launcherror", (err: Error) => this.launchError(err));
Expand All @@ -118,8 +179,6 @@ export class GDBDebugSession extends DebugSession {
this.needContinue = false;
this.crashed = false;
this.debugReady = false;
// Run in the target executables' directory, unless specified; becomes '.' if target is a module name.
let cwd = args.cwd ?? path.dirname (args.target);
this.miDebugger.load(cwd, args.target, args.arguments, args.group, args.gdbtty).then(
/*onfulfilled:*/ () => {
setTimeout(() => {
Expand Down
60 changes: 50 additions & 10 deletions src/mi2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,57 @@ export function escape(str: string) {
return str.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
}

function removePathExtension(f: string) : string {
export function removePathExtension(f: string) : string {
return path.basename(f, path.extname(f));
}

const programIdRegex =
/PROGRAM-ID\s*\.?\s*(?:"([^"]+)"|'([^']+)'|([\w-]+))(?:\s+AS\s+(?:"([^"]+)"|'([^']+)'))?/i;

/** Name of the entry point to give to `cobcrun` for the given target: the
* name of the first program in the source file, or the name of the file if it
* cannot be found. It may differ from the name of the module file (e.g,
* `PROGRAM-ID. HELLO` in `hello.cob`), so `cobcrunArgs` also passes the file
* with `-M`. The name is returned as written in the source: `cobcrun`
* encodes it itself (see `cob_encode_program_id` in libcob) when resolving
* the entry point, as for any dynamic `CALL`. */
function moduleEntryName(target: string, cwd: string): string {
const file = path.resolve(cwd, target);
try {
for (const line of fs.readFileSync(file, "utf8").split(/\r?\n/)) {
// Skip comment lines (fixed and free formats).
if (/^.{6}[*/]/.test(line) || /^\s*\*>/.test(line)) {
continue;
}
const m = programIdRegex.exec(line);
if (m) {
return m[4] ?? m[5] ?? m[1] ?? m[2] ?? m[3];
}
}
} catch (e) {
log.debug(`could not read ${file}: ${e}`);
}
return removePathExtension(target);
}
Comment thread
nberth marked this conversation as resolved.
Resolved

/** Path to the executable built from the given target. */
export function cobExecutablePath(target: string, cwd: string): string {
target = removePathExtension(target);
if (!path.isAbsolute(target)) {
target = path.join(cwd, target);
}
if (process.platform === "win32") {
target = target + '.exe';
}
return target;
}

/** Arguments to give to `cobcrun` to run the module built from the given
* target. */
export function cobcrunArgs(target: string, cwd: string): string[] {
return ["-M", removePathExtension(target), moduleEntryName(target, cwd)];
}

export function couldBeOutput(line: string) {
return !nonOutput.exec(line);
}
Expand Down Expand Up @@ -139,22 +186,15 @@ export class MI2 extends EventEmitter implements IDebugger {
}

private targetCobExecutable(target: string, cwd: string) {
target = removePathExtension(target);
if (!path.isAbsolute(target)) {
target = path.join(cwd, target);
}
if (process.platform === "win32") {
target = target + '.exe';
}
return escape(target);
return escape(cobExecutablePath(target, cwd));
}

private launchCommands(target: string, targetargs: string, cwd: string) {
let targetExec = this.useCobcrun
? this.cobcrunPath
: this.targetCobExecutable(target, cwd);
targetargs = this.useCobcrun
? `${removePathExtension(target)} ${targetargs}`
? `${cobcrunArgs(target, cwd).join(" ")} ${targetargs}`
: targetargs;
return this.commonCommands(cwd).concat([
this.sendCommand("gdb-set args " + targetargs),
Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -316,10 +316,10 @@
supports-color "^10.2.2"
yargs "^17.7.2"

"@vscode/test-electron@^2.5.2":
version "2.5.2"
resolved "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.5.2.tgz"
integrity sha512-8ukpxv4wYe0iWMRQU18jhzJOHkeGKbnw7xWRX3Zw1WJA4cEKbHcmmLPdPrPtL6rhDcrlCZN+xKRpv09n4gRHYg==
"@vscode/test-electron@^3.1.0":
version "3.1.0"
resolved "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-3.1.0.tgz"
integrity sha512-CRqv5u+YYoseuNVJ6Tyo4k0sF0mx4qnKMihRB0PjsUF8Dc0WKtCXo6CNL6nWWm5esfFQsQA/pejMj4ZbpJVLTw==
dependencies:
http-proxy-agent "^7.0.2"
https-proxy-agent "^7.0.5"
Expand Down
Loading