mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
feat: Add Windows PowerShell support to CLI runtime (#9211)
Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
parent
081880248c
commit
5d69e606eb
@ -133,13 +133,66 @@ This guide provides step-by-step instructions for running OpenHands on a Windows
|
||||
|
||||
> **Note**: If you're running the frontend in development mode (using `npm run dev`), use port 3001 instead: `http://localhost:3001`
|
||||
|
||||
## Installing and Running the CLI
|
||||
|
||||
To install and run the OpenHands CLI on Windows without WSL, follow these steps:
|
||||
|
||||
### 1. Install uv (Python Package Manager)
|
||||
|
||||
Open PowerShell as Administrator and run:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
|
||||
```
|
||||
|
||||
### 2. Install .NET SDK (Required)
|
||||
|
||||
The OpenHands CLI **requires** the .NET Core runtime for PowerShell integration. Without it, the CLI will fail to start with a `coreclr` error. Install the .NET SDK which includes the runtime:
|
||||
|
||||
```powershell
|
||||
winget install Microsoft.DotNet.SDK.8
|
||||
```
|
||||
|
||||
Alternatively, you can download and install the .NET SDK from the [official Microsoft website](https://dotnet.microsoft.com/download).
|
||||
|
||||
After installation, restart your PowerShell session to ensure the environment variables are updated.
|
||||
|
||||
### 3. Install and Run OpenHands
|
||||
|
||||
After installing the prerequisites, you can install and run OpenHands with:
|
||||
|
||||
```powershell
|
||||
uvx --python 3.12 --from openhands-ai openhands
|
||||
```
|
||||
|
||||
### Troubleshooting CLI Issues
|
||||
|
||||
#### CoreCLR Error
|
||||
|
||||
If you encounter an error like `Failed to load CoreCLR` or `pythonnet.load('coreclr')` when running OpenHands CLI, this indicates that the .NET Core runtime is missing or not properly configured. To fix this:
|
||||
|
||||
1. Install the .NET SDK as described in step 2 above
|
||||
2. Verify that your system PATH includes the .NET SDK directories
|
||||
3. Restart your PowerShell session completely after installing the .NET SDK
|
||||
4. Make sure you're using PowerShell 7 (pwsh) rather than Windows PowerShell
|
||||
|
||||
To verify your .NET installation, run:
|
||||
|
||||
```powershell
|
||||
dotnet --info
|
||||
```
|
||||
|
||||
This should display information about your installed .NET SDKs and runtimes. If this command fails, the .NET SDK is not properly installed or not in your PATH.
|
||||
|
||||
If the issue persists after installing the .NET SDK, try installing the specific .NET Runtime version 6.0 or later from the [.NET download page](https://dotnet.microsoft.com/download).
|
||||
|
||||
## Limitations on Windows
|
||||
|
||||
When running OpenHands on Windows without WSL or Docker, be aware of the following limitations:
|
||||
|
||||
1. **Browser Tool Not Supported**: The browser tool is not currently supported on Windows.
|
||||
|
||||
2. **.NET Core Requirement**: The PowerShell integration requires .NET Core Runtime to be installed. If .NET Core is not available, OpenHands will automatically fall back to a more limited PowerShell implementation with reduced functionality.
|
||||
2. **.NET Core Requirement**: The PowerShell integration requires .NET Core Runtime to be installed. The CLI implementation attempts to load the CoreCLR at startup with `pythonnet.load('coreclr')` and will fail with an error if .NET Core is not properly installed.
|
||||
|
||||
3. **Interactive Shell Commands**: Some interactive shell commands may not work as expected. The PowerShell session implementation has limitations compared to the bash session used on Linux/macOS.
|
||||
|
||||
|
||||
@ -9,11 +9,12 @@ import select
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
from typing import TYPE_CHECKING, Any, Callable
|
||||
|
||||
from binaryornot.check import is_binary
|
||||
from openhands_aci.editor.editor import OHEditor
|
||||
@ -51,6 +52,41 @@ from openhands.runtime.base import Runtime
|
||||
from openhands.runtime.plugins import PluginRequirement
|
||||
from openhands.runtime.runtime_status import RuntimeStatus
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from openhands.runtime.utils.windows_bash import WindowsPowershellSession
|
||||
|
||||
# Import Windows PowerShell support if on Windows
|
||||
if sys.platform == 'win32':
|
||||
try:
|
||||
from openhands.runtime.utils.windows_bash import WindowsPowershellSession
|
||||
from openhands.runtime.utils.windows_exceptions import DotNetMissingError
|
||||
except (ImportError, DotNetMissingError) as err:
|
||||
# Print a user-friendly error message without stack trace
|
||||
friendly_message = """
|
||||
ERROR: PowerShell and .NET SDK are required but not properly configured
|
||||
|
||||
The .NET SDK and PowerShell are required for OpenHands CLI on Windows.
|
||||
PowerShell integration cannot function without .NET Core.
|
||||
|
||||
Please install the .NET SDK by following the instructions at:
|
||||
https://docs.all-hands.dev/usage/windows-without-wsl
|
||||
|
||||
After installing .NET SDK, restart your terminal and try again.
|
||||
"""
|
||||
print(friendly_message, file=sys.stderr)
|
||||
logger.error(
|
||||
f'Windows runtime initialization failed: {type(err).__name__}: {str(err)}'
|
||||
)
|
||||
if (
|
||||
isinstance(err, DotNetMissingError)
|
||||
and hasattr(err, 'details')
|
||||
and err.details
|
||||
):
|
||||
logger.debug(f'Details: {err.details}')
|
||||
|
||||
# Exit the program with an error code
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class CLIRuntime(Runtime):
|
||||
"""
|
||||
@ -119,6 +155,10 @@ class CLIRuntime(Runtime):
|
||||
self.file_editor = OHEditor(workspace_root=self._workspace_path)
|
||||
self._shell_stream_callback: Callable[[str], None] | None = None
|
||||
|
||||
# Initialize PowerShell session on Windows
|
||||
self._is_windows = sys.platform == 'win32'
|
||||
self._powershell_session: WindowsPowershellSession | None = None
|
||||
|
||||
logger.warning(
|
||||
'Initializing CLIRuntime. WARNING: NO SANDBOX IS USED. '
|
||||
'This runtime executes commands directly on the local system. '
|
||||
@ -135,6 +175,15 @@ class CLIRuntime(Runtime):
|
||||
# Change to the workspace directory
|
||||
os.chdir(self._workspace_path)
|
||||
|
||||
# Initialize PowerShell session if on Windows
|
||||
if self._is_windows:
|
||||
self._powershell_session = WindowsPowershellSession(
|
||||
work_dir=self._workspace_path,
|
||||
username=None, # Use current user
|
||||
no_change_timeout_seconds=30,
|
||||
max_memory_mb=None,
|
||||
)
|
||||
|
||||
if not self.attach_to_existing:
|
||||
await asyncio.to_thread(self.setup_initial_env)
|
||||
|
||||
@ -241,6 +290,40 @@ class CLIRuntime(Runtime):
|
||||
except Exception as e:
|
||||
logger.error(f'Error: {e}')
|
||||
|
||||
def _execute_powershell_command(
|
||||
self, command: str, timeout: float
|
||||
) -> CmdOutputObservation | ErrorObservation:
|
||||
"""
|
||||
Execute a command using PowerShell session on Windows.
|
||||
Args:
|
||||
command: The command to execute
|
||||
timeout: Timeout in seconds for the command
|
||||
Returns:
|
||||
CmdOutputObservation containing the complete output and exit code
|
||||
"""
|
||||
if self._powershell_session is None:
|
||||
return ErrorObservation(
|
||||
content='PowerShell session is not available.',
|
||||
error_id='POWERSHELL_SESSION_ERROR',
|
||||
)
|
||||
|
||||
try:
|
||||
# Create a CmdRunAction for the PowerShell session
|
||||
from openhands.events.action import CmdRunAction
|
||||
|
||||
ps_action = CmdRunAction(command=command)
|
||||
ps_action.set_hard_timeout(timeout)
|
||||
|
||||
# Execute the command using the PowerShell session
|
||||
return self._powershell_session.execute(ps_action)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Error executing PowerShell command "{command}": {e}')
|
||||
return ErrorObservation(
|
||||
content=f'Error executing PowerShell command "{command}": {str(e)}',
|
||||
error_id='POWERSHELL_EXECUTION_ERROR',
|
||||
)
|
||||
|
||||
def _execute_shell_command(
|
||||
self, command: str, timeout: float
|
||||
) -> CmdOutputObservation:
|
||||
@ -378,9 +461,16 @@ class CLIRuntime(Runtime):
|
||||
logger.debug(
|
||||
f'Running command in CLIRuntime: "{action.command}" with effective timeout: {effective_timeout}s'
|
||||
)
|
||||
return self._execute_shell_command(
|
||||
action.command, timeout=effective_timeout
|
||||
)
|
||||
|
||||
# Use PowerShell on Windows if available, otherwise use subprocess
|
||||
if self._is_windows and self._powershell_session is not None:
|
||||
return self._execute_powershell_command(
|
||||
action.command, timeout=effective_timeout
|
||||
)
|
||||
else:
|
||||
return self._execute_shell_command(
|
||||
action.command, timeout=effective_timeout
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f'Error in CLIRuntime.run for command "{action.command}": {str(e)}'
|
||||
@ -737,6 +827,16 @@ class CLIRuntime(Runtime):
|
||||
raise RuntimeError(f'Error creating zip file: {str(e)}')
|
||||
|
||||
def close(self) -> None:
|
||||
# Clean up PowerShell session if it exists
|
||||
if self._powershell_session is not None:
|
||||
try:
|
||||
self._powershell_session.close()
|
||||
logger.debug('PowerShell session closed successfully.')
|
||||
except Exception as e:
|
||||
logger.warning(f'Error closing PowerShell session: {e}')
|
||||
finally:
|
||||
self._powershell_session = None
|
||||
|
||||
self._runtime_initialized = False
|
||||
super().close()
|
||||
|
||||
|
||||
@ -21,21 +21,31 @@ from openhands.events.observation.commands import (
|
||||
CmdOutputObservation,
|
||||
)
|
||||
from openhands.runtime.utils.bash_constants import TIMEOUT_MESSAGE_TEMPLATE
|
||||
from openhands.runtime.utils.windows_exceptions import DotNetMissingError
|
||||
from openhands.utils.shutdown_listener import should_continue
|
||||
|
||||
pythonnet.load('coreclr')
|
||||
logger.info("Successfully called pythonnet.load('coreclr')")
|
||||
|
||||
# Now that pythonnet is initialized, import clr and System
|
||||
try:
|
||||
import clr
|
||||
pythonnet.load('coreclr')
|
||||
logger.info("Successfully called pythonnet.load('coreclr')")
|
||||
|
||||
logger.debug(f'Imported clr module from: {clr.__file__}')
|
||||
# Load System assembly *after* pythonnet is initialized
|
||||
clr.AddReference('System')
|
||||
import System
|
||||
except Exception as clr_sys_ex:
|
||||
raise RuntimeError(f'FATAL: Failed to import clr or System. Error: {clr_sys_ex}')
|
||||
# Now that pythonnet is initialized, import clr and System
|
||||
try:
|
||||
import clr
|
||||
|
||||
logger.debug(f'Imported clr module from: {clr.__file__}')
|
||||
# Load System assembly *after* pythonnet is initialized
|
||||
clr.AddReference('System')
|
||||
import System
|
||||
except Exception as clr_sys_ex:
|
||||
error_msg = 'Failed to import .NET components.'
|
||||
details = str(clr_sys_ex)
|
||||
logger.error(f'{error_msg} Details: {details}')
|
||||
raise DotNetMissingError(error_msg, details)
|
||||
except Exception as coreclr_ex:
|
||||
error_msg = 'Failed to load CoreCLR.'
|
||||
details = str(coreclr_ex)
|
||||
logger.error(f'{error_msg} Details: {details}')
|
||||
raise DotNetMissingError(error_msg, details)
|
||||
|
||||
# Attempt to load the PowerShell SDK assembly only if clr and System loaded
|
||||
ps_sdk_path = None
|
||||
@ -78,9 +88,10 @@ try:
|
||||
RunspaceState,
|
||||
)
|
||||
except Exception as e:
|
||||
raise RuntimeError(
|
||||
f'FATAL: Failed to load PowerShell SDK components. Error: {e}. Check pythonnet installation and .NET Runtime compatibility. Path searched: {ps_sdk_path}'
|
||||
)
|
||||
error_msg = 'Failed to load PowerShell SDK components.'
|
||||
details = f'{str(e)} (Path searched: {ps_sdk_path})'
|
||||
logger.error(f'{error_msg} Details: {details}')
|
||||
raise DotNetMissingError(error_msg, details)
|
||||
|
||||
|
||||
class WindowsPowershellSession:
|
||||
@ -115,9 +126,11 @@ class WindowsPowershellSession:
|
||||
|
||||
if PowerShell is None: # Check if SDK loading failed during module import
|
||||
# Logged critical error during import, just raise here to prevent instantiation
|
||||
raise RuntimeError(
|
||||
'PowerShell SDK (System.Management.Automation.dll) could not be loaded. Cannot initialize WindowsPowershellSession.'
|
||||
error_msg = (
|
||||
'PowerShell SDK (System.Management.Automation.dll) could not be loaded.'
|
||||
)
|
||||
logger.error(error_msg)
|
||||
raise DotNetMissingError(error_msg)
|
||||
|
||||
self.work_dir = os.path.abspath(work_dir)
|
||||
self.username = username
|
||||
|
||||
15
openhands/runtime/utils/windows_exceptions.py
Normal file
15
openhands/runtime/utils/windows_exceptions.py
Normal file
@ -0,0 +1,15 @@
|
||||
"""
|
||||
Custom exceptions for Windows-specific runtime issues.
|
||||
"""
|
||||
|
||||
|
||||
class DotNetMissingError(Exception):
|
||||
"""
|
||||
Exception raised when .NET SDK or CoreCLR is missing or cannot be loaded.
|
||||
This is used to provide a cleaner error message to users without a full stack trace.
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, details: str | None = None):
|
||||
self.message = message
|
||||
self.details = details
|
||||
super().__init__(message)
|
||||
Loading…
x
Reference in New Issue
Block a user