From a6d280fe810db269140ed35991e875c7f8debfc1 Mon Sep 17 00:00:00 2001 From: GitHub Copilot Date: Sun, 25 Jan 2026 22:58:39 +0000 Subject: [PATCH] Fix Ctrl+C handling - add timeouts to prevent blocking - Cancel terminal read task in close() before sending SIGHUP - Add 2s timeout to terminal_session.wait() - Add 3s timeout to server _shutdown() to prevent hanging - Ensures clean exit even if child processes don't respond --- src/textual_webterm/local_server.py | 17 ++++++++++++----- src/textual_webterm/terminal_session.py | 9 ++++++--- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/textual_webterm/local_server.py b/src/textual_webterm/local_server.py index a0edb90..96db4a5 100644 --- a/src/textual_webterm/local_server.py +++ b/src/textual_webterm/local_server.py @@ -253,12 +253,19 @@ class LocalServer: async def _shutdown(self) -> None: # Set exit event first so main loop exits immediately self.exit_event.set() - # Then clean up resources (best effort, don't block exit) - for ws in list(self._websocket_connections.values()): + + # Clean up resources with timeout (best effort, don't block exit) + async def cleanup() -> None: + for ws in list(self._websocket_connections.values()): + with contextlib.suppress(Exception): + await ws.close() with contextlib.suppress(Exception): - await ws.close() - with contextlib.suppress(Exception): - await self.session_manager.close_all() + await self.session_manager.close_all() + + try: + await asyncio.wait_for(cleanup(), timeout=3.0) + except TimeoutError: + log.warning("Shutdown timed out, forcing exit") async def _run_local_server(self) -> None: app = web.Application() diff --git a/src/textual_webterm/terminal_session.py b/src/textual_webterm/terminal_session.py index 0f20f69..739b7df 100644 --- a/src/textual_webterm/terminal_session.py +++ b/src/textual_webterm/terminal_session.py @@ -262,6 +262,9 @@ class TerminalSession(Session): return True async def close(self) -> None: + # Cancel the read task first to unblock any waiting queue.get() + if self._task is not None and not self._task.done(): + self._task.cancel() if self.pid is not None: try: os.kill(self.pid, signal.SIGHUP) @@ -270,10 +273,10 @@ class TerminalSession(Session): except Exception as e: log.warning("Error closing terminal session %s: %s", self.session_id, e) - async def wait(self) -> None: + async def wait(self, timeout: float = 2.0) -> None: if self._task is not None: - with contextlib.suppress(asyncio.CancelledError): - await self._task + with contextlib.suppress(asyncio.CancelledError, TimeoutError): + await asyncio.wait_for(asyncio.shield(self._task), timeout=timeout) def is_running(self) -> bool: """Check if the terminal session is still running."""