"""Module-level holder for the uvicorn Server and TrayManager references. Allows the shutdown API endpoint to trigger graceful shutdown via ``server.should_exit = True`` + ``tray.stop()``, which is the same mechanism the system tray "Shutdown" menu item uses. """ from typing import Any, Optional from wled_controller.utils import get_logger logger = get_logger(__name__) _server: Optional[Any] = None # uvicorn.Server _tray: Optional[Any] = None # TrayManager def set_server(server: Any) -> None: """Store the uvicorn Server instance (called from __main__).""" global _server _server = server def set_tray(tray: Any) -> None: """Store the TrayManager instance (called from __main__).""" global _tray _tray = tray def request_shutdown() -> None: """Signal uvicorn + tray to perform a graceful shutdown. Broadcasts a ``server_restarting`` event so the frontend can show a restart indicator, persists all stores to disk, then sets ``should_exit = True`` on the uvicorn Server and stops the tray. """ # Notify connected clients that a restart is in progress _broadcast_restarting() # Persist stores before signaling shutdown. # The lifespan shutdown handler also saves, but it may not run # reliably when uvicorn is in a daemon thread. try: from wled_controller.main import _save_all_stores _save_all_stores() except Exception as e: logger.debug("Best-effort store save on shutdown failed: %s", e) pass # best-effort; lifespan handler is the backup if _server is not None: _server.should_exit = True if _tray is not None: _tray.stop() def _broadcast_restarting() -> None: """Push a server_restarting event to all connected WebSocket clients.""" try: from wled_controller.api.dependencies import _deps pm = _deps.get("processor_manager") if pm is not None: pm.fire_event({"type": "server_restarting"}) except Exception as e: logger.debug("Failed to broadcast server_restarting event: %s", e) pass