sandbox.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. import select
  2. import sys
  3. from abc import ABC, abstractmethod
  4. from typing import Dict
  5. from typing import Tuple
  6. class BackgroundCommand:
  7. """
  8. Represents a background command execution
  9. """
  10. def __init__(self, id: int, command: str, result, pid: int):
  11. """
  12. Initialize a BackgroundCommand instance.
  13. Args:
  14. id (int): The identifier of the command.
  15. command (str): The command to be executed.
  16. result: The result of the command execution.
  17. pid (int): The process ID (PID) of the command.
  18. """
  19. self.id = id
  20. self.command = command
  21. self.result = result
  22. self.pid = pid
  23. def parse_docker_exec_output(self, logs: bytes) -> Tuple[bytes, bytes]:
  24. """
  25. When you execute a command using `exec` in a docker container, the output produced will be in bytes. this function parses the output of a Docker exec command.
  26. Example:
  27. Considering you have a docker container named `my_container` up and running
  28. $ docker exec my_container echo "Hello OpenDevin!"
  29. >> b'\x00\x00\x00\x00\x00\x00\x00\x13Hello OpenDevin!'
  30. Such binary logs will be processed by this function.
  31. The function handles message types, padding, and byte order to create a usable result. The primary goal is to convert raw container logs into a more structured format for further analysis or display.
  32. The function also returns a tail of bytes to ensure that no information is lost. It is a way to handle edge cases and maintain data integrity.
  33. >> output_bytes = b'\x00\x00\x00\x00\x00\x00\x00\x13Hello OpenDevin!'
  34. >> parsed_output, remaining_bytes = parse_docker_exec_output(output_bytes)
  35. >> print(parsed_output)
  36. b'Hello OpenDevin!'
  37. >> print(remaining_bytes)
  38. b''
  39. Args:
  40. logs (bytes): The raw output logs of the command.
  41. Returns:
  42. Tuple[bytes, bytes]: A tuple containing the parsed output and any remaining data.
  43. """
  44. res = b''
  45. tail = b''
  46. i = 0
  47. byte_order = sys.byteorder
  48. while i < len(logs):
  49. prefix = logs[i: i + 8]
  50. if len(prefix) < 8:
  51. msg_type = prefix[0:1]
  52. if msg_type in [b'\x00', b'\x01', b'\x02', b'\x03']:
  53. tail = prefix
  54. break
  55. msg_type = prefix[0:1]
  56. padding = prefix[1:4]
  57. if (
  58. msg_type in [b'\x00', b'\x01', b'\x02', b'\x03']
  59. and padding == b'\x00\x00\x00'
  60. ):
  61. msg_length = int.from_bytes(prefix[4:8], byteorder=byte_order)
  62. res += logs[i + 8: i + 8 + msg_length]
  63. i += 8 + msg_length
  64. else:
  65. res += logs[i: i + 1]
  66. i += 1
  67. return res, tail
  68. def read_logs(self) -> str:
  69. """
  70. Read and decode the logs of the command.
  71. This function continuously reads the standard output of a subprocess and
  72. processes the output using the parse_docker_exec_output function to handle
  73. binary log messages. It concatenates and decodes the output bytes into a
  74. string, ensuring that no partial messages are lost during reading.
  75. Dummy Example:
  76. >> cmd = 'echo "Hello OpenDevin!"'
  77. >> result = subprocess.Popen(
  78. cmd, shell=True, stdout=subprocess.PIPE,
  79. stderr=subprocess.STDOUT, text=True, cwd='.'
  80. )
  81. >> bg_cmd = BackgroundCommand(id, cmd = cmd, result = result, pid)
  82. >> logs = bg_cmd.read_logs()
  83. >> print(logs)
  84. Hello OpenDevin!
  85. Returns:
  86. str: The decoded logs(string) of the command.
  87. """
  88. # TODO: get an exit code if process is exited
  89. logs = b''
  90. last_remains = b''
  91. while True:
  92. ready_to_read, _, _ = select.select(
  93. [self.result.output], [], [], 0.1) # type: ignore[has-type]
  94. if ready_to_read:
  95. data = self.result.output.read(4096) # type: ignore[has-type]
  96. if not data:
  97. break
  98. chunk, last_remains = self.parse_docker_exec_output(
  99. last_remains + data)
  100. logs += chunk
  101. else:
  102. break
  103. return (logs + last_remains).decode('utf-8', errors='replace')
  104. class Sandbox(ABC):
  105. background_commands: Dict[int, BackgroundCommand] = {}
  106. @abstractmethod
  107. def execute(self, cmd: str) -> Tuple[int, str]:
  108. pass
  109. @abstractmethod
  110. def execute_in_background(self, cmd: str):
  111. pass
  112. @abstractmethod
  113. def kill_background(self, id: int):
  114. pass
  115. @abstractmethod
  116. def read_logs(self, id: int) -> str:
  117. pass
  118. @abstractmethod
  119. def close(self):
  120. pass