From 3c8d5e4c1cd6928299680621fff59190dbefe1f1 Mon Sep 17 00:00:00 2001 From: Shawn Date: Wed, 17 Jun 2026 21:19:14 -0400 Subject: [PATCH] fix: move blocking subprocess calls off the async event loop Wrap long-running synchronous calls in run_in_executor to prevent the web UI from freezing during: - wifi_scan (~20s scan) - hotspot_enable / hotspot_disable (~60s each) - bluetooth_scan (~12s scan) - start_jack (~15s JACK restart) in set_audio_profile and save_settings Each blocking call now runs in a thread executor, keeping the FastAPI event loop responsive for other requests. Added: functools.partial import, asyncio.get_event_loop() in two handlers. --- src/web/server.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/web/server.py b/src/web/server.py index 2c509f1..c9a8843 100644 --- a/src/web/server.py +++ b/src/web/server.py @@ -21,6 +21,7 @@ import datetime from contextlib import asynccontextmanager from dataclasses import dataclass, field from pathlib import Path +from functools import partial from typing import Any, Optional import uvicorn @@ -1210,7 +1211,9 @@ class WebServer: async def wifi_scan(): """Scan for available WiFi networks.""" from ..system.network import wifi_scan - return {"networks": wifi_scan()} + loop = asyncio.get_event_loop() + networks = await loop.run_in_executor(None, wifi_scan) + return {"networks": networks} @app.get("/api/wifi/saved") async def wifi_saved(): @@ -1258,16 +1261,21 @@ class WebServer: Body: {ssid: str (optional), password: str (optional)} """ from ..system.network import hotspot_enable - return hotspot_enable( - ssid=data.get("ssid", "Pi-Pedal"), - password=data.get("password", "pedal1234"), + loop = asyncio.get_event_loop() + result = await loop.run_in_executor( + None, partial(hotspot_enable, + ssid=data.get("ssid", "Pi-Pedal"), + password=data.get("password", "pedal1234"), + ), ) + return result @app.post("/api/wifi/hotspot/disable") async def wifi_hotspot_disable(): """Disable WiFi hotspot.""" from ..system.network import hotspot_disable - return hotspot_disable() + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, hotspot_disable) # ── Bluetooth endpoints ────────────────────────────────── @@ -1299,7 +1307,8 @@ class WebServer: async def bluetooth_scan(): """Scan for discoverable Bluetooth devices.""" from ..system.bluetooth import bt_scan - return bt_scan(timeout=12) + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, partial(bt_scan, timeout=12)) @app.get("/api/bluetooth/paired") async def bluetooth_paired(): @@ -1426,6 +1435,7 @@ class WebServer: Changes are saved to ~/.pedal/config.yaml so they survive reboot. The switch restarts JACK (brief audio dropout ~1s). """ + loop = asyncio.get_event_loop() audio_sys = self.deps.audio_system if not audio_sys: raise HTTPException(status_code=503, detail="Audio system not available") @@ -1519,13 +1529,13 @@ class WebServer: # exist until jack_client.start() recreates the JACK client below saved_auto_connect = audio_sys.config.auto_connect audio_sys.config.auto_connect = False - ok = audio_sys.start_jack(timeout=15) + ok = await loop.run_in_executor(None, partial(audio_sys.start_jack, timeout=15)) audio_sys.config.auto_connect = saved_auto_connect if not ok: # Rollback on failure audio_sys.config.profile = old_key logger.warning("JACK restart failed — attempting rollback to %s", old_key) - audio_sys.start_jack(timeout=15) + ok = await loop.run_in_executor(None, partial(audio_sys.start_jack, timeout=15)) if jack_client: try: jack_client.start() @@ -1616,6 +1626,7 @@ class WebServer: Special handling: ``brightness`` — UI sends 1–10, stored as 0.1–1.0 float. """ + loop = asyncio.get_event_loop() if self.deps.config is None: raise HTTPException(status_code=503, detail="Config not available") @@ -1670,7 +1681,7 @@ class WebServer: audio_sys.stop_jack() if jack_client: jack_client.stop() - ok = audio_sys.start_jack(timeout=10) + ok = await loop.run_in_executor(None, partial(audio_sys.start_jack, timeout=10)) if not ok: logger.warning("JACK restart failed after device change — rolling back") # Restore old devices is complex, just log the failure