mirror of
https://github.com/MCKero6423/uv-k5-v3-emulator.git
synced 2026-10-02 03:15:36 +00:00
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 "-".
69 lines
2.5 KiB
Python
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)
|