Files
RTSP-Streamer/internal/logbuf/logbuf.go
Levi Woodard be1b53ae04 Restart without reboot; retrievable logs incl. mpv exit reasons
Two related gaps on the headless kiosk: no way to restart the wall short
of a full reboot, and no way to see why a stream keeps flapping (the
daemon's stderr goes to sway on tty1, and mpv's stderr was discarded).

Restart in place:
- `rtsp-streamer restart` sends a control-socket command; the daemon
  tears down its mpv children and syscall.Exec's the on-disk binary.
  Same PID, same parent (sway), same env — keeps WAYLAND_DISPLAY/
  SWAYSOCK and comes back up on the new binary, no reboot. Handles the
  os.Executable() "(deleted)" sentinel from make install's rename.
- sway config now launches the daemon in a relaunch loop, so a crash (or
  the restart) auto-recovers instead of leaving a black screen.
- `make deploy` now does `install` + `restart` instead of restarting
  getty@tty1 (which left stale duplicate sessions and forced reboots).

Retrievable logs:
- New internal/logbuf ring; the daemon tees slog output into it and
  serves the tail over the socket via `rtsp-streamer logs [-n N]` —
  readable over SSH, no file wrangling, no reboot.
- Capture the tail of each mpv's stderr and log its last line when the
  process exits, so "mpv exited, will restart" now carries the reason
  (connection refused, unsupported codec, 401, ...). This is the
  diagnostic for a single tile going unhealthy repeatedly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 10:03:25 -05:00

61 lines
1.7 KiB
Go

// Package logbuf provides an in-memory ring of recent log lines. The daemon
// tees its slog output into one of these so `rtsp-streamer logs` can retrieve
// recent activity over the control socket — the headless Pi has no reachable
// place for the daemon's stderr to land (it goes to sway on tty1), so keeping
// a buffer in the process is the practical way to inspect it over SSH.
package logbuf
import (
"strings"
"sync"
)
// Writer is an io.Writer that retains the last N newline-delimited lines
// written to it. It is safe for concurrent use, so it can sit behind an
// io.MultiWriter alongside os.Stderr in a slog handler.
type Writer struct {
mu sync.Mutex
lines []string
max int
}
// New returns a ring holding at most max lines.
func New(max int) *Writer {
if max < 1 {
max = 1
}
return &Writer{max: max, lines: make([]string, 0, max)}
}
// Write appends each complete line in p to the ring, dropping the oldest when
// it overflows. It always reports the full length written so it never trips up
// the MultiWriter it sits behind.
func (w *Writer) Write(p []byte) (int, error) {
w.mu.Lock()
defer w.mu.Unlock()
for _, line := range strings.Split(strings.TrimRight(string(p), "\n"), "\n") {
if line == "" {
continue
}
w.lines = append(w.lines, line)
}
if len(w.lines) > w.max {
w.lines = append(w.lines[:0], w.lines[len(w.lines)-w.max:]...)
}
return len(p), nil
}
// Lines returns a copy of the buffered lines, oldest first. When n > 0 only
// the most recent n are returned.
func (w *Writer) Lines(n int) []string {
w.mu.Lock()
defer w.mu.Unlock()
start := 0
if n > 0 && n < len(w.lines) {
start = len(w.lines) - n
}
out := make([]string, len(w.lines)-start)
copy(out, w.lines[start:])
return out
}