a00bd620a1
Self-hostable game-server control panel: controller + agent + 26 game modules. One-line install (prebuilt release, no Go required): curl -fsSL https://git.pdxtechs.com/dbledeez/panel/raw/branch/main/install.sh | sudo bash Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
155 lines
3.9 KiB
Go
155 lines
3.9 KiB
Go
package events
|
|
|
|
import (
|
|
"sync"
|
|
|
|
panelv1 "github.com/dbledeez/panel/proto/panel/v1"
|
|
)
|
|
|
|
// LogBuffer is a bounded per-instance ring buffer of recent events. Its
|
|
// purpose is UX: when the operator opens the Console tab for an instance,
|
|
// we want them to see what's been going on — not just events that happen
|
|
// *after* they subscribed. The buffer tails the event bus continuously so
|
|
// a short history is always available.
|
|
//
|
|
// Events are stored verbatim (full *Event payload) so the caller can render
|
|
// them exactly like the live SSE stream would. Only log, player, and
|
|
// instance_state payloads are retained — stats and app_state churn too
|
|
// fast and aren't what "Console history" should show.
|
|
type LogBuffer struct {
|
|
bus *Bus
|
|
cap int
|
|
mu sync.Mutex
|
|
byInst map[string]*ringSlice
|
|
stopped bool
|
|
done chan struct{}
|
|
}
|
|
|
|
// NewLogBuffer creates the buffer and starts a background goroutine that
|
|
// consumes the bus. Call Stop to shut down.
|
|
//
|
|
// capPerInstance is the maximum number of events kept per instance id. The
|
|
// UI typically asks for the last 400, so 500 is a comfortable default.
|
|
func NewLogBuffer(bus *Bus, capPerInstance int) *LogBuffer {
|
|
if capPerInstance <= 0 {
|
|
capPerInstance = 500
|
|
}
|
|
b := &LogBuffer{
|
|
bus: bus,
|
|
cap: capPerInstance,
|
|
byInst: map[string]*ringSlice{},
|
|
done: make(chan struct{}),
|
|
}
|
|
go b.run()
|
|
return b
|
|
}
|
|
|
|
func (b *LogBuffer) run() {
|
|
defer close(b.done)
|
|
sub := b.bus.Subscribe(Filter{})
|
|
defer sub.Close()
|
|
for ev := range sub.Events() {
|
|
if !isRetained(ev) {
|
|
continue
|
|
}
|
|
id := eventInstanceID(ev)
|
|
if id == "" {
|
|
continue
|
|
}
|
|
b.mu.Lock()
|
|
if b.stopped {
|
|
b.mu.Unlock()
|
|
return
|
|
}
|
|
r := b.byInst[id]
|
|
if r == nil {
|
|
r = newRing(b.cap)
|
|
b.byInst[id] = r
|
|
}
|
|
r.push(ev)
|
|
b.mu.Unlock()
|
|
}
|
|
}
|
|
|
|
// Recent returns up to `limit` most-recent retained events for an instance,
|
|
// oldest first. Safe to call concurrently with Run.
|
|
func (b *LogBuffer) Recent(instanceID string, limit int) []*panelv1.Event {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
r := b.byInst[instanceID]
|
|
if r == nil {
|
|
return nil
|
|
}
|
|
return r.tail(limit)
|
|
}
|
|
|
|
// Forget drops the buffer for an instance. Called when an instance is
|
|
// deleted so memory doesn't leak for dead ids.
|
|
func (b *LogBuffer) Forget(instanceID string) {
|
|
b.mu.Lock()
|
|
delete(b.byInst, instanceID)
|
|
b.mu.Unlock()
|
|
}
|
|
|
|
// Stop ends the background goroutine. Safe to call multiple times.
|
|
func (b *LogBuffer) Stop() {
|
|
b.mu.Lock()
|
|
if b.stopped {
|
|
b.mu.Unlock()
|
|
return
|
|
}
|
|
b.stopped = true
|
|
b.mu.Unlock()
|
|
}
|
|
|
|
// isRetained decides whether an event belongs in the console history.
|
|
//
|
|
// - log lines: yes, that's the whole point.
|
|
// - player events: yes — joins/leaves/chat/deaths belong on the console.
|
|
// - instance_state: yes — "server started"-style transitions are context.
|
|
// - stats / app_state: no — churn too fast; would push real content out
|
|
// of the ring.
|
|
func isRetained(e *panelv1.Event) bool {
|
|
switch e.Payload.(type) {
|
|
case *panelv1.Event_Log, *panelv1.Event_Player, *panelv1.Event_InstanceState:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ringSlice is a fixed-capacity FIFO. Implemented as a slice with head/len
|
|
// tracking to avoid allocating on steady-state appends.
|
|
type ringSlice struct {
|
|
items []*panelv1.Event
|
|
head int // index of the oldest entry (when len == cap)
|
|
len int
|
|
cap int
|
|
}
|
|
|
|
func newRing(cap int) *ringSlice {
|
|
return &ringSlice{items: make([]*panelv1.Event, cap), cap: cap}
|
|
}
|
|
|
|
func (r *ringSlice) push(e *panelv1.Event) {
|
|
if r.len < r.cap {
|
|
r.items[(r.head+r.len)%r.cap] = e
|
|
r.len++
|
|
return
|
|
}
|
|
// At capacity: overwrite the oldest.
|
|
r.items[r.head] = e
|
|
r.head = (r.head + 1) % r.cap
|
|
}
|
|
|
|
func (r *ringSlice) tail(limit int) []*panelv1.Event {
|
|
if limit <= 0 || limit > r.len {
|
|
limit = r.len
|
|
}
|
|
out := make([]*panelv1.Event, 0, limit)
|
|
start := (r.head + r.len - limit + r.cap) % r.cap
|
|
for i := 0; i < limit; i++ {
|
|
out = append(out, r.items[(start+i)%r.cap])
|
|
}
|
|
return out
|
|
}
|