local_box.py 4.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. import subprocess
  2. import atexit
  3. import os
  4. from typing import Tuple, Dict
  5. from opendevin.sandbox.sandbox import Sandbox
  6. from opendevin.sandbox.process import Process
  7. from opendevin.sandbox.docker.process import DockerProcess
  8. from opendevin import config
  9. from opendevin.schema.config import ConfigType
  10. # ===============================================================================
  11. # ** WARNING **
  12. #
  13. # This sandbox should only be used when OpenDevin is running inside a container
  14. #
  15. # Sandboxes are generally isolated so that they cannot affect the host machine.
  16. # This Sandbox implementation does not provide isolation, and can inadvertently
  17. # run dangerous commands on the host machine, potentially rendering the host
  18. # machine unusable.
  19. #
  20. # This sandbox is meant for use with OpenDevin Quickstart
  21. #
  22. # DO NOT USE THIS SANDBOX IN A PRODUCTION ENVIRONMENT
  23. # ===============================================================================
  24. class LocalBox(Sandbox):
  25. def __init__(self, timeout: int = 120):
  26. os.makedirs(config.get(ConfigType.WORKSPACE_BASE), exist_ok=True)
  27. self.timeout = timeout
  28. self.background_commands: Dict[int, Process] = {}
  29. self.cur_background_id = 0
  30. atexit.register(self.cleanup)
  31. def execute(self, cmd: str) -> Tuple[int, str]:
  32. try:
  33. completed_process = subprocess.run(
  34. cmd, shell=True, text=True, capture_output=True,
  35. timeout=self.timeout, cwd=config.get(ConfigType.WORKSPACE_BASE)
  36. )
  37. return completed_process.returncode, completed_process.stdout.strip()
  38. except subprocess.TimeoutExpired:
  39. return -1, 'Command timed out'
  40. def copy_to(self, host_src: str, sandbox_dest: str, recursive: bool = False):
  41. # mkdir -p sandbox_dest if it doesn't exist
  42. res = subprocess.run(f'mkdir -p {sandbox_dest}', shell=True, text=True, cwd=config.get(ConfigType.WORKSPACE_BASE))
  43. if res.returncode != 0:
  44. raise RuntimeError(f'Failed to create directory {sandbox_dest} in sandbox')
  45. if recursive:
  46. res = subprocess.run(
  47. f'cp -r {host_src} {sandbox_dest}', shell=True, text=True, cwd=config.get(ConfigType.WORKSPACE_BASE)
  48. )
  49. if res.returncode != 0:
  50. raise RuntimeError(f'Failed to copy {host_src} to {sandbox_dest} in sandbox')
  51. else:
  52. res = subprocess.run(
  53. f'cp {host_src} {sandbox_dest}', shell=True, text=True, cwd=config.get(ConfigType.WORKSPACE_BASE)
  54. )
  55. if res.returncode != 0:
  56. raise RuntimeError(f'Failed to copy {host_src} to {sandbox_dest} in sandbox')
  57. def execute_in_background(self, cmd: str) -> Process:
  58. process = subprocess.Popen(
  59. cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
  60. text=True, cwd=config.get(ConfigType.WORKSPACE_BASE)
  61. )
  62. bg_cmd = DockerProcess(
  63. id=self.cur_background_id, command=cmd, result=process, pid=process.pid
  64. )
  65. self.background_commands[self.cur_background_id] = bg_cmd
  66. self.cur_background_id += 1
  67. return bg_cmd
  68. def kill_background(self, id: int):
  69. if id not in self.background_commands:
  70. raise ValueError('Invalid background command id')
  71. bg_cmd = self.background_commands[id]
  72. assert isinstance(bg_cmd, DockerProcess)
  73. bg_cmd.result.terminate() # terminate the process
  74. bg_cmd.result.wait() # wait for process to terminate
  75. self.background_commands.pop(id)
  76. def read_logs(self, id: int) -> str:
  77. if id not in self.background_commands:
  78. raise ValueError('Invalid background command id')
  79. bg_cmd = self.background_commands[id]
  80. assert isinstance(bg_cmd, DockerProcess)
  81. output = bg_cmd.result.stdout.read()
  82. return output.decode('utf-8')
  83. def close(self):
  84. for id, bg_cmd in list(self.background_commands.items()):
  85. self.kill_background(id)
  86. def cleanup(self):
  87. self.close()