mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
Allow cmd execution after running a background command (#8093)
This commit is contained in:
parent
e4b7b31f48
commit
da7041b5e9
@ -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,
|
||||
|
||||
@ -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:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user