Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 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
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
33 changes: 33 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,35 @@ 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, string>();

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

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

config.environment = Array.from(mergedEnvironment.entries()).map(([name, value]) => ({ name, value }));
delete config.env;
}

private resolveSourceFileMapVariables(config: CppDebugConfiguration): void {
const messages: string[] = [];
if (config.sourceFileMap) {
Expand Down
108 changes: 93 additions & 15 deletions Extension/src/Debugger/runWithoutDebuggingAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,31 @@ 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 => {
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 Down Expand Up @@ -59,31 +84,30 @@ 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);
}
Expand All @@ -93,14 +117,28 @@ export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter {
* 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 on lines +127 to +130
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 =
Expand Down Expand Up @@ -212,6 +250,40 @@ export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter {
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 +361,9 @@ export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter {
}

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

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

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