aa7f56e5d0
* 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>
193 lines
5.7 KiB
Python
193 lines
5.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
GitHub CLI Executable Finder
|
|
============================
|
|
|
|
Utility to find the gh (GitHub CLI) executable, with platform-specific fallbacks.
|
|
"""
|
|
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
|
|
from core.platform import get_where_exe_path
|
|
|
|
_cached_gh_path: str | None = None
|
|
|
|
|
|
def invalidate_gh_cache() -> None:
|
|
"""Invalidate the cached gh executable path.
|
|
|
|
Useful when gh may have been uninstalled, updated, or when
|
|
GITHUB_CLI_PATH environment variable has changed.
|
|
"""
|
|
global _cached_gh_path
|
|
_cached_gh_path = None
|
|
|
|
|
|
def _verify_gh_executable(path: str) -> bool:
|
|
"""Verify that a path is a valid gh executable by checking version.
|
|
|
|
Args:
|
|
path: Path to the potential gh executable
|
|
|
|
Returns:
|
|
True if the path points to a valid gh executable, False otherwise
|
|
"""
|
|
try:
|
|
result = subprocess.run(
|
|
[path, "--version"],
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
timeout=5,
|
|
)
|
|
return result.returncode == 0
|
|
except (subprocess.TimeoutExpired, OSError):
|
|
return False
|
|
|
|
|
|
def _run_where_command() -> str | None:
|
|
"""Run Windows 'where gh' command to find gh executable.
|
|
|
|
Returns:
|
|
First path found, or None if command failed
|
|
"""
|
|
try:
|
|
result = subprocess.run(
|
|
[get_where_exe_path(), "gh"],
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
timeout=5,
|
|
)
|
|
if result.returncode == 0 and result.stdout.strip():
|
|
found_path = result.stdout.strip().split("\n")[0].strip()
|
|
if (
|
|
found_path
|
|
and os.path.isfile(found_path)
|
|
and _verify_gh_executable(found_path)
|
|
):
|
|
return found_path
|
|
except (subprocess.TimeoutExpired, OSError):
|
|
# 'where' command failed or timed out - fall through to return None
|
|
pass
|
|
return None
|
|
|
|
|
|
def get_gh_executable() -> str | None:
|
|
"""Find the gh executable, with platform-specific fallbacks.
|
|
|
|
Returns the path to gh executable, or None if not found.
|
|
|
|
Priority order:
|
|
1. GITHUB_CLI_PATH env var (user-configured path from frontend)
|
|
2. shutil.which (if gh is in PATH)
|
|
3. Homebrew paths on macOS
|
|
4. Windows Program Files paths
|
|
5. Windows 'where' command
|
|
|
|
Caches the result after first successful find. Use invalidate_gh_cache()
|
|
to force re-detection (e.g., after gh installation/uninstallation).
|
|
"""
|
|
global _cached_gh_path
|
|
|
|
# Return cached result if available AND still exists
|
|
if _cached_gh_path is not None and os.path.isfile(_cached_gh_path):
|
|
return _cached_gh_path
|
|
|
|
_cached_gh_path = _find_gh_executable()
|
|
return _cached_gh_path
|
|
|
|
|
|
def _find_gh_executable() -> str | None:
|
|
"""Internal function to find gh executable."""
|
|
# 1. Check GITHUB_CLI_PATH env var (set by Electron frontend)
|
|
env_path = os.environ.get("GITHUB_CLI_PATH")
|
|
if env_path and os.path.isfile(env_path) and _verify_gh_executable(env_path):
|
|
return env_path
|
|
|
|
# 2. Try shutil.which (works if gh is in PATH)
|
|
gh_path = shutil.which("gh")
|
|
if gh_path and _verify_gh_executable(gh_path):
|
|
return gh_path
|
|
|
|
# 3. macOS-specific: check Homebrew paths
|
|
if os.name != "nt": # Unix-like systems (macOS, Linux)
|
|
homebrew_paths = [
|
|
"/opt/homebrew/bin/gh", # Apple Silicon
|
|
"/usr/local/bin/gh", # Intel Mac
|
|
"/home/linuxbrew/.linuxbrew/bin/gh", # Linux Homebrew
|
|
]
|
|
for path in homebrew_paths:
|
|
if os.path.isfile(path) and _verify_gh_executable(path):
|
|
return path
|
|
|
|
# 4. Windows-specific: check Program Files paths
|
|
if os.name == "nt":
|
|
windows_paths = [
|
|
os.path.expandvars(r"%PROGRAMFILES%\GitHub CLI\gh.exe"),
|
|
os.path.expandvars(r"%PROGRAMFILES(X86)%\GitHub CLI\gh.exe"),
|
|
os.path.expandvars(r"%LOCALAPPDATA%\Programs\GitHub CLI\gh.exe"),
|
|
]
|
|
for path in windows_paths:
|
|
if os.path.isfile(path) and _verify_gh_executable(path):
|
|
return path
|
|
|
|
# 5. Try 'where' command with full path (works even when System32 isn't in PATH)
|
|
return _run_where_command()
|
|
|
|
return None
|
|
|
|
|
|
def run_gh(
|
|
args: list[str],
|
|
cwd: str | None = None,
|
|
timeout: int = 60,
|
|
input_data: str | None = None,
|
|
) -> subprocess.CompletedProcess:
|
|
"""Run a gh command with proper executable finding.
|
|
|
|
Args:
|
|
args: gh command arguments (without 'gh' prefix)
|
|
cwd: Working directory for the command
|
|
timeout: Command timeout in seconds (default: 60)
|
|
input_data: Optional string data to pass to stdin
|
|
|
|
Returns:
|
|
CompletedProcess with command results.
|
|
"""
|
|
gh = get_gh_executable()
|
|
if not gh:
|
|
return subprocess.CompletedProcess(
|
|
args=["gh"] + args,
|
|
returncode=-1,
|
|
stdout="",
|
|
stderr="GitHub CLI (gh) not found. Install from https://cli.github.com/",
|
|
)
|
|
try:
|
|
return subprocess.run(
|
|
[gh] + args,
|
|
cwd=cwd,
|
|
input=input_data,
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
errors="replace",
|
|
timeout=timeout,
|
|
)
|
|
except subprocess.TimeoutExpired:
|
|
return subprocess.CompletedProcess(
|
|
args=[gh] + args,
|
|
returncode=-1,
|
|
stdout="",
|
|
stderr=f"Command timed out after {timeout} seconds",
|
|
)
|
|
except FileNotFoundError:
|
|
return subprocess.CompletedProcess(
|
|
args=[gh] + args,
|
|
returncode=-1,
|
|
stdout="",
|
|
stderr="GitHub CLI (gh) executable not found. Install from https://cli.github.com/",
|
|
)
|