fix(windows): complete System32 executable path fixes for where.exe and taskkill.exe (#1715)

* fix(windows): complete System32 executable path fixes for where.exe and taskkill.exe

Complete the Windows System32 executable path fixes started in #1659.
The previous fix addressed where.exe in a few frontend files but missed
several critical locations in both backend and frontend.

Backend changes:
- Add get_where_exe_path() helper in core/platform/__init__.py
- Update auth.py, git_executable.py, gh_executable.py, glab_executable.py
  to use full path instead of bare 'where' command
- Remove shell=True from subprocess calls (security improvement)

Frontend changes:
- Add getTaskkillExePath() helper in windows-paths.ts
- Update platform/index.ts, subprocess-runner.ts, pty-daemon-client.ts
  to use full path for taskkill.exe
- Update claude-code-handlers.ts to use getWhereExePath()
- Add System32 to ESSENTIAL_SYSTEM_PATHS in env-utils.ts
- Update process-kill.test.ts to mock the new helper

Why full paths:
- Works even when System32 isn't in PATH (GUI launch scenarios)
- SystemRoot env var is a protected Windows system variable
- Prevents PATH hijacking attacks
- Removing shell=True prevents command injection

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address PR review feedback for Windows System32 executable paths

- Fix missed bare 'taskkill' at subprocess-runner.ts:174 (stopFn closure)
- Fix missed bare 'where' at mcp-handlers.ts:198 (MCP health check)
- Update stale comments in gh_executable.py and glab_executable.py
  (removed incorrect "shell=True" reference, now matches git_executable.py)
- Add error logging callback for taskkill in stopFn (was empty callback)
- Improve MCP health check error messages with actionable diagnostics
  for ENOENT and EACCES errors on both Windows and Unix

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: use getWhereExePath() for 'where gh' in validateGitHubModule

Fix missed bare 'where gh' at line 617 in subprocess-runner.ts.
This completes the System32 executable path refactoring.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: use execFileAsync for gh CLI detection consistency

Replace shell string interpolation with execFileAsync for the gh CLI
check in subprocess-runner.ts, matching the pattern used in
claude-code-handlers.ts and mcp-handlers.ts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(windows): address PR review findings for System32 path consistency

- Use getTaskkillExePath() in claude-code-handlers.ts install commands
  instead of bare 'taskkill' (NEW-003)
- Add ENOENT error handling in subprocess-runner.ts validateGitHubModule
  to distinguish missing where.exe from missing gh CLI (NEW-006)
- Extract shared getSystemRoot() helper in windows-paths.ts to
  deduplicate SystemRoot resolution logic (NEW-002)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(windows): add tests, export getSystemRoot, add windowsHide to MCP spawn

- Add unit tests for getWhereExePath/getTaskkillExePath with env fallback coverage
- Export getSystemRoot() for reuse across modules
- Add windowsHide: true to mcp-handlers spawn call (consistency with other sites)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Andy <119136210+AndyMik90@users.noreply.github.com>
This commit is contained in:
VDT-91
2026-02-09 22:20:41 +01:00
committed by GitHub
parent a9b93e6dfb
commit aa7f56e5d0
15 changed files with 292 additions and 44 deletions
+1
View File
@@ -7,6 +7,7 @@
Thumbs.db
ehthumbs.db
Desktop.ini
nul
# ===========================
# Security - Environment & Secrets
+3 -2
View File
@@ -15,6 +15,7 @@ import subprocess
from typing import TYPE_CHECKING
from core.platform import (
get_where_exe_path,
is_linux,
is_macos,
is_windows,
@@ -854,9 +855,9 @@ def _find_git_bash_path() -> str | None:
# Method 1: Use 'where' command to find git.exe
try:
# Use where.exe explicitly for reliability
# Use full path to where.exe for reliability (works even when System32 isn't in PATH)
result = subprocess.run(
["where.exe", "git"],
[get_where_exe_path(), "git"],
capture_output=True,
text=True,
timeout=5,
+4 -3
View File
@@ -10,6 +10,8 @@ import os
import shutil
import subprocess
from core.platform import get_where_exe_path
_cached_gh_path: str | None = None
@@ -53,12 +55,11 @@ def _run_where_command() -> str | None:
"""
try:
result = subprocess.run(
"where gh",
[get_where_exe_path(), "gh"],
capture_output=True,
text=True,
encoding="utf-8",
timeout=5,
shell=True, # Required: 'where' command must be executed through shell on Windows
)
if result.returncode == 0 and result.stdout.strip():
found_path = result.stdout.strip().split("\n")[0].strip()
@@ -133,7 +134,7 @@ def _find_gh_executable() -> str | None:
if os.path.isfile(path) and _verify_gh_executable(path):
return path
# 5. Try 'where' command with shell=True (more reliable on Windows)
# 5. Try 'where' command with full path (works even when System32 isn't in PATH)
return _run_where_command()
return None
+4 -3
View File
@@ -15,6 +15,8 @@ import shutil
import subprocess
from pathlib import Path
from core.platform import get_where_exe_path
# Git environment variables that can interfere with worktree operations
# when set by pre-commit hooks or other git configurations.
# These must be cleared to prevent cross-worktree contamination.
@@ -124,14 +126,13 @@ def _find_git_executable() -> str:
except OSError:
continue
# 4. Try 'where' command with shell=True (more reliable on Windows)
# 4. Try 'where' command with full path (works even when System32 isn't in PATH)
try:
result = subprocess.run(
"where git",
[get_where_exe_path(), "git"],
capture_output=True,
text=True,
timeout=5,
shell=True,
)
if result.returncode == 0 and result.stdout.strip():
found_path = result.stdout.strip().split("\n")[0].strip()
+4 -3
View File
@@ -10,6 +10,8 @@ import os
import shutil
import subprocess
from core.platform import get_where_exe_path
_cached_glab_path: str | None = None
@@ -53,12 +55,11 @@ def _run_where_command() -> str | None:
"""
try:
result = subprocess.run(
"where glab",
[get_where_exe_path(), "glab"],
capture_output=True,
text=True,
encoding="utf-8",
timeout=5,
shell=True, # Required: 'where' command must be executed through shell on Windows
)
if result.returncode == 0 and result.stdout.strip():
found_path = result.stdout.strip().split("\n")[0].strip()
@@ -134,7 +135,7 @@ def _find_glab_executable() -> str | None:
if os.path.isfile(path) and _verify_glab_executable(path):
return path
# 5. Try 'where' command with shell=True (more reliable on Windows)
# 5. Try 'where' command with full path (works even when System32 isn't in PATH)
return _run_where_command()
return None
+16
View File
@@ -430,6 +430,22 @@ def requires_shell(command: str) -> bool:
return ext.lower() in {".cmd", ".bat", ".ps1"}
def get_where_exe_path() -> str:
"""Get full path to where.exe on Windows.
Using the full path ensures where.exe works even when System32 isn't in PATH,
which can happen in restricted environments or when the app doesn't inherit
the full system PATH.
Returns:
Full path to where.exe (e.g., C:\\Windows\\System32\\where.exe)
"""
system_root = os.environ.get(
"SystemRoot", os.environ.get("SYSTEMROOT", "C:\\Windows")
)
return os.path.join(system_root, "System32", "where.exe")
def get_comspec_path() -> str:
"""
Get the path to cmd.exe on Windows.
+16 -8
View File
@@ -147,9 +147,15 @@ export const COMMON_BIN_PATHS: Record<string, string[]> = {
/**
* Essential system directories that must always be in PATH
* Required for core system functionality (e.g., /usr/bin/security for Keychain access)
* Required for core system functionality (e.g., /usr/bin/security for Keychain access on macOS,
* System32 for where.exe/taskkill.exe on Windows)
*/
const ESSENTIAL_SYSTEM_PATHS: string[] = ['/usr/bin', '/bin', '/usr/sbin', '/sbin'];
const ESSENTIAL_SYSTEM_PATHS: Record<string, string[]> = {
unix: ['/usr/bin', '/bin', '/usr/sbin', '/sbin'],
win32: [
`${process.env.SystemRoot || process.env.SYSTEMROOT || 'C:\\Windows'}\\System32`,
],
};
/**
* Get expanded platform paths for PATH augmentation
@@ -238,10 +244,11 @@ export function getAugmentedEnv(additionalPaths?: string[]): Record<string, stri
// The Claude Agent SDK needs /usr/bin/security to access macOS Keychain.
let currentPath = env.PATH || '';
// On macOS/Linux, ensure basic system paths are always present
if (isUnix()) {
// Ensure basic system paths are always present
{
const essentialPaths = isUnix() ? ESSENTIAL_SYSTEM_PATHS.unix : ESSENTIAL_SYSTEM_PATHS.win32;
const pathSetForEssentials = new Set(currentPath.split(pathSeparator).filter(Boolean));
const missingEssentials = ESSENTIAL_SYSTEM_PATHS.filter(p => !pathSetForEssentials.has(p));
const missingEssentials = essentialPaths.filter(p => !pathSetForEssentials.has(p));
if (missingEssentials.length > 0) {
// Append essential paths if missing (append, not prepend, to respect user's PATH)
@@ -407,12 +414,13 @@ export async function getAugmentedEnvAsync(additionalPaths?: string[]): Promise<
// Get all candidate paths (platform + additional)
const candidatePaths = getExpandedPlatformPaths(additionalPaths);
// Ensure essential system paths are present (for macOS Keychain access)
// Ensure essential system paths are present (for macOS Keychain access, Windows System32)
let currentPath = env.PATH || '';
if (isUnix()) {
{
const essentialPaths = isUnix() ? ESSENTIAL_SYSTEM_PATHS.unix : ESSENTIAL_SYSTEM_PATHS.win32;
const pathSetForEssentials = new Set(currentPath.split(pathSeparator).filter(Boolean));
const missingEssentials = ESSENTIAL_SYSTEM_PATHS.filter(p => !pathSetForEssentials.has(p));
const missingEssentials = essentialPaths.filter(p => !pathSetForEssentials.has(p));
if (missingEssentials.length > 0) {
currentPath = currentPath
@@ -19,7 +19,7 @@ import type { IPCResult } from '../../shared/types';
import type { ClaudeCodeVersionInfo, ClaudeInstallationList, ClaudeInstallationInfo } from '../../shared/types/cli';
import { getToolInfo, configureTools, sortNvmVersionDirs, getClaudeDetectionPaths, type ExecFileAsyncOptionsWithVerbatim } from '../cli-tool-manager';
import { readSettingsFile, writeSettingsFile } from '../settings-utils';
import { isSecurePath } from '../utils/windows-paths';
import { isSecurePath, getWhereExePath, getTaskkillExePath } from '../utils/windows-paths';
import { isWindows, isMacOS, isLinux } from '../platform';
import { getClaudeProfileManager } from '../claude-profile-manager';
import { isValidConfigDir } from '../utils/config-path-validator';
@@ -147,7 +147,7 @@ async function scanClaudeInstallations(activePath: string | null): Promise<Claud
// 2. Check system PATH via which/where
try {
if (isWindows()) {
const result = await execFileAsync('where', ['claude'], { timeout: 5000 });
const result = await execFileAsync(getWhereExePath(), ['claude'], { timeout: 5000 });
const paths = result.stdout.trim().split('\n').filter(p => p.trim());
for (const p of paths) {
await addInstallation(p.trim(), 'system-path');
@@ -338,7 +338,7 @@ async function fetchAvailableVersions(): Promise<string[]> {
function getInstallVersionCommand(version: string): string {
if (isWindows()) {
// Windows: kill running Claude processes first, then install specific version
return `taskkill /IM claude.exe /F 2>nul; claude install --force ${version}`;
return `"${getTaskkillExePath()}" /IM claude.exe /F 2>nul; claude install --force ${version}`;
} else {
// macOS/Linux: kill running Claude processes first, then install specific version
return `pkill -x claude 2>/dev/null; sleep 1; claude install --force ${version}`;
@@ -353,7 +353,7 @@ function getInstallCommand(isUpdate: boolean): string {
if (isWindows()) {
if (isUpdate) {
// Update: kill running Claude processes first, then update with --force
return 'taskkill /IM claude.exe /F 2>nul; claude install --force latest';
return `"${getTaskkillExePath()}" /IM claude.exe /F 2>nul; claude install --force latest`;
}
return 'irm https://claude.ai/install.ps1 | iex';
} else {
@@ -22,8 +22,10 @@ import { detectAuthFailure, detectBillingFailure } from '../../../rate-limit-det
import { getClaudeProfileManager } from '../../../claude-profile-manager';
import { getOperationRegistry, type OperationType } from '../../../claude-profile/operation-registry';
import { isWindows, isMacOS } from '../../../platform';
import { getTaskkillExePath, getWhereExePath } from '../../../utils/windows-paths';
const execAsync = promisify(exec);
const execFileAsync = promisify(execFile);
/**
* Create a fallback environment for Python subprocesses when no env is provided.
@@ -170,7 +172,9 @@ export function runPythonSubprocess<T = unknown>(
if (!isWindows()) {
process.kill(-child.pid, 'SIGTERM');
} else {
execFile('taskkill', ['/pid', String(child.pid), '/T', '/F'], () => {});
execFile(getTaskkillExePath(), ['/pid', String(child.pid), '/T', '/F'], (err: Error | null) => {
if (err) console.warn('[SubprocessRunner] taskkill error (process may have already exited):', err.message);
});
}
} catch {
child.kill('SIGTERM');
@@ -261,7 +265,7 @@ export function runPythonSubprocess<T = unknown>(
process.kill(-child.pid, 'SIGKILL');
} else {
// On Windows, use taskkill to kill the process tree
execFile('taskkill', ['/pid', String(child.pid), '/T', '/F'], (err: Error | null) => {
execFile(getTaskkillExePath(), ['/pid', String(child.pid), '/T', '/F'], (err: Error | null) => {
if (err) console.warn('[SubprocessRunner] taskkill error (process may have already exited):', err.message);
});
}
@@ -320,7 +324,7 @@ export function runPythonSubprocess<T = unknown>(
process.kill(-child.pid, 'SIGKILL');
} else {
// On Windows, use taskkill to kill the process tree
execFile('taskkill', ['/pid', String(child.pid), '/T', '/F'], (err: Error | null) => {
execFile(getTaskkillExePath(), ['/pid', String(child.pid), '/T', '/F'], (err: Error | null) => {
if (err) console.warn('[SubprocessRunner] taskkill error (process may have already exited):', err.message);
});
}
@@ -611,17 +615,25 @@ export async function validateGitHubModule(project: Project): Promise<GitHubModu
// 2. Check gh CLI installation (cross-platform)
try {
const whichCommand = isWindows() ? 'where gh' : 'which gh';
await execAsync(whichCommand);
if (isWindows()) {
await execFileAsync(getWhereExePath(), ['gh'], { timeout: 5000 });
} else {
await execAsync('which gh');
}
result.ghCliInstalled = true;
} catch {
} catch (error: unknown) {
result.ghCliInstalled = false;
const installInstructions = isWindows()
? 'winget install --id GitHub.cli'
: isMacOS()
? 'brew install gh'
: 'See https://cli.github.com/';
result.error = `GitHub CLI (gh) is not installed. Install it with:\n ${installInstructions}`;
const errCode = (error as NodeJS.ErrnoException).code;
if (errCode === 'ENOENT' && isWindows()) {
result.error = `System utility 'where.exe' not found. Check Windows installation.`;
} else {
const installInstructions = isWindows()
? 'winget install --id GitHub.cli'
: isMacOS()
? 'brew install gh'
: 'See https://cli.github.com/';
result.error = `GitHub CLI (gh) is not installed. Install it with:\n ${installInstructions}`;
}
return result;
}
@@ -10,6 +10,7 @@ import type { CustomMcpServer, McpHealthCheckResult, McpHealthStatus, McpTestCon
import { spawn } from 'child_process';
import { appLog } from '../app-logger';
import { isWindows } from '../platform';
import { getWhereExePath } from '../utils/windows-paths';
/**
* Defense-in-depth: Frontend-side command validation
@@ -194,9 +195,10 @@ async function checkCommandHealth(server: CustomMcpServer, startTime: number): P
});
}
const command = isWindows() ? 'where' : 'which';
const command = isWindows() ? getWhereExePath() : 'which';
const proc = spawn(command, [server.command!], {
timeout: 5000,
windowsHide: true,
});
let found = false;
@@ -227,12 +229,24 @@ async function checkCommandHealth(server: CustomMcpServer, startTime: number): P
found = true;
});
proc.on('error', () => {
proc.on('error', (error: Error) => {
const responseTime = Date.now() - startTime;
const errCode = (error as NodeJS.ErrnoException).code;
let message = `Failed to check command '${server.command}'`;
// Provide actionable error messages for common failures
if (errCode === 'ENOENT') {
message = isWindows()
? `System utility 'where.exe' not found. Check Windows installation.`
: `System utility 'which' not found. Check system PATH configuration.`;
} else if (errCode === 'EACCES') {
message = `Permission denied checking command '${server.command}'`;
}
resolve({
serverId: server.id,
status: 'unhealthy',
message: `Failed to check command '${server.command}'`,
message,
responseTime,
checkedAt: new Date().toISOString(),
});
@@ -8,6 +8,11 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { EventEmitter } from 'events';
import type { ChildProcess } from 'child_process';
// Mock getTaskkillExePath to return a predictable path for testing
vi.mock('../../utils/windows-paths', () => ({
getTaskkillExePath: () => 'C:\\Windows\\System32\\taskkill.exe',
}));
// Mock child_process.spawn before importing the module
vi.mock('child_process', async () => {
const actual = await vi.importActual('child_process');
@@ -95,7 +100,7 @@ describe('killProcessGracefully', () => {
// Verify taskkill was called with correct arguments
expect(mockSpawn).toHaveBeenCalledWith(
'taskkill',
'C:\\Windows\\System32\\taskkill.exe',
['/pid', '12345', '/f', '/t'],
expect.objectContaining({
stdio: 'ignore',
@@ -131,7 +136,7 @@ describe('killProcessGracefully', () => {
// taskkill should still be called - this is the key assertion for Issue #1
expect(mockSpawn).toHaveBeenCalledWith(
'taskkill',
'C:\\Windows\\System32\\taskkill.exe',
['/pid', '12345', '/f', '/t'],
expect.any(Object)
);
+2 -1
View File
@@ -16,6 +16,7 @@ import * as path from 'path';
import { existsSync } from 'fs';
import { spawn, ChildProcess } from 'child_process';
import { OS, ShellType, PathConfig, ShellConfig, BinaryDirectories } from './types';
import { getTaskkillExePath } from '../utils/windows-paths';
// Re-export from paths.ts for backward compatibility
export { getWindowsShellPaths, getOllamaExecutablePaths, getOllamaInstallCommand, getWhichCommand } from './paths';
@@ -484,7 +485,7 @@ export function killProcessGracefully(
try {
if (isWindows()) {
log('Running taskkill for PID:', pid);
spawn('taskkill', ['/pid', pid.toString(), '/f', '/t'], {
spawn(getTaskkillExePath(), ['/pid', pid.toString(), '/f', '/t'], {
stdio: 'ignore',
detached: true
}).unref();
@@ -10,6 +10,7 @@ import * as path from 'path';
import { fileURLToPath } from 'url';
import { spawn, ChildProcess } from 'child_process';
import { isWindows, GRACEFUL_KILL_TIMEOUT_MS } from '../platform';
import { getTaskkillExePath } from '../utils/windows-paths';
import { getIsShuttingDown } from './pty-manager';
// ESM-compatible __dirname
@@ -430,7 +431,7 @@ class PtyDaemonClient {
try {
if (isWindows()) {
// Windows: use taskkill to force kill process tree
spawn('taskkill', ['/pid', this.daemonProcess.pid.toString(), '/f', '/t'], {
spawn(getTaskkillExePath(), ['/pid', this.daemonProcess.pid.toString(), '/f', '/t'], {
stdio: 'ignore',
detached: true
}).unref();
@@ -0,0 +1,167 @@
/**
* Windows Paths Utility Tests
*
* Tests for getWhereExePath() and getTaskkillExePath() helper functions,
* including the private getSystemRoot() fallback logic tested indirectly.
*
* Note: On Windows, environment variables are case-insensitive, so
* `process.env.SystemRoot` and `process.env.SYSTEMROOT` always refer to the
* same value. The separate-casing fallback in getSystemRoot() is for
* cross-platform compatibility (Linux/macOS where env vars are case-sensitive).
* We test the fallback behavior using a platform-aware approach.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { getWhereExePath, getTaskkillExePath } from '../windows-paths';
const isWindows = process.platform === 'win32';
describe('windows-paths', () => {
// On Windows, SystemRoot and SYSTEMROOT are the same env var (case-insensitive).
// We save whichever exists and restore it after each test.
let savedSystemRoot: string | undefined;
beforeEach(() => {
// Save the current value (reading either casing works on Windows)
savedSystemRoot = process.env.SystemRoot;
});
afterEach(() => {
// Restore original env value
if (savedSystemRoot !== undefined) {
process.env.SystemRoot = savedSystemRoot;
} else {
delete process.env.SystemRoot;
// On non-Windows, also clean up the uppercase variant independently
if (!isWindows) {
delete process.env.SYSTEMROOT;
}
}
});
describe('getWhereExePath', () => {
it('returns correct path when SystemRoot env is set', () => {
process.env.SystemRoot = 'D:\\CustomWindows';
const result = getWhereExePath();
expect(result).toContain('CustomWindows');
expect(result).toContain('System32');
expect(result).toContain('where.exe');
});
it('returns correct path when SYSTEMROOT env is set', () => {
// On Windows, this is the same as setting SystemRoot (case-insensitive).
// On non-Windows, this tests the uppercase fallback in getSystemRoot().
if (!isWindows) {
delete process.env.SystemRoot;
}
process.env.SYSTEMROOT = 'E:\\AltWindows';
const result = getWhereExePath();
expect(result).toContain('AltWindows');
expect(result).toContain('System32');
expect(result).toContain('where.exe');
});
it('falls back to C:\\Windows when neither env var is set', () => {
delete process.env.SystemRoot;
if (!isWindows) {
delete process.env.SYSTEMROOT;
}
const result = getWhereExePath();
// When no env var is set, should fall back to C:\Windows
expect(result).toContain('Windows');
expect(result).toContain('System32');
expect(result).toContain('where.exe');
});
it('constructs path ending with System32/where.exe', () => {
process.env.SystemRoot = 'C:\\Windows';
const result = getWhereExePath();
// Accept either backslash (Windows) or forward slash (Unix) as separator
expect(result).toMatch(/System32[/\\]where\.exe$/);
});
});
describe('getTaskkillExePath', () => {
it('returns correct path when SystemRoot env is set', () => {
process.env.SystemRoot = 'D:\\CustomWindows';
const result = getTaskkillExePath();
expect(result).toContain('CustomWindows');
expect(result).toContain('System32');
expect(result).toContain('taskkill.exe');
});
it('returns correct path when SYSTEMROOT env is set', () => {
if (!isWindows) {
delete process.env.SystemRoot;
}
process.env.SYSTEMROOT = 'E:\\AltWindows';
const result = getTaskkillExePath();
expect(result).toContain('AltWindows');
expect(result).toContain('System32');
expect(result).toContain('taskkill.exe');
});
it('falls back to C:\\Windows when neither env var is set', () => {
delete process.env.SystemRoot;
if (!isWindows) {
delete process.env.SYSTEMROOT;
}
const result = getTaskkillExePath();
expect(result).toContain('Windows');
expect(result).toContain('System32');
expect(result).toContain('taskkill.exe');
});
it('constructs path ending with System32/taskkill.exe', () => {
process.env.SystemRoot = 'C:\\Windows';
const result = getTaskkillExePath();
// Accept either backslash (Windows) or forward slash (Unix) as separator
expect(result).toMatch(/System32[/\\]taskkill\.exe$/);
});
});
describe('getSystemRoot precedence (indirect)', () => {
it('uses the env var value for both functions consistently', () => {
process.env.SystemRoot = 'F:\\TestRoot';
const wherePath = getWhereExePath();
const taskkillPath = getTaskkillExePath();
expect(wherePath).toContain('TestRoot');
expect(taskkillPath).toContain('TestRoot');
expect(wherePath).toMatch(/System32[/\\]where\.exe$/);
expect(taskkillPath).toMatch(/System32[/\\]taskkill\.exe$/);
});
// On non-Windows platforms, env vars are case-sensitive, so we can test
// that SystemRoot takes precedence over SYSTEMROOT
it.skipIf(isWindows)('prefers SystemRoot over SYSTEMROOT when both are set (non-Windows only)', () => {
process.env.SystemRoot = 'D:\\Primary';
process.env.SYSTEMROOT = 'E:\\Secondary';
const wherePath = getWhereExePath();
const taskkillPath = getTaskkillExePath();
expect(wherePath).toContain('Primary');
expect(wherePath).not.toContain('Secondary');
expect(taskkillPath).toContain('Primary');
expect(taskkillPath).not.toContain('Secondary');
});
});
});
+21 -2
View File
@@ -130,6 +130,14 @@ export function getWindowsExecutablePaths(
return validPaths;
}
/**
* Get the Windows system root directory (e.g., C:\Windows).
* Checks both casing variants of the environment variable with a safe fallback.
*/
export function getSystemRoot(): string {
return process.env.SystemRoot || process.env.SYSTEMROOT || 'C:\\Windows';
}
/**
* Get the full path to where.exe.
* Using the full path ensures where.exe works even when System32 isn't in PATH,
@@ -139,8 +147,19 @@ export function getWindowsExecutablePaths(
* @returns Full path to where.exe (e.g., C:\Windows\System32\where.exe)
*/
export function getWhereExePath(): string {
const systemRoot = process.env.SystemRoot || process.env.SYSTEMROOT || 'C:\\Windows';
return path.join(systemRoot, 'System32', 'where.exe');
return path.join(getSystemRoot(), 'System32', 'where.exe');
}
/**
* Get the full path to taskkill.exe.
* Using the full path ensures taskkill.exe works even when System32 isn't in PATH,
* which can happen in restricted environments or when Electron doesn't inherit
* the full system PATH.
*
* @returns Full path to taskkill.exe (e.g., C:\Windows\System32\taskkill.exe)
*/
export function getTaskkillExePath(): string {
return path.join(getSystemRoot(), 'System32', 'taskkill.exe');
}
/**