|
@@ -1,8 +1,9 @@
|
|
|
|
|
+import atexit
|
|
|
import os
|
|
import os
|
|
|
-from pathlib import Path
|
|
|
|
|
import tempfile
|
|
import tempfile
|
|
|
import threading
|
|
import threading
|
|
|
from functools import lru_cache
|
|
from functools import lru_cache
|
|
|
|
|
+from pathlib import Path
|
|
|
from typing import Callable
|
|
from typing import Callable
|
|
|
from zipfile import ZipFile
|
|
from zipfile import ZipFile
|
|
|
|
|
|
|
@@ -35,6 +36,7 @@ from openhands.events.serialization import event_to_dict, observation_from_dict
|
|
|
from openhands.events.serialization.action import ACTION_TYPE_TO_CLASS
|
|
from openhands.events.serialization.action import ACTION_TYPE_TO_CLASS
|
|
|
from openhands.runtime.base import Runtime
|
|
from openhands.runtime.base import Runtime
|
|
|
from openhands.runtime.builder import DockerRuntimeBuilder
|
|
from openhands.runtime.builder import DockerRuntimeBuilder
|
|
|
|
|
+from openhands.runtime.impl.eventstream.containers import remove_all_containers
|
|
|
from openhands.runtime.plugins import PluginRequirement
|
|
from openhands.runtime.plugins import PluginRequirement
|
|
|
from openhands.runtime.utils import find_available_tcp_port
|
|
from openhands.runtime.utils import find_available_tcp_port
|
|
|
from openhands.runtime.utils.request import send_request
|
|
from openhands.runtime.utils.request import send_request
|
|
@@ -42,6 +44,15 @@ from openhands.runtime.utils.runtime_build import build_runtime_image
|
|
|
from openhands.utils.async_utils import call_sync_from_async
|
|
from openhands.utils.async_utils import call_sync_from_async
|
|
|
from openhands.utils.tenacity_stop import stop_if_should_exit
|
|
from openhands.utils.tenacity_stop import stop_if_should_exit
|
|
|
|
|
|
|
|
|
|
+CONTAINER_NAME_PREFIX = 'openhands-runtime-'
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def remove_all_runtime_containers():
|
|
|
|
|
+ remove_all_containers(CONTAINER_NAME_PREFIX)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+atexit.register(remove_all_runtime_containers)
|
|
|
|
|
+
|
|
|
|
|
|
|
|
class LogBuffer:
|
|
class LogBuffer:
|
|
|
"""Synchronous buffer for Docker container logs.
|
|
"""Synchronous buffer for Docker container logs.
|
|
@@ -114,8 +125,6 @@ class EventStreamRuntime(Runtime):
|
|
|
env_vars (dict[str, str] | None, optional): Environment variables to set. Defaults to None.
|
|
env_vars (dict[str, str] | None, optional): Environment variables to set. Defaults to None.
|
|
|
"""
|
|
"""
|
|
|
|
|
|
|
|
- container_name_prefix = 'openhands-runtime-'
|
|
|
|
|
-
|
|
|
|
|
# Need to provide this method to allow inheritors to init the Runtime
|
|
# Need to provide this method to allow inheritors to init the Runtime
|
|
|
# without initting the EventStreamRuntime.
|
|
# without initting the EventStreamRuntime.
|
|
|
def init_base_runtime(
|
|
def init_base_runtime(
|
|
@@ -158,7 +167,7 @@ class EventStreamRuntime(Runtime):
|
|
|
self.docker_client: docker.DockerClient = self._init_docker_client()
|
|
self.docker_client: docker.DockerClient = self._init_docker_client()
|
|
|
self.base_container_image = self.config.sandbox.base_container_image
|
|
self.base_container_image = self.config.sandbox.base_container_image
|
|
|
self.runtime_container_image = self.config.sandbox.runtime_container_image
|
|
self.runtime_container_image = self.config.sandbox.runtime_container_image
|
|
|
- self.container_name = self.container_name_prefix + sid
|
|
|
|
|
|
|
+ self.container_name = CONTAINER_NAME_PREFIX + sid
|
|
|
self.container = None
|
|
self.container = None
|
|
|
self.action_semaphore = threading.Semaphore(1) # Ensure one action at a time
|
|
self.action_semaphore = threading.Semaphore(1) # Ensure one action at a time
|
|
|
|
|
|
|
@@ -173,10 +182,6 @@ class EventStreamRuntime(Runtime):
|
|
|
f'Installing extra user-provided dependencies in the runtime image: {self.config.sandbox.runtime_extra_deps}',
|
|
f'Installing extra user-provided dependencies in the runtime image: {self.config.sandbox.runtime_extra_deps}',
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
- self.skip_container_logs = (
|
|
|
|
|
- os.environ.get('SKIP_CONTAINER_LOGS', 'false').lower() == 'true'
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
self.init_base_runtime(
|
|
self.init_base_runtime(
|
|
|
config,
|
|
config,
|
|
|
event_stream,
|
|
event_stream,
|
|
@@ -189,7 +194,15 @@ class EventStreamRuntime(Runtime):
|
|
|
|
|
|
|
|
async def connect(self):
|
|
async def connect(self):
|
|
|
self.send_status_message('STATUS$STARTING_RUNTIME')
|
|
self.send_status_message('STATUS$STARTING_RUNTIME')
|
|
|
- if not self.attach_to_existing:
|
|
|
|
|
|
|
+ try:
|
|
|
|
|
+ await call_sync_from_async(self._attach_to_container)
|
|
|
|
|
+ except docker.errors.NotFound as e:
|
|
|
|
|
+ if self.attach_to_existing:
|
|
|
|
|
+ self.log(
|
|
|
|
|
+ 'error',
|
|
|
|
|
+ f'Container {self.container_name} not found.',
|
|
|
|
|
+ )
|
|
|
|
|
+ raise e
|
|
|
if self.runtime_container_image is None:
|
|
if self.runtime_container_image is None:
|
|
|
if self.base_container_image is None:
|
|
if self.base_container_image is None:
|
|
|
raise ValueError(
|
|
raise ValueError(
|
|
@@ -210,13 +223,12 @@ class EventStreamRuntime(Runtime):
|
|
|
await call_sync_from_async(self._init_container)
|
|
await call_sync_from_async(self._init_container)
|
|
|
self.log('info', f'Container started: {self.container_name}')
|
|
self.log('info', f'Container started: {self.container_name}')
|
|
|
|
|
|
|
|
- else:
|
|
|
|
|
- await call_sync_from_async(self._attach_to_container)
|
|
|
|
|
-
|
|
|
|
|
if not self.attach_to_existing:
|
|
if not self.attach_to_existing:
|
|
|
self.log('info', f'Waiting for client to become ready at {self.api_url}...')
|
|
self.log('info', f'Waiting for client to become ready at {self.api_url}...')
|
|
|
- self.send_status_message('STATUS$WAITING_FOR_CLIENT')
|
|
|
|
|
|
|
+ self.send_status_message('STATUS$WAITING_FOR_CLIENT')
|
|
|
|
|
+
|
|
|
await call_sync_from_async(self._wait_until_alive)
|
|
await call_sync_from_async(self._wait_until_alive)
|
|
|
|
|
+
|
|
|
if not self.attach_to_existing:
|
|
if not self.attach_to_existing:
|
|
|
self.log('info', 'Runtime is ready.')
|
|
self.log('info', 'Runtime is ready.')
|
|
|
|
|
|
|
@@ -227,7 +239,8 @@ class EventStreamRuntime(Runtime):
|
|
|
'debug',
|
|
'debug',
|
|
|
f'Container initialized with plugins: {[plugin.name for plugin in self.plugins]}',
|
|
f'Container initialized with plugins: {[plugin.name for plugin in self.plugins]}',
|
|
|
)
|
|
)
|
|
|
- self.send_status_message(' ')
|
|
|
|
|
|
|
+ if not self.attach_to_existing:
|
|
|
|
|
+ self.send_status_message(' ')
|
|
|
|
|
|
|
|
@staticmethod
|
|
@staticmethod
|
|
|
@lru_cache(maxsize=1)
|
|
@lru_cache(maxsize=1)
|
|
@@ -332,13 +345,12 @@ class EventStreamRuntime(Runtime):
|
|
|
self.log('debug', f'Container started. Server url: {self.api_url}')
|
|
self.log('debug', f'Container started. Server url: {self.api_url}')
|
|
|
self.send_status_message('STATUS$CONTAINER_STARTED')
|
|
self.send_status_message('STATUS$CONTAINER_STARTED')
|
|
|
except docker.errors.APIError as e:
|
|
except docker.errors.APIError as e:
|
|
|
- # check 409 error
|
|
|
|
|
if '409' in str(e):
|
|
if '409' in str(e):
|
|
|
self.log(
|
|
self.log(
|
|
|
'warning',
|
|
'warning',
|
|
|
f'Container {self.container_name} already exists. Removing...',
|
|
f'Container {self.container_name} already exists. Removing...',
|
|
|
)
|
|
)
|
|
|
- self._close_containers(rm_all_containers=True)
|
|
|
|
|
|
|
+ remove_all_containers(self.container_name)
|
|
|
return self._init_container()
|
|
return self._init_container()
|
|
|
|
|
|
|
|
else:
|
|
else:
|
|
@@ -414,42 +426,18 @@ class EventStreamRuntime(Runtime):
|
|
|
Parameters:
|
|
Parameters:
|
|
|
- rm_all_containers (bool): Whether to remove all containers with the 'openhands-sandbox-' prefix
|
|
- rm_all_containers (bool): Whether to remove all containers with the 'openhands-sandbox-' prefix
|
|
|
"""
|
|
"""
|
|
|
-
|
|
|
|
|
if self.log_buffer:
|
|
if self.log_buffer:
|
|
|
self.log_buffer.close()
|
|
self.log_buffer.close()
|
|
|
|
|
|
|
|
if self.session:
|
|
if self.session:
|
|
|
self.session.close()
|
|
self.session.close()
|
|
|
|
|
|
|
|
- if self.attach_to_existing:
|
|
|
|
|
|
|
+ if self.config.sandbox.keep_runtime_alive or self.attach_to_existing:
|
|
|
return
|
|
return
|
|
|
- self._close_containers(rm_all_containers)
|
|
|
|
|
-
|
|
|
|
|
- def _close_containers(self, rm_all_containers: bool = True):
|
|
|
|
|
- try:
|
|
|
|
|
- containers = self.docker_client.containers.list(all=True)
|
|
|
|
|
- for container in containers:
|
|
|
|
|
- try:
|
|
|
|
|
- # If the app doesn't shut down properly, it can leave runtime containers on the system. This ensures
|
|
|
|
|
- # that all 'openhands-sandbox-' containers are removed as well.
|
|
|
|
|
- if rm_all_containers and container.name.startswith(
|
|
|
|
|
- self.container_name_prefix
|
|
|
|
|
- ):
|
|
|
|
|
- container.remove(force=True)
|
|
|
|
|
- elif container.name == self.container_name:
|
|
|
|
|
- if not self.skip_container_logs:
|
|
|
|
|
- logs = container.logs(tail=1000).decode('utf-8')
|
|
|
|
|
- self.log(
|
|
|
|
|
- 'debug',
|
|
|
|
|
- f'==== Container logs on close ====\n{logs}\n==== End of container logs ====',
|
|
|
|
|
- )
|
|
|
|
|
- container.remove(force=True)
|
|
|
|
|
- except docker.errors.APIError:
|
|
|
|
|
- pass
|
|
|
|
|
- except docker.errors.NotFound:
|
|
|
|
|
- pass
|
|
|
|
|
- except docker.errors.NotFound: # yes, this can happen!
|
|
|
|
|
- pass
|
|
|
|
|
|
|
+ close_prefix = (
|
|
|
|
|
+ CONTAINER_NAME_PREFIX if rm_all_containers else self.container_name
|
|
|
|
|
+ )
|
|
|
|
|
+ remove_all_containers(close_prefix)
|
|
|
|
|
|
|
|
def run_action(self, action: Action) -> Observation:
|
|
def run_action(self, action: Action) -> Observation:
|
|
|
if isinstance(action, FileEditAction):
|
|
if isinstance(action, FileEditAction):
|