"""GUI launcher for OpenHands CLI.""" import os import shutil import subprocess import sys from pathlib import Path from prompt_toolkit import print_formatted_text from prompt_toolkit.formatted_text import HTML from openhands_cli.locations import PERSISTENCE_DIR def _format_docker_command_for_logging(cmd: list[str]) -> str: """Format a Docker command for logging with grey color. Args: cmd (list[str]): The Docker command as a list of strings Returns: str: The formatted command string in grey HTML color """ cmd_str = ' '.join(cmd) return f'Running Docker command: {cmd_str}' def check_docker_requirements() -> bool: """Check if Docker is installed and running. Returns: bool: True if Docker is available and running, False otherwise. """ # Check if Docker is installed if not shutil.which('docker'): print_formatted_text( HTML('❌ Docker is not installed or not in PATH.') ) print_formatted_text( HTML( 'Please install Docker first: https://docs.docker.com/get-docker/' ) ) return False # Check if Docker daemon is running try: result = subprocess.run( ['docker', 'info'], capture_output=True, text=True, timeout=10 ) if result.returncode != 0: print_formatted_text( HTML('❌ Docker daemon is not running.') ) print_formatted_text( HTML('Please start Docker and try again.') ) return False except (subprocess.TimeoutExpired, subprocess.SubprocessError) as e: print_formatted_text( HTML('❌ Failed to check Docker status.') ) print_formatted_text(HTML(f'Error: {e}')) return False return True def ensure_config_dir_exists() -> Path: """Ensure the OpenHands configuration directory exists and return its path.""" path = Path(PERSISTENCE_DIR) path.mkdir(exist_ok=True, parents=True) return path def get_openhands_version() -> str: """Get the OpenHands version for Docker images. Returns: str: The version string to use for Docker images """ # For now, use 'latest' as the default version # In the future, this could be read from a version file or environment variable return os.environ.get('OPENHANDS_VERSION', 'latest') def launch_gui_server(mount_cwd: bool = False, gpu: bool = False) -> None: """Launch the OpenHands GUI server using Docker. Args: mount_cwd: If True, mount the current working directory into the container. gpu: If True, enable GPU support by mounting all GPUs into the container via nvidia-docker. """ print_formatted_text( HTML('🚀 Launching OpenHands GUI server...') ) print_formatted_text('') # Check Docker requirements if not check_docker_requirements(): sys.exit(1) # Ensure config directory exists config_dir = ensure_config_dir_exists() # Get the current version for the Docker image version = get_openhands_version() runtime_image = f'docker.openhands.dev/openhands/runtime:{version}-nikolaik' app_image = f'docker.openhands.dev/openhands/openhands:{version}' print_formatted_text(HTML('Pulling required Docker images...')) # Pull the runtime image first pull_cmd = ['docker', 'pull', runtime_image] print_formatted_text(HTML(_format_docker_command_for_logging(pull_cmd))) try: subprocess.run(pull_cmd, check=True) except subprocess.CalledProcessError: print_formatted_text( HTML('❌ Failed to pull runtime image.') ) sys.exit(1) print_formatted_text('') print_formatted_text( HTML('✅ Starting OpenHands GUI server...') ) print_formatted_text( HTML('The server will be available at: http://localhost:3000') ) print_formatted_text(HTML('Press Ctrl+C to stop the server.')) print_formatted_text('') # Build the Docker command docker_cmd = [ 'docker', 'run', '-it', '--rm', '--pull=always', '-e', f'SANDBOX_RUNTIME_CONTAINER_IMAGE={runtime_image}', '-e', 'LOG_ALL_EVENTS=true', '-v', '/var/run/docker.sock:/var/run/docker.sock', '-v', f'{config_dir}:/.openhands', ] # Add GPU support if requested if gpu: print_formatted_text( HTML('🖥️ Enabling GPU support via nvidia-docker...') ) # Add the --gpus all flag to enable all GPUs docker_cmd.insert(2, '--gpus') docker_cmd.insert(3, 'all') # Add environment variable to pass GPU support to sandbox containers docker_cmd.extend( [ '-e', 'SANDBOX_ENABLE_GPU=true', ] ) # Add current working directory mount if requested if mount_cwd: cwd = Path.cwd() # Following the documentation at https://docs.all-hands.dev/usage/runtimes/docker#connecting-to-your-filesystem docker_cmd.extend( [ '-e', f'SANDBOX_VOLUMES={cwd}:/workspace:rw', ] ) # Set user ID for Unix-like systems only if os.name != 'nt': # Not Windows try: user_id = subprocess.check_output(['id', '-u'], text=True).strip() docker_cmd.extend(['-e', f'SANDBOX_USER_ID={user_id}']) except (subprocess.CalledProcessError, FileNotFoundError): # If 'id' command fails or doesn't exist, skip setting user ID pass # Print the folder that will be mounted to inform the user print_formatted_text( HTML( f'📂 Mounting current directory: {cwd} to /workspace' ) ) docker_cmd.extend( [ '-p', '3000:3000', '--add-host', 'host.docker.internal:host-gateway', '--name', 'openhands-app', app_image, ] ) try: # Log and run the Docker command print_formatted_text(HTML(_format_docker_command_for_logging(docker_cmd))) subprocess.run(docker_cmd, check=True) except subprocess.CalledProcessError as e: print_formatted_text('') print_formatted_text( HTML('❌ Failed to start OpenHands GUI server.') ) print_formatted_text(HTML(f'Error: {e}')) sys.exit(1) except KeyboardInterrupt: print_formatted_text('') print_formatted_text( HTML('✓ OpenHands GUI server stopped successfully.') ) sys.exit(0)