From 58c0195defbfc88d932f87460ddd672d2142749e Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 5 May 2026 13:36:36 +0000 Subject: [PATCH] fix(gui): make Quit button actually terminate the server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signalling the process with SIGTERM/SIGINT didn't reliably shut Streamlit down — its tornado/asyncio loop swallowed or deferred the signal, so the browser saw the websocket drop ("Connection error") while the python process kept running. Replace the signal with a daemon-thread ``os._exit(0)`` after a short delay so the current rerun can paint the "shutting down" message before the process is hard-killed. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/gui/components/_legacy.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/gui/components/_legacy.py b/src/gui/components/_legacy.py index fc8ecf9..fa03da9 100644 --- a/src/gui/components/_legacy.py +++ b/src/gui/components/_legacy.py @@ -4,7 +4,8 @@ from __future__ import annotations import io import os -import signal +import threading +import time from typing import Optional import pandas as pd @@ -90,21 +91,27 @@ def hide_streamlit_chrome() -> None: def quit_button(label: str = "Quit app", *, key: str = "quit_app_button") -> None: """Render a Quit button that terminates the Streamlit server. - Streamlit has no first-class shutdown hook, so closing the browser tab - leaves the server (and the python process running it) alive — the user - has to Ctrl+C in the shell. This helper signals the Streamlit process - so the shell returns cleanly. SIGTERM lets Streamlit run its own - shutdown handlers; if that's unavailable on the platform, fall back to - SIGINT (the same signal Ctrl+C delivers). + Streamlit has no first-class shutdown hook, and signalling the + process (SIGTERM/SIGINT) does not reliably terminate it — Streamlit + installs its own handlers and the tornado/asyncio loop swallows or + defers the signal, so the browser sees the websocket drop while the + python process stays alive. To shut down cleanly we schedule + ``os._exit(0)`` on a daemon thread after a short delay; the delay + lets the current rerun finish painting the "shutting down" message + before the process is hard-killed. """ if st.session_state.get("_app_shutting_down"): - st.success("App shut down. You can close this browser tab.") + st.success("Shutting down… you can close this browser tab.") st.stop() if st.button(label, key=key, type="secondary"): st.session_state["_app_shutting_down"] = True - sig = getattr(signal, "SIGTERM", signal.SIGINT) - os.kill(os.getpid(), sig) + + def _hard_exit() -> None: + time.sleep(0.5) + os._exit(0) + + threading.Thread(target=_hard_exit, daemon=True).start() st.rerun()