sandbox.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. import copy
  2. import os
  3. import tarfile
  4. from glob import glob
  5. from e2b import Sandbox as E2BSandbox
  6. from e2b.sandbox.exception import TimeoutException
  7. from openhands.core.config import SandboxConfig
  8. from openhands.core.logger import openhands_logger as logger
  9. class E2BBox:
  10. closed = False
  11. _cwd: str = '/home/user'
  12. _env: dict[str, str] = {}
  13. is_initial_session: bool = True
  14. def __init__(
  15. self,
  16. config: SandboxConfig,
  17. e2b_api_key: str,
  18. template: str = 'openhands',
  19. ):
  20. self.config = copy.deepcopy(config)
  21. self.initialize_plugins: bool = config.initialize_plugins
  22. self.sandbox = E2BSandbox(
  23. api_key=e2b_api_key,
  24. template=template,
  25. # It's possible to stream stdout and stderr from sandbox and from each process
  26. on_stderr=lambda x: logger.debug(f'E2B sandbox stderr: {x}'),
  27. on_stdout=lambda x: logger.debug(f'E2B sandbox stdout: {x}'),
  28. cwd=self._cwd, # Default workdir inside sandbox
  29. )
  30. logger.debug(f'Started E2B sandbox with ID "{self.sandbox.id}"')
  31. @property
  32. def filesystem(self):
  33. return self.sandbox.filesystem
  34. def _archive(self, host_src: str, recursive: bool = False):
  35. if recursive:
  36. assert os.path.isdir(
  37. host_src
  38. ), 'Source must be a directory when recursive is True'
  39. files = glob(host_src + '/**/*', recursive=True)
  40. srcname = os.path.basename(host_src)
  41. tar_filename = os.path.join(os.path.dirname(host_src), srcname + '.tar')
  42. with tarfile.open(tar_filename, mode='w') as tar:
  43. for file in files:
  44. tar.add(
  45. file, arcname=os.path.relpath(file, os.path.dirname(host_src))
  46. )
  47. else:
  48. assert os.path.isfile(
  49. host_src
  50. ), 'Source must be a file when recursive is False'
  51. srcname = os.path.basename(host_src)
  52. tar_filename = os.path.join(os.path.dirname(host_src), srcname + '.tar')
  53. with tarfile.open(tar_filename, mode='w') as tar:
  54. tar.add(host_src, arcname=srcname)
  55. return tar_filename
  56. def execute(self, cmd: str, timeout: int | None = None) -> tuple[int, str]:
  57. timeout = timeout if timeout is not None else self.config.timeout
  58. process = self.sandbox.process.start(cmd, env_vars=self._env)
  59. try:
  60. process_output = process.wait(timeout=timeout)
  61. except TimeoutException:
  62. logger.debug('Command timed out, killing process...')
  63. process.kill()
  64. return -1, f'Command: "{cmd}" timed out'
  65. logs = [m.line for m in process_output.messages]
  66. logs_str = '\n'.join(logs)
  67. if process.exit_code is None:
  68. return -1, logs_str
  69. assert process_output.exit_code is not None
  70. return process_output.exit_code, logs_str
  71. def copy_to(self, host_src: str, sandbox_dest: str, recursive: bool = False):
  72. """Copies a local file or directory to the sandbox."""
  73. tar_filename = self._archive(host_src, recursive)
  74. # Prepend the sandbox destination with our sandbox cwd
  75. sandbox_dest = os.path.join(self._cwd, sandbox_dest.removeprefix('/'))
  76. with open(tar_filename, 'rb') as tar_file:
  77. # Upload the archive to /home/user (default destination that always exists)
  78. uploaded_path = self.sandbox.upload_file(tar_file)
  79. # Check if sandbox_dest exists. If not, create it.
  80. process = self.sandbox.process.start_and_wait(f'test -d {sandbox_dest}')
  81. if process.exit_code != 0:
  82. self.sandbox.filesystem.make_dir(sandbox_dest)
  83. # Extract the archive into the destination and delete the archive
  84. process = self.sandbox.process.start_and_wait(
  85. f'sudo tar -xf {uploaded_path} -C {sandbox_dest} && sudo rm {uploaded_path}'
  86. )
  87. if process.exit_code != 0:
  88. raise Exception(
  89. f'Failed to extract {uploaded_path} to {sandbox_dest}: {process.stderr}'
  90. )
  91. # Delete the local archive
  92. os.remove(tar_filename)
  93. def close(self):
  94. self.sandbox.close()
  95. def get_working_directory(self):
  96. return self.sandbox.cwd