Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
aebf397
Fix #12537: add env object schema to cppdbg and cppvsdbg launch configs
02prashantrai Aug 18, 2026
deb60ac
Fix #12537: add env object schema to cppdbg and cppvsdbg launch configs
8prashant Aug 18, 2026
13f598c
Add support for env object in cppdbg and runWithoutDebugging configur…
02prashantrai Aug 19, 2026
66f6d95
Add support for env object in cppdbg and runWithoutDebugging configur…
8prashant Aug 19, 2026
793ac13
Merge branch 'main' into fix/12537-env-property-schema
8prashant Aug 19, 2026
146e959
Merge branch 'main' into fix/12537-env-property-schema
8prashant Aug 19, 2026
7130385
Update env description to include message and comment structure; enha…
8prashant Sep 1, 2026
84a0467
Update env description to include message and comment structure; enha…
8prashant Sep 1, 2026
e2b17cf
Merge branch 'fix/12537-env-property-schema' of https://github.com/8p…
8prashant Sep 1, 2026
737ecd1
Merge branch 'fix/12537-env-property-schema' of https://github.com/8p…
8prashant Sep 1, 2026
d961d42
Merge branch 'fix/12537-env-property-schema' of https://github.com/8p…
8prashant Sep 1, 2026
034ed40
Merge branch 'main' into fix/12537-env-property-schema
8prashant Sep 2, 2026
30f2f9d
Refactor environment variable handling in DebugConfigurationProvider …
8prashant Sep 2, 2026
c933aee
Merge branch 'fix/12537-env-property-schema' of https://github.com/8p…
8prashant Sep 2, 2026
af30f6c
Merge branch 'main' into fix/12537-env-property-schema
8prashant Sep 4, 2026
875e702
Update environment variable handling to allow null values in DebugCon…
8prashant Sep 4, 2026
51fe13f
Fix environment variable deletion for case insensitivity
8prashant Sep 4, 2026
ce9cbd9
Enhance environment variable handling to support null values in schem…
8prashant Sep 4, 2026
dc91405
Update environment variable schema to allow null values for variable …
8prashant Sep 4, 2026
d15c9a8
Add terminal close handling to manage terminal lifecycle in RunWithou…
8prashant Sep 5, 2026
636a76a
Refactor environment variable handling to remove null type support in…
8prashant Sep 5, 2026
bc14d84
Manage terminal lifecycle by tracking active terminals in RunWithoutD…
8prashant Sep 5, 2026
917c870
Fix terminal management by ensuring closed terminals are removed from…
8prashant Sep 6, 2026
5e65c07
Refactor environment handling in DebugConfigurationProvider to use En…
8prashant Sep 6, 2026
d22eea6
Preserve case-distinct environment variable names for remote cppdbg t…
8prashant Sep 6, 2026
b0c173e
Enhance external terminal launch on macOS by incorporating environmen…
8prashant Sep 6, 2026
7880c4f
Fix terminal management by ensuring correct deletion of managed termi…
8prashant Sep 6, 2026
f4ca8e8
Add test for RunWithoutDebuggingAdapter to validate macOS external te…
8prashant Sep 6, 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
16 changes: 16 additions & 0 deletions Extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4379,6 +4379,14 @@
},
"default": []
},
"env": {
"type": "object",
"description": "%c_cpp.debuggers.env.description%",
Comment thread
8prashant marked this conversation as resolved.
"additionalProperties": {
"type": "string"
},
"default": {}
},
"envFile": {
"type": "string",
"description": "%c_cpp.debuggers.envFile.description%",
Expand Down Expand Up @@ -6052,6 +6060,14 @@
},
"default": []
},
"env": {
"type": "object",
"description": "%c_cpp.debuggers.env.description%",
"additionalProperties": {
"type": "string"
},
"default": {}
},
"envFile": {
"type": "string",
"description": "%c_cpp.debuggers.envFile.description%",
Expand Down
6 changes: 6 additions & 0 deletions Extension/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -927,6 +927,12 @@
"{Locked=\"[ { \\\"name\\\": \\\"\"} {Locked=\"\\\", \\\"value\\\": \\\"\"} {Locked=\"\\\" } ]\"} {Locked=\"[ { \\\"\"} {Locked=\"\\\": \\\"\"} {Locked=\"\\\" } ]\"}"
]
},
"c_cpp.debuggers.env.description": {
"message": "Object of environment variables to add to the environment for the program. Example: { \"MY_VAR\": \"value\" }. Use `environment` for the array-of-objects form.",
"comment": [
"{Locked=\"{ \\\"MY_VAR\\\": \\\"value\\\" }\"} {Locked=\"`environment`\"}"
]
},
"c_cpp.debuggers.envFile.description": "Absolute path to a file containing environment variable definitions. This file has key value pairs separated by an equals sign per line. E.g. KEY=VALUE.",
"c_cpp.debuggers.additionalSOLibSearchPath.description": "Semicolon separated list of directories to use to search for .so files. Example: \"c:\\dir1;c:\\dir2\".",
"c_cpp.debuggers.MIMode.description": "Indicates the console debugger that the MIDebugEngine will connect to. Allowed values are \"gdb\" \"lldb\".",
Expand Down
2 changes: 1 addition & 1 deletion Extension/src/Debugger/ParsedEnvironmentFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const localize: nls.LocalizeFunc = nls.loadMessageBundle();

export interface Environment {
name: string;
value: string;
value: string | null;
}

export class ParsedEnvironmentFile {
Expand Down
35 changes: 35 additions & 0 deletions Extension/src/Debugger/configurationProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,10 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
// Add environment variables from .env file
this.resolveEnvFile(config, folder);

// Debug adapters consume the legacy `environment` array, not `env`.
// Convert here so both syntaxes work while preserving `env` precedence.
this.resolveEnvObject(config);

await this.expand(config, folder);

this.resolveSourceFileMapVariables(config);
Expand Down Expand Up @@ -700,6 +704,37 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv
}
}

private resolveEnvObject(config: CppDebugConfiguration): void {
if ((config.type !== DebuggerType.cppdbg && config.type !== DebuggerType.cppvsdbg) || config.request !== 'launch') {
return;
}

const envObject = config.env;
if (!util.isObject(envObject)) {
return;
}

const environment: Environment[] = util.isArray(config.environment) ? config.environment : [];
const mergedEnvironment = new Map<string, Environment>();
const isCaseInsensitiveTarget = config.type === DebuggerType.cppvsdbg || (isWindows && !config.pipeTransport && !config.miDebuggerServerAddress && !config.useExtendedRemote);
const getEnvironmentKey = (name: string): string => isCaseInsensitiveTarget ? name.toLowerCase() : name;

for (const entry of environment) {
if (util.isString(entry?.name) && util.isString(entry?.value)) {
mergedEnvironment.set(getEnvironmentKey(entry.name), { name: entry.name, value: entry.value });
}
}

for (const [name, value] of Object.entries(envObject)) {
if (util.isString(value)) {
mergedEnvironment.set(getEnvironmentKey(name), { name, value });
}
Comment thread
8prashant marked this conversation as resolved.
}

config.environment = Array.from(mergedEnvironment.values());
delete config.env;
}

private resolveSourceFileMapVariables(config: CppDebugConfiguration): void {
const messages: string[] = [];
if (config.sourceFileMap) {
Expand Down
137 changes: 119 additions & 18 deletions Extension/src/Debugger/runWithoutDebuggingAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,32 @@ import { isWindows } from '../constants';

nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
const localize = nls.loadMessageBundle();
type TerminalEnvironment = NonNullable<vscode.TerminalOptions['env']>;
const managedTerminals = new Map<string, vscode.Terminal>();
Comment thread
8prashant marked this conversation as resolved.
const terminalEnvironments = new WeakMap<vscode.Terminal, TerminalEnvironment>();
const activeTerminals = new WeakSet<vscode.Terminal>();

vscode.window.onDidCloseTerminal(closedTerminal => {
activeTerminals.delete(closedTerminal);
for (const [terminalName, terminal] of managedTerminals) {
if (terminal === closedTerminal) {
managedTerminals.delete(terminalName);
return;
}
}
});

type LaunchEnvironmentEntry = { name: string; value: string | null; };

type LaunchConfiguration = {
program?: string;
args?: string[];
cwd?: string;
environment?: LaunchEnvironmentEntry[];
env?: Record<string, string | null>;
console?: string;
externalConsole?: boolean;
};

/**
* A minimal inline Debug Adapter that runs the target program directly without a debug adapter
Expand All @@ -27,6 +53,7 @@ export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter {
private childProcess?: cp.ChildProcess;
private terminal?: vscode.Terminal;
private terminalExecution?: vscode.TerminalShellExecution;
private releaseTerminalOnTerminate: boolean = false;
private hasTerminated: boolean = false;

public handleMessage(message: vscode.DebugProtocolMessage): void {
Expand Down Expand Up @@ -59,55 +86,69 @@ export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter {
}

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 config = request.arguments as LaunchConfiguration;

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 environment: LaunchEnvironmentEntry[] = config.environment ?? [];
const envObject: Record<string, string | null> = config.env ?? {};
const consoleMode: string = config.console ?? (config.externalConsole ? 'externalTerminal' : 'integratedTerminal');

// Merge the launch config's environment variables on top of the inherited process environment.
// Merge environment values in this order: inherited process environment, legacy
// `environment` entries, then shorthand `env` values (higher precedence).
const env: NodeJS.ProcessEnv = { ...process.env };
const terminalEnv: TerminalEnvironment = {};
for (const e of environment) {
env[e.name] = e.value;
this.applyEnvironmentValue(env, terminalEnv, e.name, e.value);
}
for (const [key, value] of Object.entries(envObject)) {
this.applyEnvironmentValue(env, terminalEnv, key, value);
}

this.sendResponse(request, {});

if (consoleMode === 'integratedTerminal' || consoleMode === 'internalConsole') {
await this.launchIntegratedTerminal(program, args, cwd, env);
await this.launchIntegratedTerminal(program, args, cwd, terminalEnv);
} else if (consoleMode === 'externalTerminal') {
this.launchExternalTerminal(program, args, cwd, env);
this.launchExternalTerminal(program, args, cwd, env, terminalEnv);
}
}

/**
* 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> {
private async launchIntegratedTerminal(program: string, args: string[], cwd: string | undefined, env: TerminalEnvironment): Promise<void> {
const terminalName = path.normalize(program);
const existingTerminal = vscode.window.terminals.find(t => t.name === terminalName);
const managedTerminal = managedTerminals.get(terminalName);
let existingTerminal = managedTerminal && vscode.window.terminals.includes(managedTerminal) ? managedTerminal : undefined;
if (!existingTerminal) {
managedTerminals.delete(terminalName);
}
if (existingTerminal && activeTerminals.has(existingTerminal)) {
existingTerminal = undefined;
} else if (existingTerminal && !this.environmentsEqual(terminalEnvironments.get(existingTerminal), env)) {
existingTerminal.dispose();
Comment thread
8prashant marked this conversation as resolved.
Comment thread
8prashant marked this conversation as resolved.
existingTerminal = undefined;
managedTerminals.delete(terminalName);
}
this.terminal = existingTerminal ?? vscode.window.createTerminal({
name: terminalName,
cwd,
env: env as Record<string, string>
env
});
managedTerminals.set(terminalName, this.terminal);
terminalEnvironments.set(this.terminal, env);
activeTerminals.add(this.terminal);
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.releaseTerminalOnTerminate = true;
this.monitorIntegratedTerminal(this.terminal);
let executable: string = program;
let executableArgs: string[] = args;
Expand All @@ -126,6 +167,9 @@ export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter {
// buildShellCommandLine quotes the path, and PowerShell evaluates a quoted path as a string
// literal instead of running it, so the call operator is required to invoke it.
this.terminal.sendText(this.isPowerShellTerminal() ? `& ${cmdLine}` : cmdLine);
if (managedTerminals.get(terminalName) === this.terminal) {
managedTerminals.delete(terminalName);
}

// The terminal manages its own lifecycle; notify VS Code the "debug" session is done.
this.sendEvent('terminated');
Expand Down Expand Up @@ -155,13 +199,14 @@ export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter {
/**
* 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 {
private launchExternalTerminal(program: string, args: string[], cwd: string | undefined, env: NodeJS.ProcessEnv, terminalEnv: TerminalEnvironment): void {
const cmdLine: string = buildShellCommandLine('', program, args, true);
const platform: string = os.platform();
if (platform === 'win32') {
cp.spawn('cmd.exe', ['/c', 'start', 'cmd.exe', '/K', `"${cmdLine}"`], { cwd, env, windowsVerbatimArguments: true, 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();
const terminalCommand = this.buildMacOSExternalTerminalCommand(cmdLine, terminalEnv);
cp.spawn('osascript', ['-e', `tell application "Terminal" to do script "${this.escapeQuotes(terminalCommand)}"`], { 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'
Expand Down Expand Up @@ -208,10 +253,60 @@ export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter {
vscode.window.showErrorMessage(message);
}

private buildMacOSExternalTerminalCommand(cmdLine: string, env: TerminalEnvironment): string {
const envArgs = Object.entries(env).flatMap(([name, value]) => {
if (value === null) {
return ['-u', this.escapeShellArg(name)];
}

return value === undefined ? [] : [this.escapeShellArg(`${name}=${value}`)];
});

return envArgs.length === 0 ? cmdLine : `/usr/bin/env ${envArgs.join(' ')} ${cmdLine}`;
}

private escapeShellArg(arg: string): string {
return `'${arg.replace(/'/g, `'\\''`)}'`;
}

private escapeQuotes(arg: string): string {
return arg.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
}

private applyEnvironmentValue(processEnv: NodeJS.ProcessEnv, terminalEnv: TerminalEnvironment, name: string, value: string | null): void {
const matchingKeys = isWindows
? new Set([...Object.keys(processEnv), ...Object.keys(terminalEnv)].filter(key => key.toLowerCase() === name.toLowerCase()))
: new Set([name]);

for (const key of matchingKeys) {
delete processEnv[key];
if (key !== name || value === null) {
terminalEnv[key] = null;
}
}

if (value === null) {
terminalEnv[name] = null;
} else {
processEnv[name] = value;
terminalEnv[name] = value;
}
}

private environmentsEqual(first: TerminalEnvironment | undefined, second: TerminalEnvironment): boolean {
if (!first) {
return false;
}

const firstKeys = Object.keys(first);
const secondKeys = Object.keys(second);
if (firstKeys.length !== secondKeys.length) {
return false;
}

return firstKeys.every(key => first[key] === second[key]);
}

private waitForShellIntegration(terminal: vscode.Terminal, timeoutMs: number): Promise<vscode.TerminalShellIntegration | undefined> {
return new Promise(resolve => {
let resolved: boolean = false;
Expand Down Expand Up @@ -289,6 +384,9 @@ export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter {
}

this.hasTerminated = true;
if (this.releaseTerminalOnTerminate && this.terminal) {
activeTerminals.delete(this.terminal);
}
this.disposeTerminalListeners();
}

Expand All @@ -302,6 +400,9 @@ export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter {

public dispose(): void {
this.terminateProcess();
if (this.releaseTerminalOnTerminate && this.terminal) {
activeTerminals.delete(this.terminal);
}
this.disposeTerminalListeners();
this.sendMessageEmitter.dispose();
}
Expand Down
21 changes: 21 additions & 0 deletions Extension/test/scenarios/RunWithoutDebugging/assets/envTest.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#include <cstdlib>
#include <fstream>

int main(int argc, char *argv[]) {
if (argc < 3) {
return 1;
}

const char *value = std::getenv(argv[1]);

std::ofstream resultFile(argv[2]);
if (!resultFile) {
return 2;
}

if (value) {
resultFile << value;
}

return 0;
}
Loading