Add idle auto-shutdown for self-hosted deployment

- IDLE_TIMEOUT_MINUTES env var to configure shutdown after inactivity
- Background thread monitors activity and exits when timeout reached
- Activity tracked via simulation_display fragment (runs while page open)
- Set to 0 (default) to disable auto-shutdown
This commit is contained in:
2026-01-29 21:08:17 +00:00
parent 6830b3158c
commit b7663d5a31

View File

@@ -7,6 +7,8 @@ thermal-electrical coupling in real-time using instrument interfaces.
import asyncio import asyncio
import atexit import atexit
import os
import sys
import threading import threading
import time import time
from collections import deque from collections import deque
@@ -27,6 +29,41 @@ from py_dvt_ate.tests.thermal.tempco import TempCoTest
# Thread pool for background test execution # Thread pool for background test execution
_test_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="test_runner") _test_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="test_runner")
# Idle shutdown configuration
# Set IDLE_TIMEOUT_MINUTES=0 to disable auto-shutdown
IDLE_TIMEOUT_MINUTES = int(os.environ.get("IDLE_TIMEOUT_MINUTES", "0"))
_last_activity_time: float = time.time()
_idle_monitor_started = False
def _update_activity() -> None:
"""Update the last activity timestamp."""
global _last_activity_time
_last_activity_time = time.time()
def _idle_monitor() -> None:
"""Background thread that exits the app after idle timeout."""
global _last_activity_time
while True:
time.sleep(60) # Check every minute
if IDLE_TIMEOUT_MINUTES <= 0:
continue
idle_minutes = (time.time() - _last_activity_time) / 60
if idle_minutes >= IDLE_TIMEOUT_MINUTES:
print(f"Idle timeout reached ({IDLE_TIMEOUT_MINUTES} minutes). Shutting down.")
os._exit(0)
def _start_idle_monitor() -> None:
"""Start the idle monitor thread if timeout is configured."""
global _idle_monitor_started
if IDLE_TIMEOUT_MINUTES > 0 and not _idle_monitor_started:
_idle_monitor_started = True
thread = threading.Thread(target=_idle_monitor, daemon=True)
thread.start()
print(f"Idle auto-shutdown enabled: {IDLE_TIMEOUT_MINUTES} minutes")
# History buffer size for charts # History buffer size for charts
HISTORY_SIZE = 500 HISTORY_SIZE = 500
@@ -124,6 +161,10 @@ def start_embedded_server() -> tuple[SimulationServer, threading.Thread]:
def init_session_state() -> None: def init_session_state() -> None:
"""Initialise Streamlit session state.""" """Initialise Streamlit session state."""
# Start idle monitor for auto-shutdown
_start_idle_monitor()
_update_activity()
if "server" not in st.session_state: if "server" not in st.session_state:
with st.spinner("Starting simulation server..."): with st.spinner("Starting simulation server..."):
st.session_state.server, st.session_state.server_thread = start_embedded_server() st.session_state.server, st.session_state.server_thread = start_embedded_server()
@@ -386,6 +427,8 @@ def display_controls() -> None:
@st.fragment(run_every=0.1) @st.fragment(run_every=0.1)
def simulation_display() -> None: def simulation_display() -> None:
"""Fragment that displays and updates simulation state.""" """Fragment that displays and updates simulation state."""
_update_activity() # Track that someone is viewing the dashboard
if "server" not in st.session_state: if "server" not in st.session_state:
st.warning("Initializing simulation server...") st.warning("Initializing simulation server...")
return return