local_box.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. import atexit
  2. import os
  3. import subprocess
  4. import sys
  5. from opendevin.core.config import config
  6. from opendevin.core.logger import opendevin_logger as logger
  7. from opendevin.core.schema import CancellableStream
  8. from opendevin.runtime.docker.process import DockerProcess, Process
  9. from opendevin.runtime.sandbox import Sandbox
  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 = config.sandbox_timeout):
  26. os.makedirs(config.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. super().__init__()
  32. def execute(
  33. self, cmd: str, stream: bool = False, timeout: int | None = None
  34. ) -> tuple[int, str | CancellableStream]:
  35. timeout = timeout if timeout is not None else self.timeout
  36. try:
  37. completed_process = subprocess.run(
  38. cmd,
  39. shell=True,
  40. text=True,
  41. capture_output=True,
  42. timeout=timeout,
  43. cwd=config.workspace_base,
  44. env=self._env,
  45. )
  46. return completed_process.returncode, completed_process.stdout.strip()
  47. except subprocess.TimeoutExpired:
  48. return -1, 'Command timed out'
  49. def copy_to(self, host_src: str, sandbox_dest: str, recursive: bool = False):
  50. # mkdir -p sandbox_dest if it doesn't exist
  51. res = subprocess.run(
  52. f'mkdir -p {sandbox_dest}',
  53. shell=True,
  54. text=True,
  55. cwd=config.workspace_base,
  56. env=self._env,
  57. )
  58. if res.returncode != 0:
  59. raise RuntimeError(f'Failed to create directory {sandbox_dest} in sandbox')
  60. if recursive:
  61. res = subprocess.run(
  62. f'cp -r {host_src} {sandbox_dest}',
  63. shell=True,
  64. text=True,
  65. cwd=config.workspace_base,
  66. env=self._env,
  67. )
  68. if res.returncode != 0:
  69. raise RuntimeError(
  70. f'Failed to copy {host_src} to {sandbox_dest} in sandbox'
  71. )
  72. else:
  73. res = subprocess.run(
  74. f'cp {host_src} {sandbox_dest}',
  75. shell=True,
  76. text=True,
  77. cwd=config.workspace_base,
  78. env=self._env,
  79. )
  80. if res.returncode != 0:
  81. raise RuntimeError(
  82. f'Failed to copy {host_src} to {sandbox_dest} in sandbox'
  83. )
  84. def execute_in_background(self, cmd: str) -> Process:
  85. process = subprocess.Popen(
  86. cmd,
  87. shell=True,
  88. stdout=subprocess.PIPE,
  89. stderr=subprocess.STDOUT,
  90. text=True,
  91. cwd=config.workspace_base,
  92. )
  93. bg_cmd = DockerProcess(
  94. id=self.cur_background_id, command=cmd, result=process, pid=process.pid
  95. )
  96. self.background_commands[self.cur_background_id] = bg_cmd
  97. self.cur_background_id += 1
  98. return bg_cmd
  99. def kill_background(self, id: int):
  100. if id not in self.background_commands:
  101. raise ValueError('Invalid background command id')
  102. bg_cmd = self.background_commands[id]
  103. assert isinstance(bg_cmd, DockerProcess)
  104. bg_cmd.result.terminate() # terminate the process
  105. bg_cmd.result.wait() # wait for process to terminate
  106. self.background_commands.pop(id)
  107. def read_logs(self, id: int) -> str:
  108. if id not in self.background_commands:
  109. raise ValueError('Invalid background command id')
  110. bg_cmd = self.background_commands[id]
  111. assert isinstance(bg_cmd, DockerProcess)
  112. output = bg_cmd.result.stdout.read()
  113. return output.decode('utf-8')
  114. def close(self):
  115. for id, bg_cmd in list(self.background_commands.items()):
  116. self.kill_background(id)
  117. def cleanup(self):
  118. self.close()
  119. def get_working_directory(self):
  120. return config.workspace_base
  121. if __name__ == '__main__':
  122. local_box = LocalBox()
  123. bg_cmd = local_box.execute_in_background(
  124. "while true; do echo 'dot ' && sleep 10; done"
  125. )
  126. sys.stdout.flush()
  127. try:
  128. while True:
  129. try:
  130. user_input = input('>>> ')
  131. except EOFError:
  132. logger.info('Exiting...')
  133. break
  134. if user_input.lower() == 'exit':
  135. logger.info('Exiting...')
  136. break
  137. if user_input.lower() == 'kill':
  138. local_box.kill_background(bg_cmd.pid)
  139. logger.info('Background process killed')
  140. continue
  141. exit_code, output = local_box.execute(user_input)
  142. logger.info('exit code: %d', exit_code)
  143. logger.info(output)
  144. if bg_cmd.pid in local_box.background_commands:
  145. logs = local_box.read_logs(bg_cmd.pid)
  146. logger.info('background logs: %s', logs)
  147. sys.stdout.flush()
  148. except KeyboardInterrupt:
  149. logger.info('Exiting...')
  150. local_box.close()