From 72508613fc40670473e6a742beddd0bda017c5dd Mon Sep 17 00:00:00 2001 From: Bob Brown Date: Fri, 3 Apr 2026 09:54:50 -0700 Subject: [PATCH 1/7] Adding Run Without Debugging feature --- .../src/Debugger/configurationProvider.ts | 2 + .../Debugger/debugAdapterDescriptorFactory.ts | 15 +- .../Debugger/runWithoutDebuggingAdapter.ts | 177 ++++++++++++++++++ 3 files changed, 191 insertions(+), 3 deletions(-) create mode 100644 Extension/src/Debugger/runWithoutDebuggingAdapter.ts diff --git a/Extension/src/Debugger/configurationProvider.ts b/Extension/src/Debugger/configurationProvider.ts index 8f0ae0c7c..b76cbbae1 100644 --- a/Extension/src/Debugger/configurationProvider.ts +++ b/Extension/src/Debugger/configurationProvider.ts @@ -147,12 +147,14 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv Telemetry.logDebuggerEvent(DebuggerEvent.debugPanel, { "debugType": DebugType.debug, "configSource": folder ? ConfigSource.workspaceFolder : ConfigSource.singleFile, "configMode": ConfigMode.noLaunchConfig, "cancelled": "true", "succeeded": "true" }); return undefined; // aborts debugging silently } else { + const noDebug = config.noDebug ?? false; // preserve the noDebug value from the config if it exists. // Currently, we expect only one debug config to be selected. console.assert(configs.length === 1, "More than one debug config is selected."); config = configs[0]; // Keep track of the entry point where the debug config has been selected, for telemetry purposes. config.debuggerEvent = DebuggerEvent.debugPanel; config.configSource = folder ? ConfigSource.workspaceFolder : ConfigSource.singleFile; + config.noDebug = noDebug; } } diff --git a/Extension/src/Debugger/debugAdapterDescriptorFactory.ts b/Extension/src/Debugger/debugAdapterDescriptorFactory.ts index d43d71bc3..546e14369 100644 --- a/Extension/src/Debugger/debugAdapterDescriptorFactory.ts +++ b/Extension/src/Debugger/debugAdapterDescriptorFactory.ts @@ -7,11 +7,12 @@ import * as os from 'os'; import * as path from 'path'; import * as vscode from "vscode"; import * as nls from 'vscode-nls'; +import { RunWithoutDebuggingAdapter } from './runWithoutDebuggingAdapter'; nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); const localize: nls.LocalizeFunc = nls.loadMessageBundle(); -// Registers DebugAdapterDescriptorFactory for `cppdbg` and `cppvsdbg`. If it is not ready, it will prompt a wait for the download dialog. +// Registers DebugAdapterDescriptorFactory for `cppdbg` and `cppvsdbg`. // NOTE: This file is not automatically tested. abstract class AbstractDebugAdapterDescriptorFactory implements vscode.DebugAdapterDescriptorFactory { @@ -27,7 +28,11 @@ abstract class AbstractDebugAdapterDescriptorFactory implements vscode.DebugAdap export class CppdbgDebugAdapterDescriptorFactory extends AbstractDebugAdapterDescriptorFactory { - async createDebugAdapterDescriptor(_session: vscode.DebugSession, _executable?: vscode.DebugAdapterExecutable): Promise { + async createDebugAdapterDescriptor(session: vscode.DebugSession, _executable?: vscode.DebugAdapterExecutable): Promise { + if (session.configuration.noDebug) { + return new vscode.DebugAdapterInlineImplementation(new RunWithoutDebuggingAdapter()); + } + const adapter: string = "./debugAdapters/bin/OpenDebugAD7" + (os.platform() === 'win32' ? ".exe" : ""); const command: string = path.join(this.context.extensionPath, adapter); @@ -38,7 +43,11 @@ export class CppdbgDebugAdapterDescriptorFactory extends AbstractDebugAdapterDes export class CppvsdbgDebugAdapterDescriptorFactory extends AbstractDebugAdapterDescriptorFactory { - async createDebugAdapterDescriptor(_session: vscode.DebugSession, _executable?: vscode.DebugAdapterExecutable): Promise { + async createDebugAdapterDescriptor(session: vscode.DebugSession, _executable?: vscode.DebugAdapterExecutable): Promise { + if (session.configuration.noDebug) { + return new vscode.DebugAdapterInlineImplementation(new RunWithoutDebuggingAdapter()); + } + if (os.platform() !== 'win32') { void vscode.window.showErrorMessage(localize("debugger.not.available", "Debugger type '{0}' is not available for non-Windows machines.", "cppvsdbg")); return null; diff --git a/Extension/src/Debugger/runWithoutDebuggingAdapter.ts b/Extension/src/Debugger/runWithoutDebuggingAdapter.ts new file mode 100644 index 000000000..a1e2434f7 --- /dev/null +++ b/Extension/src/Debugger/runWithoutDebuggingAdapter.ts @@ -0,0 +1,177 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +import * as cp from 'child_process'; +import * as os from 'os'; +import * as path from 'path'; +import * as vscode from 'vscode'; + +/** + * A minimal inline Debug Adapter that runs the target program directly without a debug adapter + * when the user invokes "Run Without Debugging". + */ +export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter { + private readonly sendMessageEmitter = new vscode.EventEmitter(); + public readonly onDidSendMessage: vscode.Event = this.sendMessageEmitter.event; + + private seq: number = 1; + private childProcess?: cp.ChildProcess; + private terminal?: vscode.Terminal; + + public handleMessage(message: vscode.DebugProtocolMessage): void { + const msg = message as { type: string; command: string; seq: number; arguments?: any; }; + if (msg.type === 'request') { + void this.handleRequest(msg); + } + } + + private async handleRequest(request: { command: string; seq: number; arguments?: any; }): Promise { + switch (request.command) { + case 'initialize': + this.sendResponse(request, {}); + this.sendEvent('initialized'); + break; + case 'launch': + await this.launch(request); + break; + case 'configurationDone': + this.sendResponse(request, {}); + break; + case 'disconnect': + case 'terminate': + this.sendResponse(request, {}); + break; + default: + this.sendResponse(request, {}); + break; + } + } + + private async launch(request: { command: string; seq: number; arguments?: any; }): Promise { + const config = request.arguments as { + program?: string; + args?: string[]; + cwd?: string; + environment?: { name: string; value: string; }[]; + console?: string; + }; + + const program: string = config.program ?? ''; + const args: string[] = config.args ?? []; + const cwd: string | undefined = config.cwd; + const environment: { name: string; value: string; }[] = config.environment ?? []; + const consoleMode: string = config.console ?? 'integratedTerminal'; + + // Merge the launch config's environment variables on top of the inherited process environment. + const env: NodeJS.ProcessEnv = { ...process.env }; + for (const e of environment) { + env[e.name] = e.value; + } + + this.sendResponse(request, {}); + + if (consoleMode === 'integratedTerminal') { + this.launchIntegratedTerminal(program, args, cwd, env); + } else if (consoleMode === 'externalTerminal') { + this.launchExternalTerminal(program, args, cwd, env); + } else { + this.launchInternalConsole(program, args, cwd, env); + } + } + + /** + * Launch the program in a VS Code integrated terminal. + * The terminal will remain open after the program exits and be reused for the next session, if applicable. + */ + private launchIntegratedTerminal(program: string, args: string[], cwd: string | undefined, env: NodeJS.ProcessEnv) { + const shellArgs: string[] = [program, ...args].map(a => this.quoteArg(a)); + const terminalName = path.normalize(program); + const existingTerminal = vscode.window.terminals.find(t => t.name === terminalName); + this.terminal = existingTerminal ?? vscode.window.createTerminal({ + name: terminalName, + cwd, + env: env as Record + }); + this.terminal.show(true); + this.terminal.sendText(shellArgs.join(' ')); + + // The terminal manages its own lifecycle; notify VS Code the "debug" session is done. + this.sendEvent('terminated'); + } + + /** + * Launch the program in an external terminal. We do not keep track of this terminal or the spawned process. + */ + private launchExternalTerminal(program: string, args: string[], cwd: string | undefined, env: NodeJS.ProcessEnv): void { + const quotedArgs: string[] = [program, ...args].map(a => this.quoteArg(a)); + const cmdLine: string = quotedArgs.join(' '); + const platform: string = os.platform(); + if (platform === 'win32') { + cp.spawn('cmd.exe', ['/c', 'start', 'cmd.exe', '/K', cmdLine], { cwd, env, detached: true, stdio: 'ignore' }).unref(); + } else if (platform === 'darwin') { + cp.spawn('osascript', ['-e', `tell application "Terminal" to do script "${cmdLine.replace(/"/g, '\\"')}"`], { cwd, env, detached: true, stdio: 'ignore' }).unref(); + } else { + cp.spawn('x-terminal-emulator', ['-e', cmdLine], { cwd, env, detached: true, stdio: 'ignore' }).unref(); + } + this.sendEvent('terminated'); + } + + /** + * Spawn the process and forward stdout/stderr as DAP output events. + */ + private launchInternalConsole(program: string, args: string[], cwd: string | undefined, env: NodeJS.ProcessEnv) { + this.childProcess = cp.spawn(program, args, { cwd, env }); + + this.childProcess.stdout?.on('data', (data: Buffer) => { + this.sendEvent('output', { category: 'stdout', output: data.toString() }); + }); + this.childProcess.stderr?.on('data', (data: Buffer) => { + this.sendEvent('output', { category: 'stderr', output: data.toString() }); + }); + this.childProcess.on('error', (err: Error) => { + this.sendEvent('output', { category: 'stderr', output: `${err.message}\n` }); + this.sendEvent('exited', { exitCode: 1 }); + this.sendEvent('terminated'); + }); + this.childProcess.on('exit', (code: number | null) => { + this.sendEvent('exited', { exitCode: code ?? 0 }); + this.sendEvent('terminated'); + }); + } + + private quoteArg(arg: string): string { + return /\s/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg; + } + + private sendResponse(request: { command: string; seq: number; }, body: object): void { + this.sendMessageEmitter.fire({ + type: 'response', + seq: this.seq++, + request_seq: request.seq, + success: true, + command: request.command, + body + } as vscode.DebugProtocolMessage); + } + + private sendEvent(event: string, body?: object): void { + this.sendMessageEmitter.fire({ + type: 'event', + seq: this.seq++, + event, + body + } as vscode.DebugProtocolMessage); + } + + public dispose(): void { + this.terminateProcess(); + this.sendMessageEmitter.dispose(); + } + + private terminateProcess(): void { + this.childProcess?.kill(); + this.childProcess = undefined; + } +} From 448763e10565ce4cf16803ca5b2888c83290c59f Mon Sep 17 00:00:00 2001 From: Bob Brown Date: Fri, 3 Apr 2026 11:16:43 -0700 Subject: [PATCH 2/7] Updates for WSL --- Extension/src/Debugger/configurations.ts | 2 +- Extension/src/Debugger/runWithoutDebuggingAdapter.ts | 10 +++++++--- Extension/src/common.ts | 11 +++++++++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/Extension/src/Debugger/configurations.ts b/Extension/src/Debugger/configurations.ts index 96895c6da..bb2af8646 100644 --- a/Extension/src/Debugger/configurations.ts +++ b/Extension/src/Debugger/configurations.ts @@ -97,7 +97,7 @@ function createLaunchString(name: string, type: string, executable: string): str "stopAtEntry": false, "cwd": "$\{fileDirname\}", "environment": [], -${ type === "cppdbg" ? `"externalConsole": false` : `"console": "externalTerminal"` } +${ type === "cppdbg" ? `"externalConsole": false` : `"console": "integratedTerminal"` } `; } diff --git a/Extension/src/Debugger/runWithoutDebuggingAdapter.ts b/Extension/src/Debugger/runWithoutDebuggingAdapter.ts index a1e2434f7..b86974040 100644 --- a/Extension/src/Debugger/runWithoutDebuggingAdapter.ts +++ b/Extension/src/Debugger/runWithoutDebuggingAdapter.ts @@ -7,6 +7,7 @@ import * as cp from 'child_process'; import * as os from 'os'; import * as path from 'path'; import * as vscode from 'vscode'; +import { sessionIsWsl } from '../common'; /** * A minimal inline Debug Adapter that runs the target program directly without a debug adapter @@ -56,13 +57,14 @@ export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter { cwd?: string; environment?: { name: string; value: string; }[]; console?: string; + externalConsole?: boolean; }; const program: string = config.program ?? ''; const args: string[] = config.args ?? []; const cwd: string | undefined = config.cwd; const environment: { name: string; value: string; }[] = config.environment ?? []; - const consoleMode: string = config.console ?? 'integratedTerminal'; + const consoleMode: string = config.console ?? (config.externalConsole ? 'externalTerminal' : 'integratedTerminal'); // Merge the launch config's environment variables on top of the inherited process environment. const env: NodeJS.ProcessEnv = { ...process.env }; @@ -112,8 +114,10 @@ export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter { cp.spawn('cmd.exe', ['/c', 'start', 'cmd.exe', '/K', cmdLine], { cwd, env, detached: true, stdio: 'ignore' }).unref(); } else if (platform === 'darwin') { cp.spawn('osascript', ['-e', `tell application "Terminal" to do script "${cmdLine.replace(/"/g, '\\"')}"`], { cwd, env, detached: true, stdio: 'ignore' }).unref(); - } else { - cp.spawn('x-terminal-emulator', ['-e', cmdLine], { cwd, env, detached: true, stdio: 'ignore' }).unref(); + } else if (platform === 'linux' && sessionIsWsl()) { + cp.spawn('/mnt/c/Windows/System32/cmd.exe', ['/c', 'start', 'bash', '-c', `${cmdLine};read -p 'Press enter to continue...'`], { env, detached: true, stdio: 'ignore' }).unref(); + } else { // platform === 'linux' + cp.spawn('bash', ['-c', `${cmdLine};read -p 'Press enter to continue...'`], { cwd, env, detached: true, stdio: 'ignore' }).unref(); } this.sendEvent('terminated'); } diff --git a/Extension/src/common.ts b/Extension/src/common.ts index 849f4d6f3..c394a49cc 100644 --- a/Extension/src/common.ts +++ b/Extension/src/common.ts @@ -1850,3 +1850,14 @@ export function getVSCodeLanguageModel(): any | undefined { } return vscodelm; } + +export function sessionIsWsl(): boolean { + if (process.env.WSL_DISTRO_NAME) { + return true; + } + try { + return fs.readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft'); + } catch { + return false; + } +} From c315d2028d23b85ef2b4a3c9faed1fab3e6dc1d0 Mon Sep 17 00:00:00 2001 From: "Bob Brown (DEVDIV)" Date: Fri, 3 Apr 2026 13:52:59 -0700 Subject: [PATCH 3/7] Add support for linux terminals --- .../Debugger/runWithoutDebuggingAdapter.ts | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/Extension/src/Debugger/runWithoutDebuggingAdapter.ts b/Extension/src/Debugger/runWithoutDebuggingAdapter.ts index b86974040..6701ad11a 100644 --- a/Extension/src/Debugger/runWithoutDebuggingAdapter.ts +++ b/Extension/src/Debugger/runWithoutDebuggingAdapter.ts @@ -7,8 +7,12 @@ import * as cp from 'child_process'; import * as os from 'os'; import * as path from 'path'; import * as vscode from 'vscode'; +import * as nls from 'vscode-nls'; import { sessionIsWsl } from '../common'; +nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); +const localize = nls.loadMessageBundle(); + /** * A minimal inline Debug Adapter that runs the target program directly without a debug adapter * when the user invokes "Run Without Debugging". @@ -117,11 +121,48 @@ export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter { } else if (platform === 'linux' && sessionIsWsl()) { cp.spawn('/mnt/c/Windows/System32/cmd.exe', ['/c', 'start', 'bash', '-c', `${cmdLine};read -p 'Press enter to continue...'`], { env, detached: true, stdio: 'ignore' }).unref(); } else { // platform === 'linux' - cp.spawn('bash', ['-c', `${cmdLine};read -p 'Press enter to continue...'`], { cwd, env, detached: true, stdio: 'ignore' }).unref(); + this.launchLinuxExternalTerminal(cmdLine, cwd, env); } this.sendEvent('terminated'); } + /** + * On Linux, find and launch an available terminal emulator to run the command. + */ + private launchLinuxExternalTerminal(cmdLine: string, cwd: string | undefined, env: NodeJS.ProcessEnv): void { + const bashCmd = `${cmdLine}; echo; read -p 'Press enter to continue...'`; + const bashArgs = ['bash', '-c', bashCmd]; + + // Terminal emulators in order of preference, with the correct flag style for each. + const candidates: { cmd: string; buildArgs: () => string[] }[] = [ + { cmd: 'x-terminal-emulator', buildArgs: () => ['-e', ...bashArgs] }, + { cmd: 'gnome-terminal', buildArgs: () => ['-e', ...bashArgs] }, + { cmd: 'konsole', buildArgs: () => ['-e', ...bashArgs] }, + { cmd: 'xterm', buildArgs: () => ['-e', ...bashArgs] } + ]; + + // Honor the $TERMINAL environment variable if set. + const terminalEnv = process.env['TERMINAL']; + if (terminalEnv) { + candidates.unshift({ cmd: terminalEnv, buildArgs: () => ['-e', ...bashArgs] }); + } + + for (const candidate of candidates) { + try { + const result = cp.spawnSync('which', [candidate.cmd], { stdio: 'pipe' }); + if (result.status === 0) { + cp.spawn(candidate.cmd, candidate.buildArgs(), { cwd, env, detached: true, stdio: 'ignore' }).unref(); + return; + } + } catch { + continue; + } + } + + const message = localize('no.terminal.emulator', 'No terminal emulator found. Please set the $TERMINAL environment variable to your terminal emulator of choice, or install one of the following: x-terminal-emulator, gnome-terminal, konsole, xterm.'); + vscode.window.showErrorMessage(message); + } + /** * Spawn the process and forward stdout/stderr as DAP output events. */ From a86afd679edf22f3306a1adbf352c7d001987d03 Mon Sep 17 00:00:00 2001 From: Bob Brown Date: Fri, 3 Apr 2026 15:46:18 -0700 Subject: [PATCH 4/7] Add tests --- Extension/.vscode/launch.json | 4 + .../Debugger/runWithoutDebuggingAdapter.ts | 2 +- .../RunWithoutDebugging/assets/exitCode.cpp | 3 + .../runWithoutDebugging.integration.test.ts | 305 ++++++++++++++++++ 4 files changed, 313 insertions(+), 1 deletion(-) create mode 100644 Extension/test/scenarios/RunWithoutDebugging/assets/exitCode.cpp create mode 100644 Extension/test/scenarios/RunWithoutDebugging/tests/runWithoutDebugging.integration.test.ts diff --git a/Extension/.vscode/launch.json b/Extension/.vscode/launch.json index 4323f133d..a110f3407 100644 --- a/Extension/.vscode/launch.json +++ b/Extension/.vscode/launch.json @@ -97,6 +97,10 @@ "label": "MultirootDeadlockTest ", "value": "${workspaceFolder}/test/scenarios/MultirootDeadlockTest/assets/test.code-workspace" }, + { + "label": "RunWithoutDebugging ", + "value": "${workspaceFolder}/test/scenarios/RunWithoutDebugging/assets/" + }, { "label": "SimpleCppProject ", "value": "${workspaceFolder}/test/scenarios/SimpleCppProject/assets/simpleCppProject.code-workspace" diff --git a/Extension/src/Debugger/runWithoutDebuggingAdapter.ts b/Extension/src/Debugger/runWithoutDebuggingAdapter.ts index 6701ad11a..81dccb3eb 100644 --- a/Extension/src/Debugger/runWithoutDebuggingAdapter.ts +++ b/Extension/src/Debugger/runWithoutDebuggingAdapter.ts @@ -134,7 +134,7 @@ export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter { const bashArgs = ['bash', '-c', bashCmd]; // Terminal emulators in order of preference, with the correct flag style for each. - const candidates: { cmd: string; buildArgs: () => string[] }[] = [ + const candidates: { cmd: string; buildArgs(): string[] }[] = [ { cmd: 'x-terminal-emulator', buildArgs: () => ['-e', ...bashArgs] }, { cmd: 'gnome-terminal', buildArgs: () => ['-e', ...bashArgs] }, { cmd: 'konsole', buildArgs: () => ['-e', ...bashArgs] }, diff --git a/Extension/test/scenarios/RunWithoutDebugging/assets/exitCode.cpp b/Extension/test/scenarios/RunWithoutDebugging/assets/exitCode.cpp new file mode 100644 index 000000000..dd5772855 --- /dev/null +++ b/Extension/test/scenarios/RunWithoutDebugging/assets/exitCode.cpp @@ -0,0 +1,3 @@ +int main() { + return 37; +} diff --git a/Extension/test/scenarios/RunWithoutDebugging/tests/runWithoutDebugging.integration.test.ts b/Extension/test/scenarios/RunWithoutDebugging/tests/runWithoutDebugging.integration.test.ts new file mode 100644 index 000000000..782a9d6f4 --- /dev/null +++ b/Extension/test/scenarios/RunWithoutDebugging/tests/runWithoutDebugging.integration.test.ts @@ -0,0 +1,305 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ +/* eslint-disable @typescript-eslint/triple-slash-reference */ +/// +import * as assert from 'assert'; +import * as cp from 'child_process'; +import { suite } from 'mocha'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import * as util from '../../../../src/common'; +import { isLinux, isMacOS, isWindows } from '../../../../src/constants'; +import { getEffectiveEnvironment } from '../../../../src/LanguageServer/devcmd'; + +interface ProcessResult { + code: number | null; + stdout: string; + stderr: string; +} + +interface TrackerState { + setBreakpointsRequestReceived: boolean; + stoppedEventReceived: boolean; + exitedEventReceived: boolean; + exitedBeforeStop: boolean; + actualExitCode?: number; +} + +interface TrackerController { + state: TrackerState; + lastEvent: Promise<'stopped' | 'exited'>; + dispose(): void; +} + +function runProcess(command: string, args: string[], cwd: string, env?: NodeJS.ProcessEnv): Promise { + return new Promise((resolve, reject) => { + const child = cp.spawn(command, args, { cwd, env }); + let stdout = ''; + let stderr = ''; + + child.stdout.on('data', (data: Buffer) => { + stdout += data.toString(); + }); + + child.stderr.on('data', (data: Buffer) => { + stderr += data.toString(); + }); + + child.on('error', reject); + child.on('close', (code) => resolve({ code, stdout, stderr })); + }); +} + +async function setWindowsBuildEnvironment(): Promise { + const promise = vscode.commands.executeCommand('C_Cpp.SetVsDeveloperEnvironment', 'test'); + const timer = setInterval(() => { + void vscode.commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem'); + }, 1000); + await promise; + clearInterval(timer); + assert.strictEqual(util.hasMsvcEnvironment(), true, 'MSVC environment not set correctly.'); +} + +async function compileProgram(workspacePath: string, sourcePath: string, outputPath: string): Promise { + if (isWindows) { + await setWindowsBuildEnvironment(); + const env = getEffectiveEnvironment(); + const result = await runProcess('cl.exe', ['/nologo', '/EHsc', '/Zi', '/std:c++17', `/Fe:${outputPath}`, sourcePath], workspacePath, env); + assert.strictEqual(result.code, 0, `MSVC compilation failed. stdout: ${result.stdout}\nstderr: ${result.stderr}`); + return; + } + + if (isMacOS) { + const result = await runProcess('clang++', ['-std=c++17', '-g', sourcePath, '-o', outputPath], workspacePath); + assert.strictEqual(result.code, 0, `clang++ compilation failed. stdout: ${result.stdout}\nstderr: ${result.stderr}`); + return; + } + + if (isLinux) { + const result = await runProcess('g++', ['-std=c++17', '-g', sourcePath, '-o', outputPath], workspacePath); + assert.strictEqual(result.code, 0, `g++ compilation failed. stdout: ${result.stdout}\nstderr: ${result.stderr}`); + return; + } + + assert.fail(`Unsupported test platform: ${process.platform}`); +} + +async function createBreakpointAtReturnStatement(sourceUri: vscode.Uri): Promise { + const document = await vscode.workspace.openTextDocument(sourceUri); + const returnLine = document.getText().split(/\r?\n/).findIndex((line) => line.includes('return 37;')); + assert.notStrictEqual(returnLine, -1, 'Unable to find expected return statement for breakpoint placement.'); + const breakpoint = new vscode.SourceBreakpoint(new vscode.Location(sourceUri, new vscode.Position(returnLine, 0)), true); + vscode.debug.addBreakpoints([breakpoint]); + return breakpoint; +} + +function createSessionTerminatedPromise(sessionName: string): Promise { + return new Promise((resolve) => { + const terminateSubscription = vscode.debug.onDidTerminateDebugSession((session) => { + if (session.name === sessionName) { + terminateSubscription.dispose(); + resolve(); + } + }); + }); +} + +function createTracker(debugType: string, sessionName: string, timeoutMs: number, timeoutMessage: string): TrackerController { + const state: TrackerState = { + setBreakpointsRequestReceived: false, + stoppedEventReceived: false, + exitedEventReceived: false, + exitedBeforeStop: false + }; + + let trackerRegistration: vscode.Disposable | undefined; + let timeoutHandle: NodeJS.Timeout | undefined; + + const lastEvent = new Promise<'stopped' | 'exited'>((resolve, reject) => { + timeoutHandle = setTimeout(() => { + trackerRegistration?.dispose(); + trackerRegistration = undefined; + reject(new Error(timeoutMessage)); + }, timeoutMs); + + trackerRegistration = vscode.debug.registerDebugAdapterTrackerFactory(debugType, { + createDebugAdapterTracker: (session: vscode.DebugSession): vscode.DebugAdapterTracker | undefined => { + if (session.name !== sessionName) { + return undefined; + } + + return { + onWillReceiveMessage: (message: any): void => { + if (message?.type === 'request' && message?.command === 'setBreakpoints') { + state.setBreakpointsRequestReceived = true; + } + }, + onDidSendMessage: (message: any): void => { + if (message?.type !== 'event') { + return; + } + + if (message.event === 'stopped') { + state.stoppedEventReceived = true; + if (timeoutHandle) { + clearTimeout(timeoutHandle); + timeoutHandle = undefined; + } + resolve('stopped'); + } + + if (message.event === 'exited') { + state.exitedEventReceived = true; + state.actualExitCode = message.body?.exitCode; + if (!state.stoppedEventReceived) { + state.exitedBeforeStop = true; + } + if (timeoutHandle) { + clearTimeout(timeoutHandle); + timeoutHandle = undefined; + } + resolve('exited'); + } + } + }; + } + }); + }); + + return { + state, + lastEvent, + dispose(): void { + if (timeoutHandle) { + clearTimeout(timeoutHandle); + timeoutHandle = undefined; + } + trackerRegistration?.dispose(); + trackerRegistration = undefined; + } + }; +} + +suite('Run Without Debugging Integration Test', function (): void { + + suiteSetup(async function (): Promise { + const extension: vscode.Extension = vscode.extensions.getExtension('ms-vscode.cpptools') || assert.fail('Extension not found'); + if (!extension.isActive) { + await extension.activate(); + } + }); + + suiteTeardown(async function (): Promise { + if (isWindows) { + await vscode.commands.executeCommand('C_Cpp.ClearVsDeveloperEnvironment'); + } + }); + + test('Run Without Debugging should not break on breakpoints and emit expected exit code', async () => { + const expectedExitCode = 37; + const workspaceFolder = vscode.workspace.workspaceFolders?.[0] ?? assert.fail('No workspace folder available'); + const workspacePath = workspaceFolder.uri.fsPath; + const sourceFile = path.join(workspacePath, 'exitCode.cpp'); + const sourceUri = vscode.Uri.file(sourceFile); + const executableName = isWindows ? 'exitCodeProgram.exe' : 'exitCodeProgram'; + const executablePath = path.join(workspacePath, executableName); + const sessionName = 'Run Without Debugging Exit Code'; + const debugType = isWindows ? 'cppvsdbg' : 'cppdbg'; + + await compileProgram(workspacePath, sourceFile, executablePath); + + const breakpoint = await createBreakpointAtReturnStatement(sourceUri); + const tracker = createTracker(debugType, sessionName, 30000, 'Timed out waiting for debugger event.'); + const debugSessionTerminated = createSessionTerminatedPromise(sessionName); + + try { + const started = await vscode.debug.startDebugging( + workspaceFolder, + { + name: sessionName, + type: debugType, + request: 'launch', + program: executablePath, + args: [], + cwd: workspacePath, + console: 'internalConsole' + }, + { noDebug: true }); + + assert.strictEqual(started, true, 'The noDebug launch did not start successfully.'); + + const lastEvent = await tracker.lastEvent; + await debugSessionTerminated; + + assert.strictEqual(lastEvent, 'exited', 'No-debug launch should exit rather than stop on a breakpoint.'); + assert.strictEqual(tracker.state.setBreakpointsRequestReceived, false, 'a "no debug" session should not send setBreakpoints requests.'); + assert.strictEqual(tracker.state.stoppedEventReceived, false, 'a "no debug" session should not emit stopped events.'); + assert.strictEqual(tracker.state.actualExitCode, expectedExitCode, 'Unexpected exit code from run without debugging launch.'); + } finally { + tracker.dispose(); + vscode.debug.removeBreakpoints([breakpoint]); + } + }); + + test('Debug launch should bind and stop at the breakpoint', async () => { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0] ?? assert.fail('No workspace folder available'); + const workspacePath = workspaceFolder.uri.fsPath; + const sourceFile = path.join(workspacePath, 'exitCode.cpp'); + const sourceUri = vscode.Uri.file(sourceFile); + const executableName = isWindows ? 'exitCodeProgram.exe' : 'exitCodeProgram'; + const executablePath = path.join(workspacePath, executableName); + const sessionName = 'Debug Launch Breakpoint Stop'; + const debugType = isWindows ? 'cppvsdbg' : 'cppdbg'; + + await compileProgram(workspacePath, sourceFile, executablePath); + + const breakpoint = await createBreakpointAtReturnStatement(sourceUri); + + let launchedSession: vscode.DebugSession | undefined; + const tracker = createTracker(debugType, sessionName, 45000, 'Timed out waiting for debugger event in normal debug mode.'); + + const startedSubscription = vscode.debug.onDidStartDebugSession((session) => { + if (session.name === sessionName) { + launchedSession = session; + } + }); + + const debugSessionTerminated = createSessionTerminatedPromise(sessionName); + + try { + const started = await vscode.debug.startDebugging( + workspaceFolder, + { + name: sessionName, + type: debugType, + request: 'launch', + program: executablePath, + args: [], + cwd: workspacePath, + console: 'internalConsole' + }, + { noDebug: false }); + + assert.strictEqual(started, true, 'The debug launch did not start successfully.'); + + const lastEvent = await tracker.lastEvent; + + assert.strictEqual(lastEvent, 'stopped', 'Debug launch should stop at the breakpoint before exit.'); + assert.strictEqual(tracker.state.setBreakpointsRequestReceived, true, 'Debug mode should send setBreakpoints requests.'); + assert.strictEqual(tracker.state.stoppedEventReceived, true, 'Debug mode should emit a stopped event at the breakpoint.'); + assert.strictEqual(tracker.state.exitedBeforeStop, false, 'Program exited before stopping at breakpoint in debug mode.'); + assert.strictEqual(vscode.debug.activeDebugSession?.name, sessionName, 'Debug session should still be active at breakpoint.'); + + const stoppedSession = launchedSession ?? vscode.debug.activeDebugSession; + assert.ok(stoppedSession, 'Unable to identify the running debug session for termination.'); + await vscode.debug.stopDebugging(stoppedSession); + await debugSessionTerminated; + } finally { + startedSubscription.dispose(); + tracker.dispose(); + vscode.debug.removeBreakpoints([breakpoint]); + } + }); +}); From bb77661a734d0d0a6e6a94b4500b093a2cb9feba Mon Sep 17 00:00:00 2001 From: Bob Brown Date: Fri, 3 Apr 2026 15:53:02 -0700 Subject: [PATCH 5/7] address CodeQL issue --- Extension/src/Debugger/runWithoutDebuggingAdapter.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Extension/src/Debugger/runWithoutDebuggingAdapter.ts b/Extension/src/Debugger/runWithoutDebuggingAdapter.ts index 81dccb3eb..e79f88e92 100644 --- a/Extension/src/Debugger/runWithoutDebuggingAdapter.ts +++ b/Extension/src/Debugger/runWithoutDebuggingAdapter.ts @@ -117,7 +117,7 @@ export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter { if (platform === 'win32') { cp.spawn('cmd.exe', ['/c', 'start', 'cmd.exe', '/K', cmdLine], { cwd, env, detached: true, stdio: 'ignore' }).unref(); } else if (platform === 'darwin') { - cp.spawn('osascript', ['-e', `tell application "Terminal" to do script "${cmdLine.replace(/"/g, '\\"')}"`], { cwd, env, detached: true, stdio: 'ignore' }).unref(); + cp.spawn('osascript', ['-e', `tell application "Terminal" to do script "${this.escapeQuotes(cmdLine)}"`], { cwd, env, detached: true, stdio: 'ignore' }).unref(); } else if (platform === 'linux' && sessionIsWsl()) { cp.spawn('/mnt/c/Windows/System32/cmd.exe', ['/c', 'start', 'bash', '-c', `${cmdLine};read -p 'Press enter to continue...'`], { env, detached: true, stdio: 'ignore' }).unref(); } else { // platform === 'linux' @@ -186,8 +186,12 @@ export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter { }); } + private escapeQuotes(arg: string): string { + return arg.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); + } + private quoteArg(arg: string): string { - return /\s/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg; + return /\s/.test(arg) ? `"${this.escapeQuotes(arg)}"` : arg; } private sendResponse(request: { command: string; seq: number; }, body: object): void { From 2608547f4a2aa857435849879c342a18b73503a4 Mon Sep 17 00:00:00 2001 From: Bob Brown Date: Fri, 3 Apr 2026 16:08:00 -0700 Subject: [PATCH 6/7] set the console type based on debugger type --- .../tests/runWithoutDebugging.integration.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Extension/test/scenarios/RunWithoutDebugging/tests/runWithoutDebugging.integration.test.ts b/Extension/test/scenarios/RunWithoutDebugging/tests/runWithoutDebugging.integration.test.ts index 782a9d6f4..dedd3218b 100644 --- a/Extension/test/scenarios/RunWithoutDebugging/tests/runWithoutDebugging.integration.test.ts +++ b/Extension/test/scenarios/RunWithoutDebugging/tests/runWithoutDebugging.integration.test.ts @@ -183,7 +183,6 @@ function createTracker(debugType: string, sessionName: string, timeoutMs: number } suite('Run Without Debugging Integration Test', function (): void { - suiteSetup(async function (): Promise { const extension: vscode.Extension = vscode.extensions.getExtension('ms-vscode.cpptools') || assert.fail('Extension not found'); if (!extension.isActive) { @@ -224,7 +223,8 @@ suite('Run Without Debugging Integration Test', function (): void { program: executablePath, args: [], cwd: workspacePath, - console: 'internalConsole' + externalConsole: debugType === 'cppdbg' ? false : undefined, + console: debugType === 'cppvsdbg' ? 'internalConsole' : undefined }, { noDebug: true }); @@ -278,7 +278,8 @@ suite('Run Without Debugging Integration Test', function (): void { program: executablePath, args: [], cwd: workspacePath, - console: 'internalConsole' + externalConsole: debugType === 'cppdbg' ? false : undefined, + console: debugType === 'cppvsdbg' ? 'internalConsole' : undefined }, { noDebug: false }); From 9f9dd619fcabe0eb5ea79b225ca4c072c4715c7e Mon Sep 17 00:00:00 2001 From: Bob Brown Date: Fri, 3 Apr 2026 16:31:06 -0700 Subject: [PATCH 7/7] use shell integration on terminals that support it --- .../Debugger/runWithoutDebuggingAdapter.ts | 94 +++++++++++++++++-- 1 file changed, 88 insertions(+), 6 deletions(-) diff --git a/Extension/src/Debugger/runWithoutDebuggingAdapter.ts b/Extension/src/Debugger/runWithoutDebuggingAdapter.ts index e79f88e92..8e59e00bd 100644 --- a/Extension/src/Debugger/runWithoutDebuggingAdapter.ts +++ b/Extension/src/Debugger/runWithoutDebuggingAdapter.ts @@ -20,10 +20,13 @@ const localize = nls.loadMessageBundle(); export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter { private readonly sendMessageEmitter = new vscode.EventEmitter(); public readonly onDidSendMessage: vscode.Event = this.sendMessageEmitter.event; + private readonly terminalListeners: vscode.Disposable[] = []; private seq: number = 1; private childProcess?: cp.ChildProcess; private terminal?: vscode.Terminal; + private terminalExecution?: vscode.TerminalShellExecution; + private hasTerminated: boolean = false; public handleMessage(message: vscode.DebugProtocolMessage): void { const msg = message as { type: string; command: string; seq: number; arguments?: any; }; @@ -79,7 +82,7 @@ export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter { this.sendResponse(request, {}); if (consoleMode === 'integratedTerminal') { - this.launchIntegratedTerminal(program, args, cwd, env); + await this.launchIntegratedTerminal(program, args, cwd, env); } else if (consoleMode === 'externalTerminal') { this.launchExternalTerminal(program, args, cwd, env); } else { @@ -91,8 +94,7 @@ export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter { * Launch the program in a VS Code integrated terminal. * The terminal will remain open after the program exits and be reused for the next session, if applicable. */ - private launchIntegratedTerminal(program: string, args: string[], cwd: string | undefined, env: NodeJS.ProcessEnv) { - const shellArgs: string[] = [program, ...args].map(a => this.quoteArg(a)); + private async launchIntegratedTerminal(program: string, args: string[], cwd: string | undefined, env: NodeJS.ProcessEnv): Promise { const terminalName = path.normalize(program); const existingTerminal = vscode.window.terminals.find(t => t.name === terminalName); this.terminal = existingTerminal ?? vscode.window.createTerminal({ @@ -101,10 +103,21 @@ export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter { env: env as Record }); this.terminal.show(true); - this.terminal.sendText(shellArgs.join(' ')); - // The terminal manages its own lifecycle; notify VS Code the "debug" session is done. - this.sendEvent('terminated'); + const shellIntegration: vscode.TerminalShellIntegration | undefined = + this.terminal.shellIntegration ?? await this.waitForShellIntegration(this.terminal, 3000); + + // Not all terminals support shell integration. If it's not available, we'll just send the command as text though we won't be able to monitor its execution. + if (shellIntegration) { + this.monitorIntegratedTerminal(this.terminal); + this.terminalExecution = shellIntegration.executeCommand(program, args); + } else { + const shellArgs: string[] = [program, ...args].map(a => this.quoteArg(a)); + this.terminal.sendText(shellArgs.join(' ')); + + // The terminal manages its own lifecycle; notify VS Code the "debug" session is done. + this.sendEvent('terminated'); + } } /** @@ -194,6 +207,65 @@ export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter { return /\s/.test(arg) ? `"${this.escapeQuotes(arg)}"` : arg; } + private waitForShellIntegration(terminal: vscode.Terminal, timeoutMs: number): Promise { + return new Promise(resolve => { + let resolved: boolean = false; + const done = (shellIntegration: vscode.TerminalShellIntegration | undefined): void => { + if (resolved) { + return; + } + + resolved = true; + clearTimeout(timeout); + shellIntegrationChanged.dispose(); + terminalClosed.dispose(); + resolve(shellIntegration); + }; + + const timeout = setTimeout(() => done(undefined), timeoutMs); + const shellIntegrationChanged = vscode.window.onDidChangeTerminalShellIntegration(event => { + if (event.terminal === terminal) { + done(event.shellIntegration); + } + }); + const terminalClosed = vscode.window.onDidCloseTerminal(closedTerminal => { + if (closedTerminal === terminal) { + done(undefined); + } + }); + }); + } + + private monitorIntegratedTerminal(terminal: vscode.Terminal): void { + this.disposeTerminalListeners(); + this.terminalListeners.push( + vscode.window.onDidEndTerminalShellExecution(event => { + if (event.terminal !== terminal || event.execution !== this.terminalExecution || this.hasTerminated) { + return; + } + + if (event.exitCode !== undefined) { + this.sendEvent('exited', { exitCode: event.exitCode }); + } + + this.sendEvent('terminated'); + }), + vscode.window.onDidCloseTerminal(closedTerminal => { + if (closedTerminal !== terminal || this.hasTerminated) { + return; + } + + this.sendEvent('terminated'); + }) + ); + } + + private disposeTerminalListeners(): void { + while (this.terminalListeners.length > 0) { + this.terminalListeners.pop()?.dispose(); + } + } + private sendResponse(request: { command: string; seq: number; }, body: object): void { this.sendMessageEmitter.fire({ type: 'response', @@ -206,6 +278,15 @@ export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter { } private sendEvent(event: string, body?: object): void { + if (event === 'terminated') { + if (this.hasTerminated) { + return; + } + + this.hasTerminated = true; + this.disposeTerminalListeners(); + } + this.sendMessageEmitter.fire({ type: 'event', seq: this.seq++, @@ -216,6 +297,7 @@ export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter { public dispose(): void { this.terminateProcess(); + this.disposeTerminalListeners(); this.sendMessageEmitter.dispose(); }