Веб-панель управления сервером 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,98 @@
|
||||
// Package job выполняет длинные операции (установка сервера, обновление модов,
|
||||
// бэкап) по одной за раз, чтобы они не мешали друг другу.
|
||||
package job
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ErrBusy возвращается, когда другая задача ещё выполняется.
|
||||
var ErrBusy = errors.New("уже выполняется другая задача")
|
||||
|
||||
// Status — что менеджер задач показывает веб-панели.
|
||||
type Status struct {
|
||||
Name string `json:"name"`
|
||||
Running bool `json:"running"`
|
||||
StartedAt int64 `json:"started_at"`
|
||||
FinishedAt int64 `json:"finished_at"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// Func — тело задачи. Логирование идёт через log, отмена — через ctx.
|
||||
type Func func(ctx context.Context, log func(string)) error
|
||||
|
||||
// Manager допускает только одну активную задачу.
|
||||
type Manager struct {
|
||||
logf func(stream, text string)
|
||||
|
||||
mu sync.Mutex
|
||||
status Status
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// New создаёт менеджер задач; logf получает весь вывод задач.
|
||||
func New(logf func(stream, text string)) *Manager {
|
||||
return &Manager{logf: logf}
|
||||
}
|
||||
|
||||
// Start запускает задачу в фоне. Если что-то уже выполняется — ErrBusy.
|
||||
func (m *Manager) Start(name string, fn Func) error {
|
||||
m.mu.Lock()
|
||||
if m.status.Running {
|
||||
m.mu.Unlock()
|
||||
return ErrBusy
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
m.cancel = cancel
|
||||
m.status = Status{Name: name, Running: true, StartedAt: time.Now().Unix()}
|
||||
m.mu.Unlock()
|
||||
|
||||
m.logf("task", "▶ "+name)
|
||||
go func() {
|
||||
defer cancel()
|
||||
err := fn(ctx, func(line string) { m.logf("task", line) })
|
||||
|
||||
m.mu.Lock()
|
||||
m.status.Running = false
|
||||
m.status.FinishedAt = time.Now().Unix()
|
||||
if err != nil {
|
||||
m.status.Error = err.Error()
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
if err != nil {
|
||||
m.logf("task", "✖ "+name+": "+err.Error())
|
||||
return
|
||||
}
|
||||
m.logf("task", "✔ "+name+" — готово")
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cancel просит текущую задачу завершиться.
|
||||
func (m *Manager) Cancel() {
|
||||
m.mu.Lock()
|
||||
cancel := m.cancel
|
||||
running := m.status.Running
|
||||
m.mu.Unlock()
|
||||
if running && cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
// Status отдаёт состояние последней (или текущей) задачи.
|
||||
func (m *Manager) Status() Status {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.status
|
||||
}
|
||||
|
||||
// Busy сообщает, занят ли менеджер.
|
||||
func (m *Manager) Busy() bool {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.status.Running
|
||||
}
|
||||
Reference in New Issue
Block a user