Skip to content

Commit 7250861

Browse files
committed
Adding Run Without Debugging feature
1 parent 61815d8 commit 7250861

3 files changed

Lines changed: 191 additions & 3 deletions

File tree

Extension/src/Debugger/configurationProvider.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,12 +147,14 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
147147
Telemetry.logDebuggerEvent(DebuggerEvent.debugPanel, { "debugType": DebugType.debug, "configSource": folder ? ConfigSource.workspaceFolder : ConfigSource.singleFile, "configMode": ConfigMode.noLaunchConfig, "cancelled": "true", "succeeded": "true" });
148148
return undefined; // aborts debugging silently
149149
} else {
150+
const noDebug = config.noDebug ?? false; // preserve the noDebug value from the config if it exists.
150151
// Currently, we expect only one debug config to be selected.
151152
console.assert(configs.length === 1, "More than one debug config is selected.");
152153
config = configs[0];
153154
// Keep track of the entry point where the debug config has been selected, for telemetry purposes.
154155
config.debuggerEvent = DebuggerEvent.debugPanel;
155156
config.configSource = folder ? ConfigSource.workspaceFolder : ConfigSource.singleFile;
157+
config.noDebug = noDebug;
156158
}
157159
}
158160

Extension/src/Debugger/debugAdapterDescriptorFactory.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ import * as os from 'os';
77
import * as path from 'path';
88
import * as vscode from "vscode";
99
import * as nls from 'vscode-nls';
10+
import { RunWithoutDebuggingAdapter } from './runWithoutDebuggingAdapter';
1011

1112
nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
1213
const localize: nls.LocalizeFunc = nls.loadMessageBundle();
1314

14-
// Registers DebugAdapterDescriptorFactory for `cppdbg` and `cppvsdbg`. If it is not ready, it will prompt a wait for the download dialog.
15+
// Registers DebugAdapterDescriptorFactory for `cppdbg` and `cppvsdbg`.
1516
// NOTE: This file is not automatically tested.
1617

1718
abstract class AbstractDebugAdapterDescriptorFactory implements vscode.DebugAdapterDescriptorFactory {
@@ -27,7 +28,11 @@ abstract class AbstractDebugAdapterDescriptorFactory implements vscode.DebugAdap
2728

2829
export class CppdbgDebugAdapterDescriptorFactory extends AbstractDebugAdapterDescriptorFactory {
2930

30-
async createDebugAdapterDescriptor(_session: vscode.DebugSession, _executable?: vscode.DebugAdapterExecutable): Promise<vscode.DebugAdapterDescriptor> {
31+
async createDebugAdapterDescriptor(session: vscode.DebugSession, _executable?: vscode.DebugAdapterExecutable): Promise<vscode.DebugAdapterDescriptor> {
32+
if (session.configuration.noDebug) {
33+
return new vscode.DebugAdapterInlineImplementation(new RunWithoutDebuggingAdapter());
34+
}
35+
3136
const adapter: string = "./debugAdapters/bin/OpenDebugAD7" + (os.platform() === 'win32' ? ".exe" : "");
3237

3338
const command: string = path.join(this.context.extensionPath, adapter);
@@ -38,7 +43,11 @@ export class CppdbgDebugAdapterDescriptorFactory extends AbstractDebugAdapterDes
3843

3944
export class CppvsdbgDebugAdapterDescriptorFactory extends AbstractDebugAdapterDescriptorFactory {
4045

41-
async createDebugAdapterDescriptor(_session: vscode.DebugSession, _executable?: vscode.DebugAdapterExecutable): Promise<vscode.DebugAdapterDescriptor | null> {
46+
async createDebugAdapterDescriptor(session: vscode.DebugSession, _executable?: vscode.DebugAdapterExecutable): Promise<vscode.DebugAdapterDescriptor | null> {
47+
if (session.configuration.noDebug) {
48+
return new vscode.DebugAdapterInlineImplementation(new RunWithoutDebuggingAdapter());
49+
}
50+
4251
if (os.platform() !== 'win32') {
4352
void vscode.window.showErrorMessage(localize("debugger.not.available", "Debugger type '{0}' is not available for non-Windows machines.", "cppvsdbg"));
4453
return null;
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/* --------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All Rights Reserved.
3+
* See 'LICENSE' in the project root for license information.
4+
* ------------------------------------------------------------------------------------------ */
5+
6+
import * as cp from 'child_process';
7+
import * as os from 'os';
8+
import * as path from 'path';
9+
import * as vscode from 'vscode';
10+
11+
/**
12+
* A minimal inline Debug Adapter that runs the target program directly without a debug adapter
13+
* when the user invokes "Run Without Debugging".
14+
*/
15+
export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter {
16+
private readonly sendMessageEmitter = new vscode.EventEmitter<vscode.DebugProtocolMessage>();
17+
public readonly onDidSendMessage: vscode.Event<vscode.DebugProtocolMessage> = this.sendMessageEmitter.event;
18+
19+
private seq: number = 1;
20+
private childProcess?: cp.ChildProcess;
21+
private terminal?: vscode.Terminal;
22+
23+
public handleMessage(message: vscode.DebugProtocolMessage): void {
24+
const msg = message as { type: string; command: string; seq: number; arguments?: any; };
25+
if (msg.type === 'request') {
26+
void this.handleRequest(msg);
27+
}
28+
}
29+
30+
private async handleRequest(request: { command: string; seq: number; arguments?: any; }): Promise<void> {
31+
switch (request.command) {
32+
case 'initialize':
33+
this.sendResponse(request, {});
34+
this.sendEvent('initialized');
35+
break;
36+
case 'launch':
37+
await this.launch(request);
38+
break;
39+
case 'configurationDone':
40+
this.sendResponse(request, {});
41+
break;
42+
case 'disconnect':
43+
case 'terminate':
44+
this.sendResponse(request, {});
45+
break;
46+
default:
47+
this.sendResponse(request, {});
48+
break;
49+
}
50+
}
51+
52+
private async launch(request: { command: string; seq: number; arguments?: any; }): Promise<void> {
53+
const config = request.arguments as {
54+
program?: string;
55+
args?: string[];
56+
cwd?: string;
57+
environment?: { name: string; value: string; }[];
58+
console?: string;
59+
};
60+
61+
const program: string = config.program ?? '';
62+
const args: string[] = config.args ?? [];
63+
const cwd: string | undefined = config.cwd;
64+
const environment: { name: string; value: string; }[] = config.environment ?? [];
65+
const consoleMode: string = config.console ?? 'integratedTerminal';
66+
67+
// Merge the launch config's environment variables on top of the inherited process environment.
68+
const env: NodeJS.ProcessEnv = { ...process.env };
69+
for (const e of environment) {
70+
env[e.name] = e.value;
71+
}
72+
73+
this.sendResponse(request, {});
74+
75+
if (consoleMode === 'integratedTerminal') {
76+
this.launchIntegratedTerminal(program, args, cwd, env);
77+
} else if (consoleMode === 'externalTerminal') {
78+
this.launchExternalTerminal(program, args, cwd, env);
79+
} else {
80+
this.launchInternalConsole(program, args, cwd, env);
81+
}
82+
}
83+
84+
/**
85+
* Launch the program in a VS Code integrated terminal.
86+
* The terminal will remain open after the program exits and be reused for the next session, if applicable.
87+
*/
88+
private launchIntegratedTerminal(program: string, args: string[], cwd: string | undefined, env: NodeJS.ProcessEnv) {
89+
const shellArgs: string[] = [program, ...args].map(a => this.quoteArg(a));
90+
const terminalName = path.normalize(program);
91+
const existingTerminal = vscode.window.terminals.find(t => t.name === terminalName);
92+
this.terminal = existingTerminal ?? vscode.window.createTerminal({
93+
name: terminalName,
94+
cwd,
95+
env: env as Record<string, string>
96+
});
97+
this.terminal.show(true);
98+
this.terminal.sendText(shellArgs.join(' '));
99+
100+
// The terminal manages its own lifecycle; notify VS Code the "debug" session is done.
101+
this.sendEvent('terminated');
102+
}
103+
104+
/**
105+
* Launch the program in an external terminal. We do not keep track of this terminal or the spawned process.
106+
*/
107+
private launchExternalTerminal(program: string, args: string[], cwd: string | undefined, env: NodeJS.ProcessEnv): void {
108+
const quotedArgs: string[] = [program, ...args].map(a => this.quoteArg(a));
109+
const cmdLine: string = quotedArgs.join(' ');
110+
const platform: string = os.platform();
111+
if (platform === 'win32') {
112+
cp.spawn('cmd.exe', ['/c', 'start', 'cmd.exe', '/K', cmdLine], { cwd, env, detached: true, stdio: 'ignore' }).unref();
113+
} else if (platform === 'darwin') {
114+
cp.spawn('osascript', ['-e', `tell application "Terminal" to do script "${cmdLine.replace(/"/g, '\\"')}"`], { cwd, env, detached: true, stdio: 'ignore' }).unref();
115+
} else {
116+
cp.spawn('x-terminal-emulator', ['-e', cmdLine], { cwd, env, detached: true, stdio: 'ignore' }).unref();
117+
}
118+
this.sendEvent('terminated');
119+
}
120+
121+
/**
122+
* Spawn the process and forward stdout/stderr as DAP output events.
123+
*/
124+
private launchInternalConsole(program: string, args: string[], cwd: string | undefined, env: NodeJS.ProcessEnv) {
125+
this.childProcess = cp.spawn(program, args, { cwd, env });
126+
127+
this.childProcess.stdout?.on('data', (data: Buffer) => {
128+
this.sendEvent('output', { category: 'stdout', output: data.toString() });
129+
});
130+
this.childProcess.stderr?.on('data', (data: Buffer) => {
131+
this.sendEvent('output', { category: 'stderr', output: data.toString() });
132+
});
133+
this.childProcess.on('error', (err: Error) => {
134+
this.sendEvent('output', { category: 'stderr', output: `${err.message}\n` });
135+
this.sendEvent('exited', { exitCode: 1 });
136+
this.sendEvent('terminated');
137+
});
138+
this.childProcess.on('exit', (code: number | null) => {
139+
this.sendEvent('exited', { exitCode: code ?? 0 });
140+
this.sendEvent('terminated');
141+
});
142+
}
143+
144+
private quoteArg(arg: string): string {
145+
return /\s/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
146+
}
147+
148+
private sendResponse(request: { command: string; seq: number; }, body: object): void {
149+
this.sendMessageEmitter.fire({
150+
type: 'response',
151+
seq: this.seq++,
152+
request_seq: request.seq,
153+
success: true,
154+
command: request.command,
155+
body
156+
} as vscode.DebugProtocolMessage);
157+
}
158+
159+
private sendEvent(event: string, body?: object): void {
160+
this.sendMessageEmitter.fire({
161+
type: 'event',
162+
seq: this.seq++,
163+
event,
164+
body
165+
} as vscode.DebugProtocolMessage);
166+
}
167+
168+
public dispose(): void {
169+
this.terminateProcess();
170+
this.sendMessageEmitter.dispose();
171+
}
172+
173+
private terminateProcess(): void {
174+
this.childProcess?.kill();
175+
this.childProcess = undefined;
176+
}
177+
}

0 commit comments

Comments
 (0)