process.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. import select
  2. import sys
  3. from opendevin.runtime.process import Process
  4. class DockerProcess(Process):
  5. """
  6. Represents a background command execution
  7. """
  8. def __init__(self, id: int, command: str, result, pid: int):
  9. """
  10. Initialize a DockerProcess instance.
  11. Args:
  12. id (int): The identifier of the command.
  13. command (str): The command to be executed.
  14. result: The result of the command execution.
  15. pid (int): The process ID (PID) of the command.
  16. """
  17. self.id = id
  18. self._command = command
  19. self.result = result
  20. self._pid = pid
  21. @property
  22. def pid(self) -> int:
  23. return self._pid
  24. @property
  25. def command(self) -> str:
  26. return self._command
  27. def parse_docker_exec_output(self, logs: bytes) -> tuple[bytes, bytes]:
  28. """
  29. 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.
  30. Example:
  31. Considering you have a docker container named `my_container` up and running
  32. $ docker exec my_container echo "Hello OpenDevin!"
  33. >> b'\x00\x00\x00\x00\x00\x00\x00\x13Hello OpenDevin!'
  34. Such binary logs will be processed by this function.
  35. 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.
  36. 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.
  37. >> output_bytes = b'\x00\x00\x00\x00\x00\x00\x00\x13Hello OpenDevin!'
  38. >> parsed_output, remaining_bytes = parse_docker_exec_output(output_bytes)
  39. >> print(parsed_output)
  40. b'Hello OpenDevin!'
  41. >> print(remaining_bytes)
  42. b''
  43. Args:
  44. logs (bytes): The raw output logs of the command.
  45. Returns:
  46. tuple[bytes, bytes]: A tuple containing the parsed output and any remaining data.
  47. """
  48. res = b''
  49. tail = b''
  50. i = 0
  51. byte_order = sys.byteorder
  52. while i < len(logs):
  53. prefix = logs[i : i + 8]
  54. if len(prefix) < 8:
  55. msg_type = prefix[0:1]
  56. if msg_type in [b'\x00', b'\x01', b'\x02', b'\x03']:
  57. tail = prefix
  58. break
  59. msg_type = prefix[0:1]
  60. padding = prefix[1:4]
  61. if (
  62. msg_type in [b'\x00', b'\x01', b'\x02', b'\x03']
  63. and padding == b'\x00\x00\x00'
  64. ):
  65. msg_length = int.from_bytes(prefix[4:8], byteorder=byte_order)
  66. res += logs[i + 8 : i + 8 + msg_length]
  67. i += 8 + msg_length
  68. else:
  69. res += logs[i : i + 1]
  70. i += 1
  71. return res, tail
  72. def read_logs(self) -> str:
  73. """
  74. Read and decode the logs of the command.
  75. This function continuously reads the standard output of a subprocess and
  76. processes the output using the parse_docker_exec_output function to handle
  77. binary log messages. It concatenates and decodes the output bytes into a
  78. string, ensuring that no partial messages are lost during reading.
  79. Dummy Example:
  80. >> cmd = 'echo "Hello OpenDevin!"'
  81. >> result = subprocess.Popen(
  82. cmd, shell=True, stdout=subprocess.PIPE,
  83. stderr=subprocess.STDOUT, text=True, cwd='.'
  84. )
  85. >> bg_cmd = DockerProcess(id, cmd = cmd, result = result, pid)
  86. >> logs = bg_cmd.read_logs()
  87. >> print(logs)
  88. Hello OpenDevin!
  89. Returns:
  90. str: The decoded logs(string) of the command.
  91. """
  92. # TODO: get an exit code if process is exited
  93. logs = b''
  94. last_remains = b''
  95. while True:
  96. ready_to_read, _, _ = select.select([self.result.output], [], [], 0.1) # type: ignore[has-type]
  97. if ready_to_read:
  98. data = self.result.output.read(4096) # type: ignore[has-type]
  99. if not data:
  100. break
  101. chunk, last_remains = self.parse_docker_exec_output(last_remains + data)
  102. logs += chunk
  103. else:
  104. break
  105. return (logs + last_remains).decode('utf-8', errors='replace')