left4me/l4d2web/routes/log_routes.py
mwiegand 4552af6544
fix(l4d2-web): keep SSE log stream from pinning gunicorn threads
stream_command used a blocking proc.stdout.readline() that never woke
when the underlying journalctl was silent, so Flask never delivered
GeneratorExit on client disconnect — the worker thread and the journalctl
child both leaked permanently and pinned the gunicorn thread pool.

Switch to a select-based read loop with a 15s heartbeat tick (yielded as
""), and translate the tick to an SSE keepalive comment in the log route.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 11:18:56 +02:00

36 lines
1.1 KiB
Python

from flask import Blueprint, Response
from sqlalchemy import select
from l4d2web.auth import current_user, require_login
from l4d2web.db import session_scope
from l4d2web.models import Server
from l4d2web.services import l4d2_facade as facade
bp = Blueprint("logs", __name__)
def load_authorized_server(server_id: int) -> Server | None:
user = current_user()
if user is None:
return None
with session_scope() as db:
server = db.scalar(select(Server).where(Server.id == server_id, Server.user_id == user.id))
return server
@bp.get("/servers/<int:server_id>/logs/stream")
@require_login
def stream_server_logs(server_id: int) -> Response:
server = load_authorized_server(server_id)
if server is None:
return Response(status=404)
def generate():
for line in facade.stream_server_logs(server.name, lines=200, follow=True):
if line == "":
yield ": keepalive\n\n"
else:
yield f"data: {line}\n\n"
return Response(generate(), mimetype="text/event-stream")