diff --git a/src/gui/components/_legacy.py b/src/gui/components/_legacy.py index 1f59d11..4a06989 100644 --- a/src/gui/components/_legacy.py +++ b/src/gui/components/_legacy.py @@ -80,6 +80,53 @@ def hide_streamlit_chrome() -> None: # Clean shutdown # --------------------------------------------------------------------------- +_FAREWELL_SCRIPT = """ + +""" + + def quit_button(label: str = "Quit app", *, key: str = "quit_app_button") -> None: """Render a Quit button that terminates the Streamlit server. @@ -87,12 +134,15 @@ def quit_button(label: str = "Quit app", *, key: str = "quit_app_button") -> Non 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. + python process stays alive. We schedule ``os._exit(0)`` on a daemon + thread to hard-kill the process, and inject a small JS shim that + either closes the tab (when allowed) or replaces the top frame with + a self-contained "App closed" page so the user never sees + Streamlit's red connection-error overlay. """ if st.session_state.get("_app_shutting_down"): + from streamlit.components.v1 import html as _components_html + _components_html(_FAREWELL_SCRIPT, height=0) st.success("Shutting down… you can close this browser tab.") st.stop() @@ -100,7 +150,7 @@ def quit_button(label: str = "Quit app", *, key: str = "quit_app_button") -> Non st.session_state["_app_shutting_down"] = True def _hard_exit() -> None: - time.sleep(0.5) + time.sleep(1.0) os._exit(0) threading.Thread(target=_hard_exit, daemon=True).start()