Skip to content
104 changes: 102 additions & 2 deletions workspaces/ballerina/ballerina-extension/src/core/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1791,6 +1791,18 @@ export class BallerinaExtension {
if (distPath) { break; }
}
}
} else if (isWindows() && !ballerinaHome) {
// On Windows, if syncEnvironment() already merged the User+Machine PATH the
// 'bal.bat version' call below will just work via PATH lookup (distPath stays
// empty). But for restricted environments (where even User
// PATH is locked, or where VSCode's inherited PATH is still stale), we run a
// proactive directory search here so that we can use an absolute path instead
// of relying on PATH resolution.
const detectedBinPath = findWindowsBallerinaPath();
if (detectedBinPath) {
distPath = detectedBinPath;
debug(`[VERSION] Windows fallback search found Ballerina bin: ${distPath}`);
Comment on lines +1794 to +1804
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

This fallback search runs unconditionally whenever isWindows() && !ballerinaHome, even though the comment suggests PATH-based resolution will often already work after syncEnvironment(). Since findWindowsBallerinaPath() does synchronous PowerShell + filesystem scans (up to 10s timeout), consider only running it after the initial bal(.bat) version attempt fails (e.g. ENOENT / "not recognized"), and/or caching the detected result so repeated version checks don’t repeatedly invoke PowerShell.

Copilot uses AI. Check for mistakes.
}
Comment on lines +1801 to +1805
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

findWindowsBallerinaPath() can return an absolute bin path under locations like C:\Program Files\.... That distPath is later concatenated into a command string (e.g. distPath + 'bal' + exeExtension) and executed via exec() / cmd.exe /c. Without quoting or switching to execFile/spawn with args, paths containing spaces will fail to execute on Windows. Consider returning a fully-quoted executable path, or (preferably) avoid shell parsing by invoking bal.bat via spawn/execFile with an explicit file path + args.

Copilot uses AI. Check for mistakes.
}

let exeExtension = "";
Expand Down Expand Up @@ -2679,6 +2691,83 @@ function updateProcessEnv(newEnv: NodeJS.ProcessEnv): void {
debug("[UPDATE_ENV] Process environment update completed");
}

/**
* Searches for the Ballerina bin directory on Windows using two strategies:
* 1. Read the User-scope and Machine-scope PATH entries from the registry and look
* for a directory that contains bal.bat.
* 2. Check well-known installation directories (LOCALAPPDATA, ProgramFiles, etc.).
*
* Returns the bin directory path (with trailing separator) or an empty string when
* nothing is found. This is used as a last-resort fallback for environments where the
* process PATH was not updated (e.g. company laptops with restricted System PATH, or
* VS Code opened before the installer ran).
*/
function findWindowsBallerinaPath(): string {
debug('[WIN_BAL_FIND] Searching for Ballerina installation on Windows...');

// --- Strategy 1: scan PATH entries from User + Machine registry scopes ---
try {
const psCommand =
'[Environment]::GetEnvironmentVariable(\'Path\',\'Machine\') + \';\' + ' +
'[Environment]::GetEnvironmentVariable(\'Path\',\'User\')';
const rawPaths = execSync(
`powershell.exe -NoProfile -Command "${psCommand}"`,
{ encoding: 'utf8', timeout: 10000 }
).trim();

debug(`[WIN_BAL_FIND] Registry PATH (Machine+User) length: ${rawPaths.length} chars`);

const pathEntries = rawPaths.split(';').map(p => p.trim()).filter(Boolean);
for (const entry of pathEntries) {
const candidate = path.join(entry, 'bal.bat');
if (fs.existsSync(candidate)) {
debug(`[WIN_BAL_FIND] Found bal.bat in registry PATH entry: ${entry}`);
return entry + path.sep;
}
Comment on lines +2723 to +2726
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

findWindowsBallerinaPath() returns entry + path.sep when it finds a PATH entry containing bal.bat. If the PATH entry already ends with a separator, this can produce a double separator (e.g. ...\\). It’s safer to normalize before returning (e.g. ensure exactly one trailing separator) so downstream string concatenations produce consistent command paths.

Copilot uses AI. Check for mistakes.
}
debug('[WIN_BAL_FIND] bal.bat not found in registry PATH entries');
} catch (err) {
debug(`[WIN_BAL_FIND] Failed to read registry PATH: ${err}`);
}

// --- Strategy 2: check well-known Ballerina installation directories ---
const localAppData = process.env.LOCALAPPDATA || '';
const programFiles = process.env.ProgramFiles || 'C:\\Program Files';
const programFilesX86 = process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)';

const searchRoots = [
localAppData ? path.join(localAppData, 'Programs', 'Ballerina') : '',
path.join(programFiles, 'Ballerina'),
path.join(programFilesX86, 'Ballerina'),
'C:\\Ballerina',
].filter(Boolean);

for (const root of searchRoots) {
const directBin = path.join(root, 'bin');
if (fs.existsSync(path.join(directBin, 'bal.bat'))) {
debug(`[WIN_BAL_FIND] Found bal.bat in common directory: ${directBin}`);
return directBin + path.sep;
}
// Handle versioned subdirectory layout, e.g. Ballerina\ballerina-2.x.x\bin
try {
const children = fs.readdirSync(root);
for (const child of children) {
const versionedBin = path.join(root, child, 'bin');
if (fs.existsSync(path.join(versionedBin, 'bal.bat'))) {
debug(`[WIN_BAL_FIND] Found bal.bat in versioned directory: ${versionedBin}`);
return versionedBin + path.sep;
}
}
} catch (err) {
// Directory doesn't exist or isn't readable — skip
debug(`[WIN_BAL_FIND] Failed to read directory "${root}" for versioned Ballerina installations: ${err}`);
}
}

debug('[WIN_BAL_FIND] Ballerina installation not found via fallback search');
return '';
}

function getShellEnvironment(): Promise<NodeJS.ProcessEnv> {
return new Promise((resolve, reject) => {
debug('[SHELL_ENV] Starting shell environment retrieval...');
Expand All @@ -2688,8 +2777,19 @@ function getShellEnvironment(): Promise<NodeJS.ProcessEnv> {

if (isWindowsPlatform) {
debug('[SHELL_ENV] Windows platform detected');
// Windows: use PowerShell to get environment
command = 'powershell.exe -Command "[Environment]::GetEnvironmentVariables(\'Process\') | ConvertTo-Json"';
// Windows: read from registry (Machine + User scopes) so that paths added by
// a fresh Ballerina install (which goes to the User PATH registry key) are
// picked up even when VS Code's process was launched before the installation.
// We start with the current Process environment so that VS Code-internal
// variables are preserved, but we override Path with the merged registry value.
command = 'powershell.exe -NoProfile -Command "' +
'$e=[Environment]::GetEnvironmentVariables(\'Process\');' +
'$mp=[Environment]::GetEnvironmentVariable(\'Path\',\'Machine\');' +
'$up=[Environment]::GetEnvironmentVariable(\'Path\',\'User\');' +
'if($mp -and $up){$e[\'Path\']=$mp+\';\'+$up}' +
'elseif($mp){$e[\'Path\']=$mp}' +
'elseif($up){$e[\'Path\']=$up};' +
'$e | ConvertTo-Json"';
Comment on lines +2780 to +2792
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Overriding Path with registry-only values drops VS Code-internal PATH entries.

The new PowerShell command replaces $e['Path'] with Machine + User from the registry. Any paths that VS Code (or its extension host) injected into the process environment at runtime — for example, its embedded git or node binary directories — will be lost when updateProcessEnv later sets process.env.Path = newEnv.Path.

Consider merging the registry values with the current process PATH instead of replacing it, so that VS Code-internal tooling remains functional:

Proposed fix: merge registry PATH with existing process PATH
             command = 'powershell.exe -NoProfile -Command "' +
                 '$e=[Environment]::GetEnvironmentVariables(\'Process\');' +
                 '$mp=[Environment]::GetEnvironmentVariable(\'Path\',\'Machine\');' +
                 '$up=[Environment]::GetEnvironmentVariable(\'Path\',\'User\');' +
-                'if($mp -and $up){$e[\'Path\']=$mp+\';\'+$up}' +
-                'elseif($mp){$e[\'Path\']=$mp}' +
-                'elseif($up){$e[\'Path\']=$up};' +
+                '$pp=$e[\'Path\'];' +
+                '$merged=@($mp,$up,$pp|Where-Object{$_})-join\';\';' +
+                'if($merged){$e[\'Path\']=$merged};' +
                 '$e | ConvertTo-Json"';

This keeps existing process Path entries (including VS Code-injected ones) while prepending the registry Machine and User values so that freshly-installed tools are found first.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Windows: read from registry (Machine + User scopes) so that paths added by
// a fresh Ballerina install (which goes to the User PATH registry key) are
// picked up even when VS Code's process was launched before the installation.
// We start with the current Process environment so that VS Code-internal
// variables are preserved, but we override Path with the merged registry value.
command = 'powershell.exe -NoProfile -Command "' +
'$e=[Environment]::GetEnvironmentVariables(\'Process\');' +
'$mp=[Environment]::GetEnvironmentVariable(\'Path\',\'Machine\');' +
'$up=[Environment]::GetEnvironmentVariable(\'Path\',\'User\');' +
'if($mp -and $up){$e[\'Path\']=$mp+\';\'+$up}' +
'elseif($mp){$e[\'Path\']=$mp}' +
'elseif($up){$e[\'Path\']=$up};' +
'$e | ConvertTo-Json"';
command = 'powershell.exe -NoProfile -Command "' +
'$e=[Environment]::GetEnvironmentVariables(\'Process\');' +
'$mp=[Environment]::GetEnvironmentVariable(\'Path\',\'Machine\');' +
'$up=[Environment]::GetEnvironmentVariable(\'Path\',\'User\');' +
'$pp=$e[\'Path\'];' +
'$merged=@($mp,$up,$pp|Where-Object{$_})-join\';\';' +
'if($merged){$e[\'Path\']=$merged};' +
'$e | ConvertTo-Json"';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@workspaces/ballerina/ballerina-extension/src/core/extension.ts` around lines
2780 - 2792, The PowerShell snippet assigned to command currently overwrites
$e['Path'] with registry-only values ($mp + $up), which drops VS Code-injected
entries when updateProcessEnv later sets process.env.Path; change the logic in
the command construction so that $e['Path'] preserves the existing process Path
($e['Path']) and prepends or merges the registry Machine/User values ($mp, $up)
into it (avoid simple replacement), ensuring process.env.Path retains VS
Code/internal paths after updateProcessEnv uses the returned newEnv.Path.

debug(`[SHELL_ENV] Windows command: ${command}`);
} else if (isWSL()) {
debug("[SHELL_ENV] Windows WSL platform, using non-interactive shell");
Expand Down
Loading