mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-01-09 14:57:59 -05:00
feat: Add Windows PowerShell support to CLI runtime (#9211)
Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user