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
This commit is contained in:
GitHub Copilot
2026-01-25 22:58:39 +00:00
parent c02a8f9f02
commit a6d280fe81
2 changed files with 18 additions and 8 deletions
+12 -5
View File
@@ -253,12 +253,19 @@ class LocalServer:
async def _shutdown(self) -> None: async def _shutdown(self) -> None:
# Set exit event first so main loop exits immediately # Set exit event first so main loop exits immediately
self.exit_event.set() 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): with contextlib.suppress(Exception):
await ws.close() await self.session_manager.close_all()
with contextlib.suppress(Exception):
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: async def _run_local_server(self) -> None:
app = web.Application() app = web.Application()
+6 -3
View File
@@ -262,6 +262,9 @@ class TerminalSession(Session):
return True return True
async def close(self) -> None: 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: if self.pid is not None:
try: try:
os.kill(self.pid, signal.SIGHUP) os.kill(self.pid, signal.SIGHUP)
@@ -270,10 +273,10 @@ class TerminalSession(Session):
except Exception as e: except Exception as e:
log.warning("Error closing terminal session %s: %s", self.session_id, 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: if self._task is not None:
with contextlib.suppress(asyncio.CancelledError): with contextlib.suppress(asyncio.CancelledError, TimeoutError):
await self._task await asyncio.wait_for(asyncio.shield(self._task), timeout=timeout)
def is_running(self) -> bool: def is_running(self) -> bool:
"""Check if the terminal session is still running.""" """Check if the terminal session is still running."""