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.
This commit is contained in:
+20
-9
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user