[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