Fix 300s executor shutdown with threading.Event

Replace time.sleep() with threading.Event.wait() in all poll
loops so worker threads exit immediately on shutdown instead of
blocking for up to 500ms per iteration. Fixes the on_unmount
crash (NoMatches from querying removed DOM nodes) by signaling
the event directly rather than iterating child widgets.

Three shutdown paths covered: q key (on_unmount), Ctrl+C
(try/finally in main), and Textual internal shutdown.
This commit is contained in:
Ryan Malloy 2026-02-14 10:21:42 -07:00
parent 48746937a7
commit ba8859cc31
4 changed files with 23 additions and 19 deletions

View file

@ -6,6 +6,7 @@ device status bar, and five swappable screen panels.
import argparse
import logging
import threading
from textual.app import App, ComposeResult
from textual.binding import Binding
@ -52,6 +53,7 @@ class BirdcageApp(App):
firmware_name: str = "g2"
skip_init: bool = False
device: object = None
shutdown_event: threading.Event = threading.Event()
@property
def SUB_TITLE(self) -> str: # noqa: N802
@ -130,13 +132,8 @@ class BirdcageApp(App):
screen.on_show()
def on_unmount(self) -> None:
"""Stop all polling threads and disconnect the device on shutdown."""
for mode_key in MODES:
screen = self.query_one(f"#{mode_key}")
if hasattr(screen, "_polling"):
screen._polling = False
if hasattr(screen, "_monitoring"):
screen._monitoring = False
"""Signal all worker threads to exit and disconnect the device."""
self.shutdown_event.set()
if self.device and hasattr(self.device, "disconnect"):
self.device.disconnect()
@ -173,4 +170,11 @@ def main() -> None:
app.serial_port = args.port
app.firmware_name = args.firmware
app.skip_init = args.skip_init
app.run()
try:
app.run()
except KeyboardInterrupt:
pass
finally:
app.shutdown_event.set()
if app.device and hasattr(app.device, "disconnect"):
app.device.disconnect()