Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7250861
Adding Run Without Debugging feature
bobbrow Apr 3, 2026
448763e
Updates for WSL
bobbrow Apr 3, 2026
c315d20
Add support for linux terminals
bobbrow Apr 3, 2026
a86afd6
Add tests
bobbrow Apr 3, 2026
bb77661
address CodeQL issue
bobbrow Apr 3, 2026
2608547
set the console type based on debugger type
bobbrow Apr 3, 2026
9f9dd61
use shell integration on terminals that support it
bobbrow Apr 3, 2026
d59f6c0
Merge branch 'main' into bobbrow/runWithoutDebugging
bobbrow Apr 4, 2026
de0e20c
PR comments and placeholder for automation for later
bobbrow Apr 6, 2026
6008c13
warn the user if they try to do run without debugging in unsupported …
bobbrow Apr 6, 2026
da02611
Use buildShellCommandLine
bobbrow Apr 6, 2026
68dddd8
reinterpret internalConsole to be integratedTerminal
bobbrow Apr 7, 2026
da18921
set the MIMode explicitly
bobbrow Apr 7, 2026
ad01d4a
Fix the test to work with integratedTerminal mode
bobbrow Apr 7, 2026
a69ba2a
Change the test to read from file instead of exit code
bobbrow Apr 9, 2026
b94df3e
Merge branch 'main' into bobbrow/runWithoutDebugging
bobbrow Apr 9, 2026
44dda31
handle programs with spaces in the name for shell integration
bobbrow Apr 9, 2026
161627d
Addres feedback and test more launch combinations
bobbrow Apr 10, 2026
826e1d2
PowerShell is the default shell on Windows
bobbrow Apr 13, 2026
bd6eb8b
Merge branch 'main' into bobbrow/runWithoutDebugging
bobbrow Apr 13, 2026
cf48779
Merge branch 'main' into bobbrow/runWithoutDebugging
bobbrow Apr 13, 2026
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
2 changes: 2 additions & 0 deletions Extension/src/Debugger/configurationProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment thread
bobbrow marked this conversation as resolved.
Outdated
// 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;
}
}

Expand Down
2 changes: 1 addition & 1 deletion Extension/src/Debugger/configurations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"` }
Comment thread
bobbrow marked this conversation as resolved.
Outdated
`;
}

Expand Down
15 changes: 12 additions & 3 deletions Extension/src/Debugger/debugAdapterDescriptorFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -27,7 +28,11 @@ abstract class AbstractDebugAdapterDescriptorFactory implements vscode.DebugAdap

export class CppdbgDebugAdapterDescriptorFactory extends AbstractDebugAdapterDescriptorFactory {

async createDebugAdapterDescriptor(_session: vscode.DebugSession, _executable?: vscode.DebugAdapterExecutable): Promise<vscode.DebugAdapterDescriptor> {
async createDebugAdapterDescriptor(session: vscode.DebugSession, _executable?: vscode.DebugAdapterExecutable): Promise<vscode.DebugAdapterDescriptor> {
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);
Expand All @@ -38,7 +43,11 @@ export class CppdbgDebugAdapterDescriptorFactory extends AbstractDebugAdapterDes

export class CppvsdbgDebugAdapterDescriptorFactory extends AbstractDebugAdapterDescriptorFactory {

async createDebugAdapterDescriptor(_session: vscode.DebugSession, _executable?: vscode.DebugAdapterExecutable): Promise<vscode.DebugAdapterDescriptor | null> {
async createDebugAdapterDescriptor(session: vscode.DebugSession, _executable?: vscode.DebugAdapterExecutable): Promise<vscode.DebugAdapterDescriptor | null> {
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;
Expand Down
222 changes: 222 additions & 0 deletions Extension/src/Debugger/runWithoutDebuggingAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
/* --------------------------------------------------------------------------------------------
* 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';
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".
*/
export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter {
private readonly sendMessageEmitter = new vscode.EventEmitter<vscode.DebugProtocolMessage>();
public readonly onDidSendMessage: vscode.Event<vscode.DebugProtocolMessage> = 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<void> {
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<void> {
const config = request.arguments as {
program?: string;
args?: string[];
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 ?? (config.externalConsole ? 'externalTerminal' : '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<string, string>
});
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();

Check failure

Code scanning / CodeQL

Incomplete string escaping or encoding High

This does not escape backslash characters in the input.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
} 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'
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[] }[] = [

Check failure on line 137 in Extension/src/Debugger/runWithoutDebuggingAdapter.ts

View workflow job for this annotation

GitHub Actions / job / build

Function property signature is forbidden. Use a method shorthand instead

Check failure on line 137 in Extension/src/Debugger/runWithoutDebuggingAdapter.ts

View workflow job for this annotation

GitHub Actions / job / build

Function property signature is forbidden. Use a method shorthand instead

Check failure on line 137 in Extension/src/Debugger/runWithoutDebuggingAdapter.ts

View workflow job for this annotation

GitHub Actions / job / build

Function property signature is forbidden. Use a method shorthand instead
{ 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.
*/
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;

Check failure

Code scanning / CodeQL

Incomplete string escaping or encoding High

This does not escape backslash characters in the input.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
}

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;
}
}
11 changes: 11 additions & 0 deletions Extension/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Loading