Менеджер запускает сервер 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>
99 lines
2.7 KiB
Go
99 lines
2.7 KiB
Go
// 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
|
|
}
|