Files
mrleo1nidandClaude Opus 5 d0ff64c272 Веб-панель управления сервером 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>
2026-09-03 22:41:11 +03:00

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
}