Files
VoiceStudio/backend/worker/inbound/connection_log.py
T
velixio 33714b2fe0 fix(workers): make Disconnect hold, and stop a bad paste from replacing a good key
Two more found on hardware.

Disconnect ended the session and the panel redialled two seconds later, so the
log read disconnected and connected in the same breath and the button appeared
to do nothing. A kicked key now sits out for a minute — long enough that the
disconnect is real and the person notices, short enough that it is plainly not
a revocation, which stays a separate and permanent action. The docs now say
which of the two buttons does which.

Re-pasting a connection string for an already-connected machine saved the new
string and then short-circuited on the existing session, so a wrong key
reported success, kept running on the old connection, and only failed after a
restart — by which point nothing pointed back at the paste that caused it. The
live session is now torn down before the new one is dialled.
2026-08-11 23:22:47 +05:30

163 lines
6.0 KiB
Python

"""What the node owner can see, and what they can do about it.
Inbound mode has no per-job approval prompt — the enable toggle is the consent
surface, and a prompt per job would make a shared GPU unusable. That trade only
holds if "who is using my machine right now" is answerable at a glance and
actable in one click. Without this, a key that leaked is invisible until the
electricity bill.
Events are kept in memory with a hard cap. Persisting them would put a record
of other people's activity on disk by default, which is a bigger promise than
this feature needs to make.
"""
from __future__ import annotations
import threading
import time
from collections import deque
from dataclasses import dataclass
from typing import Callable, Optional
# Enough to cover a working day of joins and drops without becoming a log file
# nobody rotates.
_MAX_EVENTS = 200
# How long a kicked panel stays out. Long enough that the disconnect is
# visible and the person notices; short enough that it is plainly not a
# revocation, which is a separate and permanent action.
_KICK_COOLDOWN_SECONDS = 60.0
@dataclass
class Session:
"""One panel currently attached to this node."""
session_id: str
key_id: str
label: str
peer: str
connected_at: float
tasks_run: int = 0
# Set when the owner kicks the session. The stream loop checks it, so a
# disconnect that arrives mid-task ends cleanly rather than by exception.
disconnect_requested: bool = False
@dataclass
class Event:
at: float
kind: str # connected | disconnected | rejected | kicked
label: str = ""
peer: str = ""
detail: str = ""
class ConnectionLog:
def __init__(self, *, now: Optional[Callable[[], float]] = None) -> None:
self._now = now or time.time
self._lock = threading.Lock()
self._sessions: dict[str, Session] = {}
self._events: deque[Event] = deque(maxlen=_MAX_EVENTS)
self._cooldowns: dict[str, float] = {}
# ── Sessions ──────────────────────────────────────────────────────────
def opened(self, *, session_id: str, key_id: str, label: str, peer: str) -> Session:
session = Session(
session_id=session_id,
key_id=key_id,
label=label,
peer=peer,
connected_at=self._now(),
)
with self._lock:
self._sessions[session_id] = session
self._events.append(
Event(at=session.connected_at, kind="connected", label=label, peer=peer)
)
return session
def closed(self, session_id: str, *, detail: str = "") -> None:
with self._lock:
session = self._sessions.pop(session_id, None)
if session is None:
return
self._events.append(
Event(
at=self._now(),
kind="kicked" if session.disconnect_requested else "disconnected",
label=session.label,
peer=session.peer,
detail=detail,
)
)
def rejected(self, *, peer: str, detail: str) -> None:
"""A refused attempt is the event that matters most and the one a
success-only log would omit entirely."""
with self._lock:
self._events.append(
Event(at=self._now(), kind="rejected", peer=peer, detail=detail)
)
def task_started(self, session_id: str) -> None:
with self._lock:
session = self._sessions.get(session_id)
if session is not None:
session.tasks_run += 1
def kick(self, session_id: str) -> bool:
"""Ask a session to end. Returns False if it already went away."""
with self._lock:
session = self._sessions.get(session_id)
if session is None:
return False
session.disconnect_requested = True
# A panel reconnects on its own, so without this the person is back
# within two seconds and the button appears to do nothing —
# verified on hardware, where the log read disconnected/connected
# in the same breath. The cooldown makes the disconnect visible and
# deliberately does NOT last: revoking the key is how you stop
# somebody for good, and a kick that silently became permanent
# would be a different promise than the button makes.
self._cooldowns[session.key_id] = self._now() + _KICK_COOLDOWN_SECONDS
return True
def cooling_down(self, key_id: str) -> bool:
with self._lock:
return self._cooldowns.get(key_id, 0.0) > self._now()
def disconnect_requested(self, session_id: str) -> bool:
with self._lock:
session = self._sessions.get(session_id)
return session is not None and session.disconnect_requested
# ── Reporting ─────────────────────────────────────────────────────────
def snapshot(self) -> dict:
with self._lock:
return {
"sessions": [
{
"session_id": s.session_id,
"key_id": s.key_id,
"label": s.label,
"peer": s.peer,
"connected_at": s.connected_at,
"tasks_run": s.tasks_run,
}
for s in sorted(self._sessions.values(), key=lambda s: s.connected_at)
],
"events": [
{
"at": e.at,
"kind": e.kind,
"label": e.label,
"peer": e.peer,
"detail": e.detail,
}
for e in reversed(self._events)
],
}