fix: address review bugs in server implementation

- Fix keyframe 6-tuple → 4-tuple mismatch crashing ExportRunner
- Fix ws.broadcast() using wrong event loop from background threads
- Fix export counter hardcoded to 1, now auto-increments
- Add path traversal protection to file/stream/delete endpoints
- Use proper HTTP error codes (was returning 200 for errors)
- Add thread safety to WebSocket connection list
- Record exports to DB so markers appear
- Move WS endpoint to /ws/export (was /api/ws/export)
- Prune dead threads from cache job tracker

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-16 13:55:25 +02:00
parent 3d6469c60c
commit 2200da491f
6 changed files with 100 additions and 44 deletions
+20 -16
View File
@@ -1,37 +1,41 @@
import asyncio
import json
import threading
from fastapi import WebSocket, WebSocketDisconnect
_lock = threading.Lock()
_connections: list[WebSocket] = []
_loop: asyncio.AbstractEventLoop | None = None
async def connect(ws: WebSocket):
global _loop
_loop = asyncio.get_running_loop()
await ws.accept()
_connections.append(ws)
with _lock:
_connections.append(ws)
try:
while True:
await ws.receive_text() # keep alive
except WebSocketDisconnect:
_connections.remove(ws)
with _lock:
if ws in _connections:
_connections.remove(ws)
def broadcast(msg: dict):
"""Send a message to all connected WebSocket clients.
Called from sync code (export callbacks), so we schedule the coroutine
on each connection's event loop.
Called from sync code (export callbacks running in background threads),
so we schedule sends on uvicorn's event loop.
"""
if _loop is None:
return
data = json.dumps(msg)
stale = []
for ws in _connections:
try:
loop = asyncio.get_event_loop()
if loop.is_running():
asyncio.run_coroutine_threadsafe(ws.send_text(data), loop)
else:
loop.run_until_complete(ws.send_text(data))
except Exception:
stale.append(ws)
for ws in stale:
_connections.remove(ws)
with _lock:
for ws in list(_connections):
try:
asyncio.run_coroutine_threadsafe(ws.send_text(data), _loop)
except Exception:
pass