Профили серверов: несколько миров с раздельными настройками
Профиль — это отдельный сервер: свой мир, .ini, песочница, набор модов и лимит памяти JVM. Project Zomboid различает их по -servername, поэтому файлы профилей не пересекаются. Работает всегда один профиль: серверные файлы, игровые порты и файл запуска JVM общие, так что переключение разрешено только на остановленном сервере. - config: список профилей вместо одиночных server_name/admin_password/ java_memory; старые поля переносятся в первый профиль при загрузке; - supervisor: запускает активный профиль и выставляет его -Xmx на каждом старте, потому что файл запуска общий; - бэкапы: архив снимается с одного профиля (мир + его конфиги), имя профиля попадает в имя файла, ротация считается по профилям; - API профилей: список, создание с копированием настроек из другого профиля, правка, удаление и переключение активного; - панель: селектор профиля в шапке и вкладка со списком профилей. Удаление профиля убирает его только из панели: мир и конфиги остаются на диске, чтобы случайное нажатие не стоило сохранений. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/mrleo1nid/pzmanager/internal/config"
|
||||
"github.com/mrleo1nid/pzmanager/internal/pzconfig"
|
||||
"github.com/mrleo1nid/pzmanager/internal/sysinfo"
|
||||
)
|
||||
|
||||
@@ -43,16 +44,20 @@ const restartDelay = 10 * time.Second
|
||||
|
||||
// Status — снимок состояния для веб-панели.
|
||||
type Status struct {
|
||||
State State `json:"state"`
|
||||
Installed bool `json:"installed"`
|
||||
PID int `json:"pid"`
|
||||
UptimeSec int64 `json:"uptime_sec"`
|
||||
Players []string `json:"players"`
|
||||
PlayersAt int64 `json:"players_at"`
|
||||
LastExit string `json:"last_exit"`
|
||||
AutoRestart bool `json:"auto_restart"`
|
||||
Proc sysinfo.ProcStats `json:"proc"`
|
||||
Host sysinfo.HostStats `json:"host"`
|
||||
State State `json:"state"`
|
||||
// Profile и ProfileTitle описывают профиль, который сейчас работает
|
||||
// (а если сервер остановлен — который будет запущен).
|
||||
Profile string `json:"profile"`
|
||||
ProfileTitle string `json:"profile_title"`
|
||||
Installed bool `json:"installed"`
|
||||
PID int `json:"pid"`
|
||||
UptimeSec int64 `json:"uptime_sec"`
|
||||
Players []string `json:"players"`
|
||||
PlayersAt int64 `json:"players_at"`
|
||||
LastExit string `json:"last_exit"`
|
||||
AutoRestart bool `json:"auto_restart"`
|
||||
Proc sysinfo.ProcStats `json:"proc"`
|
||||
Host sysinfo.HostStats `json:"host"`
|
||||
}
|
||||
|
||||
// Supervisor владеет процессом сервера: запускает, кормит командами консоли,
|
||||
@@ -70,6 +75,9 @@ type Supervisor struct {
|
||||
lastExit string
|
||||
players []string
|
||||
playersAt time.Time
|
||||
// profile — профиль, с которым сервер был запущен: активный профиль
|
||||
// в конфиге может отличаться, если его успели переключить.
|
||||
profile config.Profile
|
||||
// intentionalStop отличает остановку по кнопке от падения: во втором
|
||||
// случае включается автоперезапуск.
|
||||
intentionalStop bool
|
||||
@@ -136,12 +144,19 @@ func (s *Supervisor) State() State {
|
||||
|
||||
// Status собирает полный снимок состояния вместе с метриками процесса и хоста.
|
||||
func (s *Supervisor) Status() Status {
|
||||
profile := s.cfg.ActiveProfile()
|
||||
|
||||
s.mu.Lock()
|
||||
if s.state != StateStopped {
|
||||
profile = s.profile
|
||||
}
|
||||
st := Status{
|
||||
State: s.state,
|
||||
Players: append([]string{}, s.players...),
|
||||
LastExit: s.lastExit,
|
||||
AutoRestart: s.cfg.AutoRestart,
|
||||
State: s.state,
|
||||
Profile: profile.ID,
|
||||
ProfileTitle: profile.Name(),
|
||||
Players: append([]string{}, s.players...),
|
||||
LastExit: s.lastExit,
|
||||
AutoRestart: s.cfg.AutoRestart,
|
||||
}
|
||||
if !s.playersAt.IsZero() {
|
||||
st.PlayersAt = s.playersAt.Unix()
|
||||
@@ -186,11 +201,18 @@ func (s *Supervisor) startLocked() error {
|
||||
return fmt.Errorf("chmod %s: %w", script, err)
|
||||
}
|
||||
|
||||
args := []string{"-servername", s.cfg.ServerName}
|
||||
if s.cfg.AdminPassword != "" {
|
||||
args = append(args, "-adminpassword", s.cfg.AdminPassword)
|
||||
profile := s.cfg.ActiveProfile()
|
||||
// Лимит памяти живёт в файле запуска, общем для всех профилей, поэтому
|
||||
// выставляем его на каждом старте — иначе профиль унаследует чужой -Xmx.
|
||||
if err := pzconfig.SetJavaMemory(s.cfg.ServerDir, profile.JavaMemory); err != nil {
|
||||
s.AppendLog("manager", "Не удалось выставить лимит памяти JVM: "+err.Error())
|
||||
}
|
||||
args = append(args, s.cfg.ExtraArgs...)
|
||||
|
||||
args := []string{"-servername", profile.ID}
|
||||
if profile.AdminPassword != "" {
|
||||
args = append(args, "-adminpassword", profile.AdminPassword)
|
||||
}
|
||||
args = append(args, profile.ExtraArgs...)
|
||||
|
||||
cmd := exec.Command(script, args...)
|
||||
cmd.Dir = s.cfg.ServerDir
|
||||
@@ -220,6 +242,7 @@ func (s *Supervisor) startLocked() error {
|
||||
}
|
||||
|
||||
s.cmd = cmd
|
||||
s.profile = profile
|
||||
s.stdin = stdin
|
||||
s.pgid = processGroupID(cmd.Process.Pid)
|
||||
s.state = StateStarting
|
||||
@@ -230,8 +253,8 @@ func (s *Supervisor) startLocked() error {
|
||||
s.exited = make(chan struct{})
|
||||
s.sampler.Reset()
|
||||
|
||||
s.AppendLog("manager", fmt.Sprintf("Запуск сервера: %s %s (pid %d)",
|
||||
script, strings.Join(args, " "), cmd.Process.Pid))
|
||||
s.AppendLog("manager", fmt.Sprintf("Запуск профиля %q: %s %s (pid %d)",
|
||||
profile.Name(), script, strings.Join(args, " "), cmd.Process.Pid))
|
||||
|
||||
go s.readPipe(stdout, "stdout")
|
||||
go s.readPipe(stderr, "stderr")
|
||||
|
||||
Reference in New Issue
Block a user