mixin.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. import os
  2. from typing import Protocol
  3. from opendevin.core.logger import opendevin_logger as logger
  4. from opendevin.core.schema import CancellableStream
  5. from opendevin.runtime.plugins.requirement import PluginRequirement
  6. class SandboxProtocol(Protocol):
  7. # https://stackoverflow.com/questions/51930339/how-do-i-correctly-add-type-hints-to-mixin-classes
  8. @property
  9. def initialize_plugins(self) -> bool: ...
  10. def execute(
  11. self, cmd: str, stream: bool = False
  12. ) -> tuple[int, str | CancellableStream]: ...
  13. def copy_to(self, host_src: str, sandbox_dest: str, recursive: bool = False): ...
  14. def _source_bashrc(sandbox: SandboxProtocol):
  15. exit_code, output = sandbox.execute(
  16. 'source /opendevin/bash.bashrc && source ~/.bashrc'
  17. )
  18. if exit_code != 0:
  19. raise RuntimeError(
  20. f'Failed to source /opendevin/bash.bashrc and ~/.bashrc with exit code {exit_code} and output: {output}'
  21. )
  22. logger.info('Sourced /opendevin/bash.bashrc and ~/.bashrc successfully')
  23. class PluginMixin:
  24. """Mixin for Sandbox to support plugins."""
  25. def init_plugins(self: SandboxProtocol, requirements: list[PluginRequirement]):
  26. """Load a plugin into the sandbox."""
  27. if hasattr(self, 'plugin_initialized') and self.plugin_initialized:
  28. return
  29. if self.initialize_plugins:
  30. logger.info('Initializing plugins in the sandbox')
  31. # clean-up ~/.bashrc and touch ~/.bashrc
  32. exit_code, output = self.execute('rm -f ~/.bashrc && touch ~/.bashrc')
  33. if exit_code != 0:
  34. logger.warning(
  35. f'Failed to clean-up ~/.bashrc with exit code {exit_code} and output: {output}'
  36. )
  37. for requirement in requirements:
  38. # source bashrc file when plugin loads
  39. _source_bashrc(self)
  40. # copy over the files
  41. self.copy_to(
  42. requirement.host_src, requirement.sandbox_dest, recursive=True
  43. )
  44. logger.info(
  45. f'Copied files from [{requirement.host_src}] to [{requirement.sandbox_dest}] inside sandbox.'
  46. )
  47. # Execute the bash script
  48. abs_path_to_bash_script = os.path.join(
  49. requirement.sandbox_dest, requirement.bash_script_path
  50. )
  51. logger.info(
  52. f'Initializing plugin [{requirement.name}] by executing [{abs_path_to_bash_script}] in the sandbox.'
  53. )
  54. exit_code, output = self.execute(abs_path_to_bash_script, stream=True)
  55. if isinstance(output, CancellableStream):
  56. total_output = ''
  57. for line in output:
  58. # Removes any trailing whitespace, including \n and \r\n
  59. line = line.rstrip()
  60. # logger.debug(line)
  61. # Avoid text from lines running into each other
  62. total_output += line + ' '
  63. _exit_code = output.exit_code()
  64. output.close()
  65. if _exit_code != 0:
  66. raise RuntimeError(
  67. f'Failed to initialize plugin {requirement.name} with exit code {_exit_code} and output: {total_output.strip()}'
  68. )
  69. logger.debug(f'Output: {total_output.strip()}')
  70. else:
  71. if exit_code != 0:
  72. raise RuntimeError(
  73. f'Failed to initialize plugin {requirement.name} with exit code {exit_code} and output: {output}'
  74. )
  75. logger.debug(f'Output: {output}')
  76. logger.info(f'Plugin {requirement.name} initialized successfully')
  77. else:
  78. logger.info('Skipping plugin initialization in the sandbox')
  79. if len(requirements) > 0:
  80. _source_bashrc(self)
  81. self.plugin_initialized = True