Веб-панель управления сервером 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,201 @@
|
||||
// Package config загружает и сохраняет настройки самого менеджера.
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Config — всё, что менеджеру нужно знать о хосте и о сервере PZ.
|
||||
type Config struct {
|
||||
// Listen — адрес веб-панели. Для доступа только через SSH-туннель
|
||||
// используйте 127.0.0.1:8080.
|
||||
Listen string `yaml:"listen"`
|
||||
|
||||
// DataDir — рабочая директория менеджера (пользователи, сессии, бэкапы).
|
||||
DataDir string `yaml:"data_dir"`
|
||||
|
||||
// ServerDir — куда SteamCMD ставит серверные файлы PZ.
|
||||
ServerDir string `yaml:"server_dir"`
|
||||
|
||||
// ZomboidDir — ~/Zomboid: сохранения, конфиги, логи.
|
||||
ZomboidDir string `yaml:"zomboid_dir"`
|
||||
|
||||
// SteamCMDPath — путь к бинарю steamcmd.sh (пустой = ищем в PATH).
|
||||
SteamCMDPath string `yaml:"steamcmd_path"`
|
||||
|
||||
// ServerName — имя набора конфигов PZ (servertest -> servertest.ini).
|
||||
ServerName string `yaml:"server_name"`
|
||||
|
||||
// AdminPassword — пароль админа игрового сервера, нужен при первом
|
||||
// запуске PZ. Хранится здесь, чтобы не вводить его в консоли.
|
||||
AdminPassword string `yaml:"admin_password"`
|
||||
|
||||
// JavaMemory — -Xmx для JVM сервера, например "8g".
|
||||
JavaMemory string `yaml:"java_memory"`
|
||||
|
||||
// ExtraArgs — дополнительные аргументы запуска сервера.
|
||||
ExtraArgs []string `yaml:"extra_args"`
|
||||
|
||||
// AutoStart — поднимать игровой сервер сразу при старте менеджера.
|
||||
AutoStart bool `yaml:"autostart"`
|
||||
|
||||
// AutoRestart — перезапускать сервер, если он упал сам.
|
||||
AutoRestart bool `yaml:"autorestart"`
|
||||
|
||||
// StopTimeout — сколько ждать корректного завершения после "quit".
|
||||
StopTimeout Duration `yaml:"stop_timeout"`
|
||||
|
||||
// LogBufferLines — сколько строк консоли держать в памяти для веб-панели.
|
||||
LogBufferLines int `yaml:"log_buffer_lines"`
|
||||
|
||||
// Backup — настройки резервного копирования.
|
||||
Backup BackupConfig `yaml:"backup"`
|
||||
|
||||
path string
|
||||
}
|
||||
|
||||
// BackupConfig описывает расписание и ротацию бэкапов мира.
|
||||
type BackupConfig struct {
|
||||
// Dir — куда складывать архивы.
|
||||
Dir string `yaml:"dir"`
|
||||
// Schedule — интервал автобэкапа; 0 отключает автоматику.
|
||||
Schedule Duration `yaml:"schedule"`
|
||||
// Keep — сколько последних архивов хранить (0 = не удалять).
|
||||
Keep int `yaml:"keep"`
|
||||
// StopServer — останавливать сервер на время бэкапа. Безопаснее, но
|
||||
// игроков выкидывает; по умолчанию выключено.
|
||||
StopServer bool `yaml:"stop_server"`
|
||||
}
|
||||
|
||||
// Default возвращает конфиг с разумными значениями для Ubuntu.
|
||||
func Default() *Config {
|
||||
home, _ := os.UserHomeDir()
|
||||
return &Config{
|
||||
Listen: "127.0.0.1:8080",
|
||||
DataDir: filepath.Join(home, ".local", "share", "pzmanager"),
|
||||
ServerDir: filepath.Join(home, "pzserver"),
|
||||
ZomboidDir: filepath.Join(home, "Zomboid"),
|
||||
ServerName: "servertest",
|
||||
JavaMemory: "4g",
|
||||
AutoRestart: true,
|
||||
StopTimeout: Duration(90 * time.Second),
|
||||
LogBufferLines: 5000,
|
||||
Backup: BackupConfig{
|
||||
Dir: filepath.Join(home, "pzbackups"),
|
||||
Schedule: Duration(6 * time.Hour),
|
||||
Keep: 20,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Load читает конфиг с диска, дополняя пропущенные поля значениями по
|
||||
// умолчанию. Если файла нет — создаёт его.
|
||||
func Load(path string) (*Config, error) {
|
||||
cfg := Default()
|
||||
cfg.path = path
|
||||
|
||||
raw, err := os.ReadFile(path)
|
||||
if os.IsNotExist(err) {
|
||||
if err := cfg.Save(); err != nil {
|
||||
return nil, fmt.Errorf("создать конфиг %s: %w", path, err)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("прочитать конфиг %s: %w", path, err)
|
||||
}
|
||||
if err := yaml.Unmarshal(raw, cfg); err != nil {
|
||||
return nil, fmt.Errorf("разобрать конфиг %s: %w", path, err)
|
||||
}
|
||||
cfg.applyFallbacks()
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// applyFallbacks подставляет умолчания вместо пустых значений: конфиг,
|
||||
// написанный руками, редко заполнен целиком.
|
||||
func (c *Config) applyFallbacks() {
|
||||
d := Default()
|
||||
if c.Listen == "" {
|
||||
c.Listen = d.Listen
|
||||
}
|
||||
if c.DataDir == "" {
|
||||
c.DataDir = d.DataDir
|
||||
}
|
||||
if c.ServerDir == "" {
|
||||
c.ServerDir = d.ServerDir
|
||||
}
|
||||
if c.ZomboidDir == "" {
|
||||
c.ZomboidDir = d.ZomboidDir
|
||||
}
|
||||
if c.ServerName == "" {
|
||||
c.ServerName = d.ServerName
|
||||
}
|
||||
if c.JavaMemory == "" {
|
||||
c.JavaMemory = d.JavaMemory
|
||||
}
|
||||
if c.StopTimeout <= 0 {
|
||||
c.StopTimeout = d.StopTimeout
|
||||
}
|
||||
if c.LogBufferLines <= 0 {
|
||||
c.LogBufferLines = d.LogBufferLines
|
||||
}
|
||||
if c.Backup.Dir == "" {
|
||||
c.Backup.Dir = d.Backup.Dir
|
||||
}
|
||||
}
|
||||
|
||||
// Save записывает конфиг атомарно: сначала во временный файл рядом, затем
|
||||
// переименование, чтобы падение посреди записи не оставило огрызок.
|
||||
func (c *Config) Save() error {
|
||||
if c.path == "" {
|
||||
return fmt.Errorf("путь к конфигу неизвестен")
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(c.path), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
raw, err := yaml.Marshal(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmp := c.path + ".tmp"
|
||||
if err := os.WriteFile(tmp, raw, 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmp, c.path)
|
||||
}
|
||||
|
||||
// Path возвращает путь, откуда конфиг был загружен.
|
||||
func (c *Config) Path() string { return c.path }
|
||||
|
||||
// SetPath задаёт файл для последующего Save.
|
||||
func (c *Config) SetPath(p string) { c.path = p }
|
||||
|
||||
// EnsureDirs создаёт директории, без которых менеджер не сможет работать.
|
||||
func (c *Config) EnsureDirs() error {
|
||||
for _, dir := range []string{c.DataDir, c.ServerDir, c.Backup.Dir} {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return fmt.Errorf("создать %s: %w", dir, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServerConfigPath — путь к servertest.ini (или другому имени сервера).
|
||||
func (c *Config) ServerConfigPath() string {
|
||||
return filepath.Join(c.ZomboidDir, "Server", c.ServerName+".ini")
|
||||
}
|
||||
|
||||
// SandboxConfigPath — путь к <name>_SandboxVars.lua.
|
||||
func (c *Config) SandboxConfigPath() string {
|
||||
return filepath.Join(c.ZomboidDir, "Server", c.ServerName+"_SandboxVars.lua")
|
||||
}
|
||||
|
||||
// SavesDir — директория с мирами (то, что бэкапим).
|
||||
func (c *Config) SavesDir() string {
|
||||
return filepath.Join(c.ZomboidDir, "Saves")
|
||||
}
|
||||
Reference in New Issue
Block a user