Files
uv-k5-v3-emulator/tools/uvk5_logs.py
T
mckero f2c5c6b31b Attribute log entries to the client IP
Entries gain an "ip" field, rendered between the time and the source as asked.
The buffer is shared by every viewer, so without attribution a log of keypresses
from two people is unreadable.

Resolving the address matters more than it looks: behind the nginx reverse proxy
REMOTE_ADDR is always 127.0.0.1, so the first hop of X-Forwarded-For is what
identifies the real client. Only the first entry is trusted -- the rest of the
chain is set by the caller and a test covers that.

Power actions are logged at the route rather than in the supervisor, which has no
request context, so "who powered it off" is recorded.

Entries with no client behind them keep ip=None and render as "-": firmware serial
and QEMU stderr are not caused by a request.

The sharing and history the user asked for already worked and needed no change --
verified rather than assumed. The front end starts at logCursor=0, so a page
opened now receives the full buffer, including lines produced before it connected
and lines from other people. Confirmed live through the proxy: a new reader saw
entries attributed to 172.21.91.140, fd3c:3f9b:6424:2::5 and "-".
2026-08-28 11:37:06 +01:00

69 lines
2.5 KiB
Python

#!/usr/bin/env python3
"""A bounded log buffer for the web UI.
Collects supervisor events, QEMU stderr, and firmware serial output (which the
machine model prints as "SERIAL <line>") so the browser has something to show.
In memory and bounded on purpose: this is a debugging aid inside a long-running
server, so an unbounded buffer would be a slow leak. Anyone wanting a permanent
record can redirect the server's own stderr to a file.
Every entry carries a monotonic `seq`, so a polling client can ask for "anything
after N" and get each line exactly once even when older entries have been evicted.
"""
import collections
import threading
import time
class LogBuffer:
def __init__(self, capacity: int = 500):
self._entries = collections.deque(maxlen=capacity)
self._lock = threading.Lock()
self._seq = 0
def add(self, source: str, text: str, ip: str = None):
"""Record one line. `ip` identifies the client that caused it.
The buffer is shared by every viewer, so without an attributed IP a log of
keypresses from two people is unreadable. Entries with no client behind
them -- firmware serial, QEMU stderr -- carry None.
"""
with self._lock:
self._seq += 1
self._entries.append({
"seq": self._seq,
"time": time.strftime("%H:%M:%S"),
"ip": ip,
"source": source,
"text": text,
})
def cursor(self) -> int:
with self._lock:
return self._seq
def entries(self, since: int = 0):
with self._lock:
return [e for e in self._entries if e["seq"] > since]
def pump_stream(self, stream, default_source: str = "qemu"):
"""Read a byte stream to EOF, one entry per line.
Lines the machine model tags with "SERIAL " are firmware output and are
recorded under their own source, so the UI can tell them apart from QEMU's
own chatter.
Decoding is lenient: serial bytes can be garbage before the firmware has
configured the port, and losing the whole stream to one bad byte would be
worse than showing a replacement character.
"""
for raw in iter(stream.readline, b""):
line = raw.decode("utf-8", "replace").rstrip("\r\n")
if not line:
continue
if line.startswith("SERIAL "):
self.add("serial", line[len("SERIAL "):])
else:
self.add(default_source, line)