mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-25 21:36:52 +08:00
Co-authored-by: openhands <openhands@all-hands.dev> Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com> Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
91 lines
3.2 KiB
Python
91 lines
3.2 KiB
Python
import asyncio
|
|
import logging
|
|
from typing import AsyncGenerator
|
|
|
|
import docker
|
|
from fastapi import Request
|
|
from pydantic import Field
|
|
|
|
from openhands.app_server.errors import SandboxError
|
|
from openhands.app_server.sandbox.preset_sandbox_spec_service import (
|
|
PresetSandboxSpecService,
|
|
)
|
|
from openhands.app_server.sandbox.sandbox_spec_models import (
|
|
SandboxSpecInfo,
|
|
)
|
|
from openhands.app_server.sandbox.sandbox_spec_service import (
|
|
AGENT_SERVER_VERSION,
|
|
SandboxSpecService,
|
|
SandboxSpecServiceInjector,
|
|
)
|
|
from openhands.app_server.services.injector import InjectorState
|
|
|
|
_global_docker_client: docker.DockerClient | None = None
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
def get_docker_client() -> docker.DockerClient:
|
|
global _global_docker_client
|
|
if _global_docker_client is None:
|
|
_global_docker_client = docker.from_env()
|
|
return _global_docker_client
|
|
|
|
|
|
def get_default_sandbox_specs():
|
|
return [
|
|
SandboxSpecInfo(
|
|
id=f'ghcr.io/all-hands-ai/agent-server:{AGENT_SERVER_VERSION[:7]}-python',
|
|
command=['--port', '8000'],
|
|
initial_env={
|
|
'OPENVSCODE_SERVER_ROOT': '/openhands/.openvscode-server',
|
|
'OH_ENABLE_VNC': '0',
|
|
'LOG_JSON': 'true',
|
|
'OH_CONVERSATIONS_PATH': '/home/openhands/conversations',
|
|
'OH_BASH_EVENTS_DIR': '/home/openhands/bash_events',
|
|
},
|
|
working_dir='/home/openhands/workspace',
|
|
)
|
|
]
|
|
|
|
|
|
class DockerSandboxSpecServiceInjector(SandboxSpecServiceInjector):
|
|
specs: list[SandboxSpecInfo] = Field(
|
|
default_factory=get_default_sandbox_specs,
|
|
description='Preset list of sandbox specs',
|
|
)
|
|
pull_if_missing: bool = Field(
|
|
default=True,
|
|
description=(
|
|
'Flag indicating that any missing specs should be pulled from '
|
|
'remote repositories.'
|
|
),
|
|
)
|
|
|
|
async def inject(
|
|
self, state: InjectorState, request: Request | None = None
|
|
) -> AsyncGenerator[SandboxSpecService, None]:
|
|
if self.pull_if_missing:
|
|
await self.pull_missing_specs()
|
|
# Prevent repeated checks - more efficient but it does mean if you
|
|
# delete a docker image outside the app you need to restart
|
|
self.pull_if_missing = False
|
|
yield PresetSandboxSpecService(specs=self.specs)
|
|
|
|
async def pull_missing_specs(self):
|
|
await asyncio.gather(*[self.pull_spec_if_missing(spec) for spec in self.specs])
|
|
|
|
async def pull_spec_if_missing(self, spec: SandboxSpecInfo):
|
|
_logger.debug(f'Checking Docker Image: {spec.id}')
|
|
try:
|
|
docker_client = get_docker_client()
|
|
try:
|
|
docker_client.images.get(spec.id)
|
|
except docker.errors.ImageNotFound:
|
|
_logger.info(f'⬇️ Pulling Docker Image: {spec.id}')
|
|
# Pull in a background thread to prevent locking up the main runloop
|
|
loop = asyncio.get_running_loop()
|
|
await loop.run_in_executor(None, docker_client.images.pull, spec.id)
|
|
_logger.info(f'⬇️ Finished Pulling Docker Image: {spec.id}')
|
|
except docker.errors.APIError as exc:
|
|
raise SandboxError(f'Error Getting Docker Image: {spec.id}') from exc
|