From 3beb5dd78fc4184f47e57a5b2ccb008f59406686 Mon Sep 17 00:00:00 2001 From: Matthew McGowan Date: Sat, 31 Jan 2026 17:10:50 -0800 Subject: [PATCH] fix: clean shutdown without hanging on Ctrl+C When pressing Ctrl+C to exit, the application would sometimes hang because BLE notifications continued to queue data during shutdown, preventing the run loop from reaching its exit sentinel. Changes: - linux_pty.py: Add _stopping flag to reject writes during shutdown - linux_pty.py: Clear pending queue items before adding exit sentinel - main.py: Reorder shutdown to stop loops before cleanup - main.py: Add timeouts to prevent hanging on disconnect This ensures a clean, prompt shutdown even when BLE data is still being received. Co-Authored-By: Claude Opus 4.5 --- ble_serial/main.py | 29 +++++++++++++++++++++++++++-- ble_serial/ports/linux_pty.py | 10 ++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/ble_serial/main.py b/ble_serial/main.py index 2fa8596..a4da0f4 100644 --- a/ble_serial/main.py +++ b/ble_serial/main.py @@ -75,10 +75,35 @@ async def _run(self): logging.exception(e) finally: logging.warning('Shutdown initiated') + + # Stop the run loops first (signals tasks to exit gracefully) if hasattr(self, 'uart'): - self.uart.remove() + self.uart.stop_loop() if hasattr(self, 'bt'): - await self.bt.disconnect() + self.bt.stop_loop() + + # Cancel and await pending tasks before cleanup + try: + if pending: + for t in pending: + t.cancel() + await asyncio.wait_for( + asyncio.gather(*pending, return_exceptions=True), + timeout=2.0 + ) + except NameError: + pass # pending not defined if we failed before asyncio.wait + except asyncio.TimeoutError: + logging.warning('Timeout waiting for tasks to cancel') + + # Now safe to disconnect BLE and remove uart + if hasattr(self, 'bt'): + try: + await asyncio.wait_for(self.bt.disconnect(), timeout=3.0) + except asyncio.TimeoutError: + logging.warning('Timeout disconnecting BLE') + if hasattr(self, 'uart'): + self.uart.remove() if hasattr(self, 'log'): self.log.finish() logging.info('Shutdown complete.') diff --git a/ble_serial/ports/linux_pty.py b/ble_serial/ports/linux_pty.py index 2709609..e09ed55 100644 --- a/ble_serial/ports/linux_pty.py +++ b/ble_serial/ports/linux_pty.py @@ -11,6 +11,7 @@ def __init__(self, symlink: str, ev_loop: asyncio.AbstractEventLoop, mtu: int): self.loop = ev_loop self.mtu = mtu self._send_queue = asyncio.Queue() + self._stopping = False self._controller_fd, endpoint_fd = pty.openpty() self.endpoint_path = os.ttyname(endpoint_fd) @@ -39,6 +40,13 @@ def start(self): def stop_loop(self): logging.info('Stopping serial event loop') + self._stopping = True + # Clear any pending items so we exit quickly + while not self._send_queue.empty(): + try: + self._send_queue.get_nowait() + except asyncio.QueueEmpty: + break self._send_queue.put_nowait(None) def remove(self): @@ -58,6 +66,8 @@ def read_sync(self): return value def queue_write(self, value: bytes): + if self._stopping: + return # Ignore writes during shutdown self._send_queue.put_nowait(value) async def run_loop(self):