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 contextlib import asynccontextmanager
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from functools import partial
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
@@ -1210,7 +1211,9 @@ class WebServer:
|
|||||||
async def wifi_scan():
|
async def wifi_scan():
|
||||||
"""Scan for available WiFi networks."""
|
"""Scan for available WiFi networks."""
|
||||||
from ..system.network import wifi_scan
|
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")
|
@app.get("/api/wifi/saved")
|
||||||
async def wifi_saved():
|
async def wifi_saved():
|
||||||
@@ -1258,16 +1261,21 @@ class WebServer:
|
|||||||
Body: {ssid: str (optional), password: str (optional)}
|
Body: {ssid: str (optional), password: str (optional)}
|
||||||
"""
|
"""
|
||||||
from ..system.network import hotspot_enable
|
from ..system.network import hotspot_enable
|
||||||
return hotspot_enable(
|
loop = asyncio.get_event_loop()
|
||||||
ssid=data.get("ssid", "Pi-Pedal"),
|
result = await loop.run_in_executor(
|
||||||
password=data.get("password", "pedal1234"),
|
None, partial(hotspot_enable,
|
||||||
|
ssid=data.get("ssid", "Pi-Pedal"),
|
||||||
|
password=data.get("password", "pedal1234"),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
@app.post("/api/wifi/hotspot/disable")
|
@app.post("/api/wifi/hotspot/disable")
|
||||||
async def wifi_hotspot_disable():
|
async def wifi_hotspot_disable():
|
||||||
"""Disable WiFi hotspot."""
|
"""Disable WiFi hotspot."""
|
||||||
from ..system.network import hotspot_disable
|
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 ──────────────────────────────────
|
# ── Bluetooth endpoints ──────────────────────────────────
|
||||||
|
|
||||||
@@ -1299,7 +1307,8 @@ class WebServer:
|
|||||||
async def bluetooth_scan():
|
async def bluetooth_scan():
|
||||||
"""Scan for discoverable Bluetooth devices."""
|
"""Scan for discoverable Bluetooth devices."""
|
||||||
from ..system.bluetooth import bt_scan
|
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")
|
@app.get("/api/bluetooth/paired")
|
||||||
async def bluetooth_paired():
|
async def bluetooth_paired():
|
||||||
@@ -1426,6 +1435,7 @@ class WebServer:
|
|||||||
Changes are saved to ~/.pedal/config.yaml so they survive reboot.
|
Changes are saved to ~/.pedal/config.yaml so they survive reboot.
|
||||||
The switch restarts JACK (brief audio dropout ~1s).
|
The switch restarts JACK (brief audio dropout ~1s).
|
||||||
"""
|
"""
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
audio_sys = self.deps.audio_system
|
audio_sys = self.deps.audio_system
|
||||||
if not audio_sys:
|
if not audio_sys:
|
||||||
raise HTTPException(status_code=503, detail="Audio system not available")
|
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
|
# exist until jack_client.start() recreates the JACK client below
|
||||||
saved_auto_connect = audio_sys.config.auto_connect
|
saved_auto_connect = audio_sys.config.auto_connect
|
||||||
audio_sys.config.auto_connect = False
|
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
|
audio_sys.config.auto_connect = saved_auto_connect
|
||||||
if not ok:
|
if not ok:
|
||||||
# Rollback on failure
|
# Rollback on failure
|
||||||
audio_sys.config.profile = old_key
|
audio_sys.config.profile = old_key
|
||||||
logger.warning("JACK restart failed — attempting rollback to %s", 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:
|
if jack_client:
|
||||||
try:
|
try:
|
||||||
jack_client.start()
|
jack_client.start()
|
||||||
@@ -1616,6 +1626,7 @@ class WebServer:
|
|||||||
Special handling:
|
Special handling:
|
||||||
``brightness`` — UI sends 1–10, stored as 0.1–1.0 float.
|
``brightness`` — UI sends 1–10, stored as 0.1–1.0 float.
|
||||||
"""
|
"""
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
if self.deps.config is None:
|
if self.deps.config is None:
|
||||||
raise HTTPException(status_code=503, detail="Config not available")
|
raise HTTPException(status_code=503, detail="Config not available")
|
||||||
|
|
||||||
@@ -1670,7 +1681,7 @@ class WebServer:
|
|||||||
audio_sys.stop_jack()
|
audio_sys.stop_jack()
|
||||||
if jack_client:
|
if jack_client:
|
||||||
jack_client.stop()
|
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:
|
if not ok:
|
||||||
logger.warning("JACK restart failed after device change — rolling back")
|
logger.warning("JACK restart failed after device change — rolling back")
|
||||||
# Restore old devices is complex, just log the failure
|
# Restore old devices is complex, just log the failure
|
||||||
|
|||||||
Reference in New Issue
Block a user