Веб-панель управления сервером Project Zomboid
Менеджер запускает сервер 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>
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user