Менеджер запускает сервер PZ как дочерний процесс и держит его stdin/stdout: отсюда живая консоль без RCON, стриминг логов через SSE и корректная остановка по цепочке quit -> SIGTERM -> SIGKILL для всей группы процессов. Сам менеджер работает под systemd. Что входит: - управление жизненным циклом сервера, автозапуск и автоподъём после падения; - метрики процесса из /proc и состояние хоста, список игроков онлайн; - редакторы servertest.ini и SandboxVars.lua, сохраняющие исходное форматирование файлов; - моды: списки Workshop ID и Mod ID с порядком загрузки, названия из Steam; - бэкапы мира по расписанию и вручную, с ротацией и восстановлением; - установка и обновление сервера через SteamCMD; - вход по логину и паролю, первичная настройка по одноразовому коду. Веб-интерфейс без сборщика, вшит в бинарник через embed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
92 lines
2.4 KiB
Go
92 lines
2.4 KiB
Go
package pzserver
|
|
|
|
import "sync"
|
|
|
|
// LogLine — одна строка консоли сервера с монотонным номером, по которому
|
|
// веб-клиент понимает, что он пропустил, и откуда догружать.
|
|
type LogLine struct {
|
|
Seq int64 `json:"seq"`
|
|
Time int64 `json:"time"` // Unix-миллисекунды
|
|
Stream string `json:"stream"`
|
|
Text string `json:"text"`
|
|
}
|
|
|
|
// logBuffer — кольцевой буфер последних строк консоли плюс рассылка их
|
|
// живым подписчикам (SSE-соединениям веб-панели).
|
|
type logBuffer struct {
|
|
mu sync.RWMutex
|
|
lines []LogLine
|
|
capacity int
|
|
nextSeq int64
|
|
subs map[int]chan LogLine
|
|
nextSub int
|
|
}
|
|
|
|
func newLogBuffer(capacity int) *logBuffer {
|
|
return &logBuffer{
|
|
lines: make([]LogLine, 0, capacity),
|
|
capacity: capacity,
|
|
nextSeq: 1,
|
|
subs: make(map[int]chan LogLine),
|
|
}
|
|
}
|
|
|
|
// append кладёт строку в буфер и рассылает подписчикам.
|
|
func (b *logBuffer) append(line LogLine) {
|
|
b.mu.Lock()
|
|
line.Seq = b.nextSeq
|
|
b.nextSeq++
|
|
if len(b.lines) == b.capacity {
|
|
copy(b.lines, b.lines[1:])
|
|
b.lines = b.lines[:len(b.lines)-1]
|
|
}
|
|
b.lines = append(b.lines, line)
|
|
|
|
subs := make([]chan LogLine, 0, len(b.subs))
|
|
for _, ch := range b.subs {
|
|
subs = append(subs, ch)
|
|
}
|
|
b.mu.Unlock()
|
|
|
|
for _, ch := range subs {
|
|
// Неблокирующая отправка: подписчик, который не успевает читать,
|
|
// теряет строки, но не тормозит вывод сервера.
|
|
select {
|
|
case ch <- line:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
// since возвращает строки с номером больше seq (seq<=0 — весь буфер).
|
|
func (b *logBuffer) since(seq int64) []LogLine {
|
|
b.mu.RLock()
|
|
defer b.mu.RUnlock()
|
|
out := make([]LogLine, 0, len(b.lines))
|
|
for _, l := range b.lines {
|
|
if l.Seq > seq {
|
|
out = append(out, l)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// subscribe открывает канал живых строк и функцию отписки.
|
|
func (b *logBuffer) subscribe(bufferSize int) (<-chan LogLine, func()) {
|
|
ch := make(chan LogLine, bufferSize)
|
|
b.mu.Lock()
|
|
id := b.nextSub
|
|
b.nextSub++
|
|
b.subs[id] = ch
|
|
b.mu.Unlock()
|
|
|
|
return ch, func() {
|
|
b.mu.Lock()
|
|
if existing, ok := b.subs[id]; ok {
|
|
delete(b.subs, id)
|
|
close(existing)
|
|
}
|
|
b.mu.Unlock()
|
|
}
|
|
}
|