From 824f4f587c4df1896ec414d2ae9f5286adaea80f Mon Sep 17 00:00:00 2001 From: Kai Chappell Date: Thu, 6 Nov 2025 09:52:35 +0000 Subject: [PATCH] Fix server initialization race condition with proper event signaling --- src/py_dvt_ate/app/dashboard/app.py | 51 ++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/src/py_dvt_ate/app/dashboard/app.py b/src/py_dvt_ate/app/dashboard/app.py index 811d879..d06881b 100644 --- a/src/py_dvt_ate/app/dashboard/app.py +++ b/src/py_dvt_ate/app/dashboard/app.py @@ -61,25 +61,34 @@ def start_embedded_server() -> tuple[SimulationServer, threading.Thread]: ) ) + server_ready = threading.Event() + def run_server() -> None: """Run the async server in a new event loop.""" loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) try: loop.run_until_complete(server.start()) + # Signal that server is ready + server_ready.set() # Keep the event loop running loop.run_forever() except Exception as e: st.error(f"Server error: {e}") + server_ready.set() # Signal even on error finally: - loop.run_until_complete(server.stop()) + try: + loop.run_until_complete(server.stop()) + except Exception: + pass loop.close() thread = threading.Thread(target=run_server, daemon=True) thread.start() - # Wait a moment for server to start - time.sleep(0.5) + # Wait for server to be fully started (up to 5 seconds) + if not server_ready.wait(timeout=5.0): + st.error("Server failed to start within timeout") return server, thread @@ -87,13 +96,22 @@ def start_embedded_server() -> tuple[SimulationServer, threading.Thread]: def init_session_state() -> None: """Initialise Streamlit session state.""" if "server" not in st.session_state: - st.session_state.server, st.session_state.server_thread = start_embedded_server() + with st.spinner("Starting simulation server..."): + st.session_state.server, st.session_state.server_thread = start_embedded_server() + + # Verify server started correctly + if st.session_state.server.physics_engine is None: + st.error("Failed to start simulation server. Please refresh the page.") + st.stop() # Register cleanup def cleanup() -> None: if hasattr(st.session_state, "server") and st.session_state.server is not None: loop = asyncio.new_event_loop() - loop.run_until_complete(st.session_state.server.stop()) + try: + loop.run_until_complete(st.session_state.server.stop()) + except Exception: + pass loop.close() atexit.register(cleanup) @@ -308,7 +326,24 @@ def display_controls() -> None: @st.fragment(run_every=0.1) def simulation_display() -> None: """Fragment that displays and updates simulation state.""" + # Check if server is initialized + if "server" not in st.session_state: + st.warning("⏳ Initializing simulation server...") + return + server: SimulationServer = st.session_state.server + + # Get current state from physics engine (for visualization) + engine: PhysicsEngine | None = server.physics_engine + if engine is None: + st.error("❌ Physics engine not available. The server may not have started correctly. Try refreshing the page.") + return + + # Check if server is running + if not server.is_running: + st.warning("⚠️ Server is not running. Try refreshing the page.") + return + instruments: InstrumentSet = st.session_state.instruments history: SimulationHistory = st.session_state.history @@ -319,12 +354,6 @@ def simulation_display() -> None: if st.session_state.running: step_simulation() - # Get current state from physics engine (for visualization) - engine: PhysicsEngine | None = server.physics_engine - if engine is None: - st.error("Physics engine not available") - return - thermal = engine.get_thermal_state() electrical = engine.get_electrical_state()