simpleadmin-web 0.1.0: web panel for CS2-SimpleAdmin
This commit is contained in:
commit
4f38daf0e6
31 changed files with 6870 additions and 0 deletions
171
internal/live/live.go
Normal file
171
internal/live/live.go
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
// Package live polls the game server through the WebPanelBridge plugin and caches the result, so
|
||||
// page views never wait on RCON.
|
||||
package live
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.zio.sh/cs2/simpleadmin-web/internal/rcon"
|
||||
)
|
||||
|
||||
const (
|
||||
TeamSpectator = 1
|
||||
TeamT = 2
|
||||
TeamCT = 3
|
||||
)
|
||||
|
||||
type Player struct {
|
||||
UserID int `json:"userid"`
|
||||
Name string `json:"name"`
|
||||
SteamID string `json:"steamid"`
|
||||
Team int `json:"team"`
|
||||
Kills int `json:"kills"`
|
||||
Deaths int `json:"deaths"`
|
||||
Ping int `json:"ping"`
|
||||
Bot bool `json:"bot"`
|
||||
}
|
||||
|
||||
type Status struct {
|
||||
Online bool `json:"online"`
|
||||
Error string `json:"-"`
|
||||
Hostname string `json:"hostname"`
|
||||
Map string `json:"map"`
|
||||
MaxPlayers int `json:"maxPlayers"`
|
||||
ScoreT int `json:"scoreT"`
|
||||
ScoreCT int `json:"scoreCt"`
|
||||
Warmup bool `json:"warmup"`
|
||||
Players []Player `json:"players"`
|
||||
Updated time.Time `json:"updated"`
|
||||
}
|
||||
|
||||
// Humans returns the connected players that aren't bots.
|
||||
func (s Status) Humans() []Player {
|
||||
out := make([]Player, 0, len(s.Players))
|
||||
for _, p := range s.Players {
|
||||
if !p.Bot {
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Find returns the online player with this SteamID64.
|
||||
func (s Status) Find(steamid string) (Player, bool) {
|
||||
for _, p := range s.Players {
|
||||
if !p.Bot && p.SteamID == steamid {
|
||||
return p, true
|
||||
}
|
||||
}
|
||||
return Player{}, false
|
||||
}
|
||||
|
||||
var errNoBridge = errors.New("WebPanelBridge didn't answer; is the plugin installed?")
|
||||
|
||||
// Parse reads css_webpanel_status output.
|
||||
func Parse(out string) (Status, error) {
|
||||
var st Status
|
||||
gotServer, gotEnd := false, false
|
||||
for line := range strings.SplitSeq(out, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
rest, ok := strings.CutPrefix(line, "wpb ")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if rest == "end" {
|
||||
gotEnd = true
|
||||
continue
|
||||
}
|
||||
if !gotServer {
|
||||
var srv struct {
|
||||
V int `json:"v"`
|
||||
Hostname string `json:"hostname"`
|
||||
Map string `json:"map"`
|
||||
MaxPlayers int `json:"maxPlayers"`
|
||||
ScoreT int `json:"scoreT"`
|
||||
ScoreCt int `json:"scoreCt"`
|
||||
Warmup bool `json:"warmup"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(rest), &srv); err != nil || srv.V != 1 {
|
||||
return st, errors.New("unrecognised WebPanelBridge output")
|
||||
}
|
||||
st.Hostname, st.Map, st.MaxPlayers = srv.Hostname, srv.Map, srv.MaxPlayers
|
||||
st.ScoreT, st.ScoreCT, st.Warmup = srv.ScoreT, srv.ScoreCt, srv.Warmup
|
||||
gotServer = true
|
||||
continue
|
||||
}
|
||||
var p Player
|
||||
if err := json.Unmarshal([]byte(rest), &p); err == nil {
|
||||
st.Players = append(st.Players, p)
|
||||
}
|
||||
}
|
||||
if !gotServer {
|
||||
return st, errNoBridge
|
||||
}
|
||||
if !gotEnd {
|
||||
return st, errors.New("WebPanelBridge output was cut off")
|
||||
}
|
||||
st.Online = true
|
||||
return st, nil
|
||||
}
|
||||
|
||||
type Poller struct {
|
||||
rc *rcon.Client
|
||||
interval time.Duration
|
||||
|
||||
mu sync.RWMutex
|
||||
current Status
|
||||
}
|
||||
|
||||
func NewPoller(rc *rcon.Client, interval time.Duration) *Poller {
|
||||
return &Poller{rc: rc, interval: interval}
|
||||
}
|
||||
|
||||
// Run polls until ctx is done.
|
||||
func (p *Poller) Run(ctx context.Context) {
|
||||
t := time.NewTicker(p.interval)
|
||||
defer t.Stop()
|
||||
p.Refresh()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
p.Refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh polls now. Actions call it so the next page load shows their effect.
|
||||
func (p *Poller) Refresh() Status {
|
||||
out, err := p.rc.Exec("css_webpanel_status")
|
||||
var st Status
|
||||
if err == nil {
|
||||
st, err = Parse(out)
|
||||
}
|
||||
st.Updated = time.Now()
|
||||
if err != nil {
|
||||
st = Status{Error: err.Error(), Updated: st.Updated}
|
||||
p.mu.RLock()
|
||||
was := p.current.Online || p.current.Updated.IsZero()
|
||||
p.mu.RUnlock()
|
||||
if was && !errors.Is(err, rcon.ErrDown) {
|
||||
slog.Warn("game server status unavailable", "err", err)
|
||||
}
|
||||
}
|
||||
p.mu.Lock()
|
||||
p.current = st
|
||||
p.mu.Unlock()
|
||||
return st
|
||||
}
|
||||
|
||||
func (p *Poller) Current() Status {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
return p.current
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue