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/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/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/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..8e59e00bd --- /dev/null +++ b/Extension/src/Debugger/runWithoutDebuggingAdapter.ts @@ -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(); + 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; }; + 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; + 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 { + 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); + + 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(); + } 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 { + 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; + } +} 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; + } +} 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..dedd3218b --- /dev/null +++ b/Extension/test/scenarios/RunWithoutDebugging/tests/runWithoutDebugging.integration.test.ts @@ -0,0 +1,306 @@ +/* -------------------------------------------------------------------------------------------- + * 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, + externalConsole: debugType === 'cppdbg' ? false : undefined, + console: debugType === 'cppvsdbg' ? 'internalConsole' : undefined + }, + { 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, + externalConsole: debugType === 'cppdbg' ? false : undefined, + console: debugType === 'cppvsdbg' ? 'internalConsole' : undefined + }, + { 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]); + } + }); +});