Allow cmd execution after running a background command (#8093)

This commit is contained in:
Boxuan Li 2025-04-26 11:04:05 +08:00 committed by GitHub
parent e4b7b31f48
commit da7041b5e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 63 additions and 10 deletions

View File

@ -505,9 +505,19 @@ class BashSession:
)
)
# Get initial state before sending command
initial_pane_output = self._get_pane_content()
initial_ps1_matches = CmdOutputMetadata.matches_ps1_metadata(
initial_pane_output
)
initial_ps1_count = len(initial_ps1_matches)
logger.debug(f'Initial PS1 count: {initial_ps1_count}')
start_time = time.time()
last_change_time = start_time
last_pane_output = self._get_pane_content()
last_pane_output = (
initial_pane_output # Use initial output as the starting point
)
# When prev command is still running, and we are trying to send a new command
if (
@ -516,15 +526,20 @@ class BashSession:
BashCommandStatus.HARD_TIMEOUT,
BashCommandStatus.NO_CHANGE_TIMEOUT,
}
and not last_pane_output.endswith(
CMD_OUTPUT_PS1_END
and not last_pane_output.rstrip().endswith(
CMD_OUTPUT_PS1_END.rstrip()
) # prev command is not completed
and not is_input
and command != '' # not input and not empty command
):
_ps1_matches = CmdOutputMetadata.matches_ps1_metadata(last_pane_output)
# Use initial_ps1_matches if _ps1_matches is empty, otherwise use _ps1_matches
# This handles the case where the prompt might be scrolled off screen but existed before
current_matches_for_output = (
_ps1_matches if _ps1_matches else initial_ps1_matches
)
raw_command_output = self._combine_outputs_between_matches(
last_pane_output, _ps1_matches
last_pane_output, current_matches_for_output
)
metadata = CmdOutputMetadata() # No metadata available
metadata.suffix = (
@ -577,23 +592,32 @@ class BashSession:
logger.debug(f"BEGIN OF PANE CONTENT: {cur_pane_output.split('\n')[:10]}")
logger.debug(f"END OF PANE CONTENT: {cur_pane_output.split('\n')[-10:]}")
ps1_matches = CmdOutputMetadata.matches_ps1_metadata(cur_pane_output)
current_ps1_count = len(ps1_matches)
if cur_pane_output != last_pane_output:
last_pane_output = cur_pane_output
last_change_time = time.time()
logger.debug(f'CONTENT UPDATED DETECTED at {last_change_time}')
# 1) Execution completed
# if the last command output contains the end marker
if cur_pane_output.rstrip().endswith(CMD_OUTPUT_PS1_END.rstrip()):
# 1) Execution completed:
# Condition 1: A new prompt has appeared since the command started.
# Condition 2: The prompt count hasn't increased (potentially because the initial one scrolled off),
# BUT the *current* visible pane ends with a prompt, indicating completion.
if (
current_ps1_count > initial_ps1_count
or cur_pane_output.rstrip().endswith(CMD_OUTPUT_PS1_END.rstrip())
):
return self._handle_completed_command(
command,
pane_content=cur_pane_output,
ps1_matches=ps1_matches,
)
# Timeout checks should only trigger if a new prompt hasn't appeared yet.
# 2) Execution timed out since there's no change in output
# for a while (self.NO_CHANGE_TIMEOUT_SECONDS)
# We ignore this if the command is *blocking
# We ignore this if the command is *blocking*
time_since_last_change = time.time() - last_change_time
logger.debug(
f'CHECKING NO CHANGE TIMEOUT ({self.NO_CHANGE_TIMEOUT_SECONDS}s): elapsed {time_since_last_change}. Action blocking: {action.blocking}'
@ -609,10 +633,12 @@ class BashSession:
)
# 3) Execution timed out due to hard timeout
elapsed_time = time.time() - start_time
logger.debug(
f'CHECKING HARD TIMEOUT ({action.timeout}s): elapsed {time.time() - start_time}'
f'CHECKING HARD TIMEOUT ({action.timeout}s): elapsed {elapsed_time:.2f}'
)
if action.timeout and time.time() - start_time >= action.timeout:
if action.timeout and elapsed_time >= action.timeout:
logger.debug('Hard timeout triggered.')
return self._handle_hard_timeout_command(
command,
pane_content=cur_pane_output,

View File

@ -87,6 +87,33 @@ def test_bash_server(temp_dir, runtime_cls, run_as_openhands):
_close_test_runtime(runtime)
def test_bash_background_server(temp_dir, runtime_cls, run_as_openhands):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
server_port = 8081
try:
# Start the server, expect it to timeout (run in background manner)
action = CmdRunAction(f'python3 -m http.server {server_port} &')
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert isinstance(obs, CmdOutputObservation)
assert obs.exit_code == 0 # Should not timeout since this runs in background
# Give the server a moment to be ready
time.sleep(1)
# Verify the server is running by curling it
curl_action = CmdRunAction(f'curl http://localhost:{server_port}')
curl_obs = runtime.run_action(curl_action)
logger.info(curl_obs, extra={'msg_type': 'OBSERVATION'})
assert isinstance(curl_obs, CmdOutputObservation)
assert curl_obs.exit_code == 0
# Check for content typical of python http.server directory listing
assert 'Directory listing for' in curl_obs.content
finally:
_close_test_runtime(runtime)
def test_multiline_commands(temp_dir, runtime_cls):
runtime, config = _load_runtime(temp_dir, runtime_cls)
try: