feat: Add Windows PowerShell support to CLI runtime (#9211)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Graham Neubig
2025-06-22 20:17:40 -04:00
committed by GitHub
parent 081880248c
commit 5d69e606eb
4 changed files with 202 additions and 21 deletions

View File

@@ -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()