diff --git a/openhands/runtime/utils/windows_bash.py b/openhands/runtime/utils/windows_bash.py index 6ecaaa2da6..6771664939 100644 --- a/openhands/runtime/utils/windows_bash.py +++ b/openhands/runtime/utils/windows_bash.py @@ -5,6 +5,8 @@ way to manage PowerShell processes compared to using temporary script files. """ import os +import re +import subprocess import time from pathlib import Path from threading import RLock @@ -45,39 +47,121 @@ except Exception as coreclr_ex: logger.error(f'{error_msg} Details: {details}') raise DotNetMissingError(error_msg, details) + +def find_latest_pwsh_sdk_path( + executable_name='pwsh.exe', + dll_name='System.Management.Automation.dll', + min_version=(7, 0, 0), + env_var='PWSH_DIR', +): + """ + Checks PWSH_DIR environment variable first to find pwsh and DLL. + If not found or not suitable, scans all pwsh executables in PATH, runs --version to find latest >= min_version. + Returns full DLL path if found, else None. + """ + + def parse_version(output): + # Extract semantic version from pwsh --version output + match = re.search(r'(\d+)\.(\d+)\.(\d+)', output) + if match: + return tuple(map(int, match.groups())) + return None + + # Try environment variable override first + pwsh_dir = os.environ.get(env_var) + if pwsh_dir: + pwsh_path = Path(pwsh_dir) / executable_name + dll_path = Path(pwsh_dir) / dll_name + if pwsh_path.is_file() and dll_path.is_file(): + try: + completed = subprocess.run( + [str(pwsh_path), '--version'], + capture_output=True, + text=True, + timeout=5, + ) + if completed.returncode == 0: + ver = parse_version(completed.stdout) + if ver and ver >= min_version: + logger.info(f'Found pwsh from env variable "{env_var}"') + return str(dll_path) + except Exception: + pass + + # Adjust executable_name for Windows if needed + if os.name == 'nt' and not executable_name.lower().endswith('.exe'): + executable_name += '.exe' + + # Search PATH for all pwsh executables + paths = os.environ.get('PATH', '').split(os.pathsep) + candidates = [] + for p in paths: + exe_path = Path(p) / executable_name + if exe_path.is_file() and os.access(str(exe_path), os.X_OK): + try: + completed = subprocess.run( + [str(exe_path), '--version'], + capture_output=True, + text=True, + timeout=5, + ) + if completed.returncode == 0: + ver = parse_version(completed.stdout) + if ver: + candidates.append((ver, exe_path.resolve())) + except Exception: + pass + + # Sort candidates by version descending + candidates.sort(key=lambda x: x[0], reverse=True) + + for ver, exe_path in candidates: + if ver >= min_version: + dll_path = exe_path.parent / dll_name + if dll_path.is_file(): + return str(dll_path) + + return None + + # Attempt to load the PowerShell SDK assembly only if clr and System loaded ps_sdk_path = None try: - # Prioritize PowerShell 7+ if available (adjust path if necessary) - pwsh7_path = ( - Path(os.environ.get('ProgramFiles', 'C:\\Program Files')) - / 'PowerShell' - / '7' - / 'System.Management.Automation.dll' - ) - if pwsh7_path.exists(): - ps_sdk_path = str(pwsh7_path) + # Attempt primary detection via helper function + ps_sdk_path = find_latest_pwsh_sdk_path() + if ps_sdk_path: clr.AddReference(ps_sdk_path) - logger.info(f'Loaded PowerShell SDK (Core): {ps_sdk_path}') + logger.info(f'Loaded PowerShell SDK dynamically detected: {ps_sdk_path}') else: - # Fallback to Windows PowerShell 5.1 bundled with Windows - winps_path = ( - Path(os.environ.get('SystemRoot', 'C:\\Windows')) - / 'System32' - / 'WindowsPowerShell' - / 'v1.0' + pwsh7_path = ( + Path(os.environ.get('ProgramFiles', 'C:\\Program Files')) + / 'PowerShell' + / '7' / 'System.Management.Automation.dll' ) - if winps_path.exists(): - ps_sdk_path = str(winps_path) + if pwsh7_path.exists(): + ps_sdk_path = str(pwsh7_path) clr.AddReference(ps_sdk_path) - logger.debug(f'Loaded PowerShell SDK (Desktop): {ps_sdk_path}') + logger.info(f'Loaded PowerShell SDK (Core): {ps_sdk_path}') else: - # Last resort: try loading by assembly name (might work if in GAC or path) - clr.AddReference('System.Management.Automation') - logger.info( - 'Attempted to load PowerShell SDK by name (System.Management.Automation)' + # Fallback to Windows PowerShell 5.1 bundled with Windows + winps_path = ( + Path(os.environ.get('SystemRoot', 'C:\\Windows')) + / 'System32' + / 'WindowsPowerShell' + / 'v1.0' + / 'System.Management.Automation.dll' ) + if winps_path.exists(): + ps_sdk_path = str(winps_path) + clr.AddReference(ps_sdk_path) + logger.debug(f'Loaded PowerShell SDK (Desktop): {ps_sdk_path}') + else: + # Last resort: try loading by assembly name (might work if in GAC or path) + clr.AddReference('System.Management.Automation') + logger.info( + 'Attempted to load PowerShell SDK by name (System.Management.Automation)' + ) from System.Management.Automation import JobState, PowerShell from System.Management.Automation.Language import Parser