From 09f5e3cbdeadda3cb98b1b1009fdc941dfa90f34 Mon Sep 17 00:00:00 2001 From: Benny Zlotnik Date: Fri, 7 Aug 2026 16:55:07 +0300 Subject: [PATCH] fix: catch Exception in _flush_lines In case there are unexepected exceptions, to avoid hitting flakes https://github.com/jumpstarter-dev/jumpstarter/actions/runs/31179469197/attempts/1 Signed-off-by: Benny Zlotnik --- .../jumpstarter/jumpstarter/exporter/hooks.py | 5 ++++ .../jumpstarter/exporter/hooks_test.py | 27 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/python/packages/jumpstarter/jumpstarter/exporter/hooks.py b/python/packages/jumpstarter/jumpstarter/exporter/hooks.py index e7b509efb..9e7d04f95 100644 --- a/python/packages/jumpstarter/jumpstarter/exporter/hooks.py +++ b/python/packages/jumpstarter/jumpstarter/exporter/hooks.py @@ -427,6 +427,11 @@ async def read_pty_output() -> None: # noqa: C901 # PTY closed or read error logger.debug("read_pty_output: OSError in loop: %s", e) break + + except Exception as e: + logger.debug("read_pty_output: unexpected error in loop: %s", e) + break + finally: # Drain any remaining data from the PTY buffer. # On macOS, PTY output may still be in the kernel buffer diff --git a/python/packages/jumpstarter/jumpstarter/exporter/hooks_test.py b/python/packages/jumpstarter/jumpstarter/exporter/hooks_test.py index 01acee354..08c32a41f 100644 --- a/python/packages/jumpstarter/jumpstarter/exporter/hooks_test.py +++ b/python/packages/jumpstarter/jumpstarter/exporter/hooks_test.py @@ -1042,6 +1042,33 @@ def flush_lines_with_drain_error(buffer, output_lines): result = await executor.execute_before_lease_hook(lease_scope) assert result is None + @macos_pty_xfail + async def test_main_loop_non_oserror_is_caught(self, lease_scope) -> None: + """Verify that a non-OSError exception in the main read loop is caught + by the except-Exception handler and does not propagate to the caller. + """ + hook_config = HookConfigV1Alpha1( + before_lease=HookInstanceConfigV1Alpha1( + script="echo MAIN_LOOP_ERROR", + timeout=10, + ), + ) + executor = HookExecutor(config=hook_config) + + def flush_lines_always_error(buffer, output_lines): + raise ValueError("simulated non-OSError") + + with ( + patch("jumpstarter.exporter.hooks._flush_lines", side_effect=flush_lines_always_error), + patch("jumpstarter.exporter.hooks.logger") as mock_logger, + ): + result = await executor.execute_before_lease_hook(lease_scope) + assert result is None + debug_calls = [str(c) for c in mock_logger.debug.call_args_list] + assert any("unexpected error in loop" in c for c in debug_calls), ( + f"Expected main-loop exception handler to log, got: {debug_calls}" + ) + @macos_pty_xfail async def test_drain_retries_empty_select_then_captures_data(self, lease_scope) -> None: """Verify that the drain retries after empty select() calls and still