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:
@@ -7,6 +7,7 @@
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
Desktop.ini
|
||||
nul
|
||||
|
||||
# ===========================
|
||||
# Security - Environment & Secrets
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user