From c1467e67c0f1759888e0cc86763f88a06cc4b2e2 Mon Sep 17 00:00:00 2001 From: openhands Date: Thu, 21 Nov 2024 18:21:05 +0000 Subject: [PATCH] Refactor bash output capture to use file-based streaming --- openhands/runtime/utils/bash.py | 40 +++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/openhands/runtime/utils/bash.py b/openhands/runtime/utils/bash.py index 675012953ecc4..10b2727249446 100644 --- a/openhands/runtime/utils/bash.py +++ b/openhands/runtime/utils/bash.py @@ -2,6 +2,7 @@ import time import uuid from enum import Enum +from pathlib import Path import bashlex import libtmux @@ -90,12 +91,18 @@ def __init__( ): self.NO_CHANGE_TIMEOUT_SECONDS = no_change_timeout_seconds + # Create output directory + self.command_id = str(uuid.uuid4()) + self.output_dir = Path('/openhands/commands') + self.output_dir.mkdir(parents=True, exist_ok=True) + self.output_file = self.output_dir / f'{self.command_id}.txt' + self.server = libtmux.Server() window_command = '/bin/bash' if username: window_command = f'su {username}' - session_name = f'openhands-{username}-{uuid.uuid4()}' + session_name = f'openhands-{username}-{self.command_id}' self.session = self.server.new_session( session_name=session_name, window_name='bash', @@ -105,10 +112,6 @@ def __init__( x=1000, y=1000, ) - # https://unix.stackexchange.com/questions/43414/unlimited-history-in-tmux - _history_limit = 999999999 - self.session.set_option('history-limit', str(_history_limit), _global=True) - self.session.history_limit = _history_limit # We need to create a new pane because the initial pane's history limit is (default) 2000 _initial_window = self.session.attached_window @@ -117,12 +120,13 @@ def __init__( start_directory=work_dir, ) self.pane = self.window.attached_pane - logger.debug(f'pane: {self.pane}; history_limit: {self.session.history_limit}') _initial_window.kill_window() - # Configure bash to use simple PS1 and disable PS2 + # Configure bash to use simple PS1, disable PS2, and redirect all output to file self.pane.send_keys( - f'export PROMPT_COMMAND=\'export PS1="{self.PS1}"\'; export PS2=""' + f'export PROMPT_COMMAND=\'export PS1="{self.PS1}"\'; ' + f'export PS2=""; ' + f'exec > >(tee -a "{self.output_file}") 2>&1' ) time.sleep(0.2) # Wait for command to take effect self._clear_screen() @@ -137,6 +141,10 @@ def __init__( def close(self): self.session.kill_session() + try: + self.output_file.unlink() + except FileNotFoundError: + pass @property def pwd(self): @@ -158,16 +166,14 @@ def _get_pane_content(self, full: bool = False) -> str: """Get the current content of the tmux pane. Args: - full: If True, capture the entire history of the pane. + full: If True, read the entire file. Otherwise read only the last part. """ - # https://man7.org/linux/man-pages/man1/tmux.1.html - # -J preserves trailing spaces and joins any wrapped lines; - # -p direct output to stdout - # -S -: start from the start of history - if full: - # Capture the entire history of the pane - return '\n'.join(self.pane.cmd('capture-pane', '-J', '-pS', '-').stdout) - return '\n'.join(self.pane.cmd('capture-pane', '-J', '-p').stdout) + try: + with open(self.output_file, 'r') as f: + content = f.read() + return content + except FileNotFoundError: + return "" def _get_command_output( self,