Skip to content
Open
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
4 changes: 4 additions & 0 deletions Extension/.vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments ends with a period but doesn't start with a capital letter (versus the case on the next line).

// 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"` }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this changing?

`;
}

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
308 changes: 308 additions & 0 deletions Extension/src/Debugger/runWithoutDebuggingAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
/* --------------------------------------------------------------------------------------------
* 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 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; };
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') {
await 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 async launchIntegratedTerminal(program: string, args: string[], cwd: string | undefined, env: NodeJS.ProcessEnv): Promise<void> {
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);

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');
}
}

/**
* 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 "${this.escapeQuotes(cmdLine)}"`], { cwd, env, detached: true, stdio: 'ignore' }).unref();
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be tested.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bobbrow I've tested this code path on mac (it worked).

Copy link
Copy Markdown
Member Author

@bobbrow bobbrow Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sean-mcmanus cool! Does it work for paths with spaces and args with spaces too?

Copy link
Copy Markdown
Contributor

@sean-mcmanus sean-mcmanus Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bobbrow Yeah, it works with folders/files with spaces in the path, but what do you mean by "args with spaces"? i.e. I see the args be an array of strings. Do you mean like [ "one two", "three four" ]?

} 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[] }[] = [
{ 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 escapeQuotes(arg: string): string {
return arg.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
}

private quoteArg(arg: string): string {
return /\s/.test(arg) ? `"${this.escapeQuotes(arg)}"` : arg;
}

private waitForShellIntegration(terminal: vscode.Terminal, timeoutMs: number): Promise<vscode.TerminalShellIntegration | undefined> {
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',
seq: this.seq++,
request_seq: request.seq,
success: true,
command: request.command,
body
} as vscode.DebugProtocolMessage);
}

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++,
event,
body
} as vscode.DebugProtocolMessage);
}

public dispose(): void {
this.terminateProcess();
this.disposeTerminalListeners();
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
Loading