Integrate copy_to in E2BBox (#1298)

* initialize plugin definition

* initialize plugin definition

* simplify mixin

* further improve plugin mixin

* add cache dir for pip

* support clean up cache

* add script for setup jupyter and execution server

* integrate JupyterRequirement to ssh_box

* source bashrc at the end of plugin load

* add execute_cli that accept code via stdin

* make JUPYTER_EXEC_SERVER_PORT configurable via env var

* increase background cmd sleep time

* Update opendevin/sandbox/plugins/mixin.py

Co-authored-by: Robert Brennan <accounts@rbren.io>

* add mixin to base class

* make jupyter requirement a dataclass

* source plugins only when >0 requirements

* add `sandbox_plugins` for each agent & have controller take care of it

* update build.sh to make logs available in /opendevin/logs

* switch to use config for lib and cache dir

* fix permission issue with /workspace

* use python to implement execute_cli to avoid stdin escape issue

* wait until jupyter is avaialble

* support plugin via copying instead of mounting

* Fix linter issue

---------

Co-authored-by: Xingyao Wang <xingyao6@illinois.edu>
Co-authored-by: Robert Brennan <accounts@rbren.io>
This commit is contained in:
Vasek Mlejnsky
2024-04-23 04:55:27 -07:00
committed by GitHub
parent 0a2e90dece
commit a2a86e84f0

View File

@@ -1,3 +1,6 @@
import os
import tarfile
from glob import glob
from typing import Dict, Tuple
from e2b import Sandbox as E2BSandbox
from e2b.sandbox.exception import (
@@ -15,6 +18,7 @@ class E2BBox(Sandbox):
closed = False
cur_background_id = 0
background_commands: Dict[int, Process] = {}
_cwd: str = '/home/user'
def __init__(
self,
@@ -27,7 +31,7 @@ class E2BBox(Sandbox):
# It's possible to stream stdout and stderr from sandbox and from each process
on_stderr=lambda x: logger.info(f'E2B sandbox stderr: {x}'),
on_stdout=lambda x: logger.info(f'E2B sandbox stdout: {x}'),
cwd='/home/user', # Default workdir inside sandbox
cwd=self._cwd, # Default workdir inside sandbox
)
self.timeout = timeout
logger.info(f'Started E2B sandbox with ID "{self.sandbox.id}"')
@@ -36,6 +40,23 @@ class E2BBox(Sandbox):
def filesystem(self):
return self.sandbox.filesystem
def _archive(self, host_src: str, recursive: bool = False):
if recursive:
assert os.path.isdir(host_src), 'Source must be a directory when recursive is True'
files = glob(host_src + '/**/*', recursive=True)
srcname = os.path.basename(host_src)
tar_filename = os.path.join(os.path.dirname(host_src), srcname + '.tar')
with tarfile.open(tar_filename, mode='w') as tar:
for file in files:
tar.add(file, arcname=os.path.relpath(file, os.path.dirname(host_src)))
else:
assert os.path.isfile(host_src), 'Source must be a file when recursive is False'
srcname = os.path.basename(host_src)
tar_filename = os.path.join(os.path.dirname(host_src), srcname + '.tar')
with tarfile.open(tar_filename, mode='w') as tar:
tar.add(host_src, arcname=srcname)
return tar_filename
# TODO: This won't work if we didn't wait for the background process to finish
def read_logs(self, process_id: int) -> str:
proc = self.background_commands.get(process_id)
@@ -62,8 +83,28 @@ class E2BBox(Sandbox):
return process_output.exit_code, logs_str
def copy_to(self, host_src: str, sandbox_dest: str, recursive: bool = False):
# FIXME
raise NotImplementedError('Copying files to E2B sandbox is not implemented yet')
"""Copies a local file or directory to the sandbox."""
tar_filename = self._archive(host_src, recursive)
# Prepend the sandbox destination with our sandbox cwd
sandbox_dest = os.path.join(self._cwd, sandbox_dest.lstrip('/'))
with open(tar_filename, 'rb') as tar_file:
# Upload the archive to /home/user (default destination that always exists)
uploaded_path = self.sandbox.upload_file(tar_file)
# Check if sandbox_dest exists. If not, create it.
process = self.sandbox.process.start_and_wait(f'test -d {sandbox_dest}')
if process.exit_code != 0:
self.sandbox.filesystem.make_dir(sandbox_dest)
# Extract the archive into the destination and delete the archive
process = self.sandbox.process.start_and_wait(f'sudo tar -xf {uploaded_path} -C {sandbox_dest} && sudo rm {uploaded_path}')
if process.exit_code != 0:
raise Exception(f'Failed to extract {uploaded_path} to {sandbox_dest}: {process.stderr}')
# Delete the local archive
os.remove(tar_filename)
def execute_in_background(self, cmd: str) -> Process:
process = self.sandbox.process.start(cmd)