[PATCH 13/26] lucid: replace daemon thread with atexit cleanup
Daniel Gomez <[email protected]> Tue, 19 May 2026 15:28:10 +0200
| Newsgroups | dev.linux.lists.kdevops |
|---|---|
| Message-ID | <[email protected]> |
From: Daniel Gomez <[email protected]> Marking the display update thread as a daemon lets the interpreter terminate it abruptly during shutdown, at any point in its write cycle. If the thread was mid-way through emitting an ANSI cursor- control sequence, only the prefix reaches the terminal and the emulator is left in a corrupt state that persists past the playbook's exit, requiring the user to run reset to recover. The existing __del__ hook cannot paper over this because CPython does not guarantee object finalization during interpreter shutdown, especially when the shutdown path is triggered by a signal or an unhandled exception. Drop the daemon flag and register a _cleanup handler with atexit. The atexit path fires on normal interpreter exit with the runtime still healthy, signals the stop event, joins the thread with a bounded timeout so a wedged writer cannot hang the process, clears any remaining dynamic display lines, and always restores cursor visibility with \033[?25h emitted as a single write under output_lock. The __del__ method is removed as redundant and unreliable. The playbook-end cleanup in v2_playbook_on_stats still runs first in the normal case; atexit is the backstop for shutdown paths that bypass it. Suggested-by: Chuck Lever <[email protected]> Generated-by: Claude AI Signed-off-by: Daniel Gomez <[email protected]> --- callback_plugins/lucid.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/callback_plugins/lucid.py b/callback_plugins/lucid.py index cdce984f..79f07ea0 100644 --- a/callback_plugins/lucid.py +++ b/callback_plugins/lucid.py @@ -12,6 +12,7 @@ A modern stdout callback plugin providing: from __future__ import annotations +import atexit import json import os import shutil @@ -221,8 +222,22 @@ class CallbackModule(CallbackBase): def _start_update_thread(self): """Start background thread for live display updates""" self.update_thread_stop = threading.Event() - self.update_thread = threading.Thread(target=self._update_loop, daemon=True) + self.update_thread = threading.Thread(target=self._update_loop) self.update_thread.start() + atexit.register(self._cleanup) + + def _cleanup(self): + """Clean up display thread and reset terminal state""" + if self.update_thread_stop: + self.update_thread_stop.set() + if self.update_thread: + self.update_thread.join(timeout=2.0) + # Reset terminal state + if self.dynamic_mode and self.display_lines > 0: + self._clear_display() + with self.output_lock: + sys.stdout.write("\033[?25h") # Ensure cursor visible + sys.stdout.flush() def _update_loop(self): """Update display every 0.5 seconds in dynamic mode""" @@ -974,10 +989,3 @@ class CallbackModule(CallbackBase): self.dynamic_mode = False if self.update_thread_stop: self.update_thread_stop.set() - - def __del__(self): - """Cleanup when plugin is destroyed""" - if self.update_thread_stop: - self.update_thread_stop.set() - if self.update_thread: - self.update_thread.join(timeout=1.0) -- 2.53.0