diff --git a/README.md b/README.md index c437c13..4c414cb 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,9 @@ песочница, моды и лимит памяти) с переключением одной кнопкой. - **Управление сервером** — запуск, остановка (через команду `quit`, мир сохраняется), перезапуск, автоподъём после падения, автозапуск вместе с - менеджером. + менеджером. «Работает» панель показывает не раньше, чем сервер напечатает + `*** SERVER STARTED ****`: пока идёт загрузка мира, состояние остаётся + «Запускается». - **Живая консоль** — вывод сервера в реальном времени и отправка любых команд (`players`, `save`, `servermsg`, `kickuser`, …). - **Мониторинг** — состояние, аптайм, CPU и память процесса сервера, свободные diff --git a/internal/i18n/catalog.go b/internal/i18n/catalog.go index efababc..f4c2d03 100644 --- a/internal/i18n/catalog.go +++ b/internal/i18n/catalog.go @@ -70,6 +70,7 @@ var messages = map[string]string{ "Останавливаю сервер командой quit...": "Stopping the server with the quit command...", "Сервер не ответил на quit — отправляю SIGTERM": "The server did not answer quit — sending SIGTERM", "Сервер завис — отправляю SIGKILL": "The server hung — sending SIGKILL", + "Сервер не сообщил о готовности — считаю его запущенным": "The server did not report that it is ready — treating it as running", "Автоперезапуск отменён": "Automatic restart cancelled", "Посмотрите ошибку выше и запустите сервер вручную.": "Check the error above and start the server manually.", "Запускаю обновление — оно идёт отдельной службой и переживёт перезапуск панели": "Starting the update — it runs as a separate service and survives the panel restart", diff --git a/internal/pzserver/supervisor.go b/internal/pzserver/supervisor.go index 45f8cb2..a55982d 100644 --- a/internal/pzserver/supervisor.go +++ b/internal/pzserver/supervisor.go @@ -42,6 +42,11 @@ var ( // playersPollInterval — как часто менеджер спрашивает у сервера список игроков. const playersPollInterval = 30 * time.Second +// startupGrace — сколько ждём строку о готовности. Загрузка мира с модами +// занимает минуты, поэтому запас большой: раньше времени объявлять сервер +// работающим хуже, чем подождать. +const startupGrace = 15 * time.Minute + // restartDelay — пауза перед автоподъёмом упавшего сервера. const restartDelay = 10 * time.Second @@ -355,10 +360,13 @@ func (s *Supervisor) handleLine(stream, text string) { } // isServerReady распознаёт строку, которой PZ сообщает о готовности принимать -// игроков. +// игроков: «LOG : Network f:0 st:81,093,763> *** SERVER STARTED ****». +// +// Других маркеров тут нет намеренно. Регистрацию в Steam сервер печатает +// задолго до того, как мир загружен, и по ней панель показывала «Работает» на +// сервере, который ещё никого не пустит. func isServerReady(line string) bool { - return strings.Contains(line, "SERVER STARTED") || - strings.Contains(line, "Server Steam ID") + return strings.Contains(strings.ToUpper(line), "SERVER STARTED") } // wait дожидается завершения процесса и решает, поднимать ли его заново. @@ -542,6 +550,23 @@ func (s *Supervisor) WaitStopped(timeout time.Duration) bool { return s.State() == StateStopped } +// checkStartupGrace страхует от сервера, который так и не напечатал строку о +// готовности: со своими модами и своей версией он может назвать её иначе, а +// вечное «Запускается» лишило бы панель опроса игроков. +func (s *Supervisor) checkStartupGrace() { + s.mu.Lock() + overdue := s.state == StateStarting && !s.startedAt.IsZero() && + time.Since(s.startedAt) > startupGrace + if overdue { + s.state = StateRunning + } + s.mu.Unlock() + + if overdue { + s.AppendLog("manager", "Сервер не сообщил о готовности — считаю его запущенным") + } +} + // pollPlayers периодически спрашивает у работающего сервера, кто онлайн. func (s *Supervisor) pollPlayers() { ticker := time.NewTicker(playersPollInterval) @@ -551,6 +576,7 @@ func (s *Supervisor) pollPlayers() { case <-s.quit: return case <-ticker.C: + s.checkStartupGrace() if s.State() != StateRunning { continue } diff --git a/internal/pzserver/supervisor_test.go b/internal/pzserver/supervisor_test.go new file mode 100644 index 0000000..906478d --- /dev/null +++ b/internal/pzserver/supervisor_test.go @@ -0,0 +1,33 @@ +package pzserver + +import "testing" + +// Строка о готовности решает, когда панель перестанет писать «Запускается». +// Ошибиться в обе стороны одинаково неприятно: рано — покажем работающим +// сервер, который никого не пустит; поздно — сервер работает, а панель молчит. +func TestIsServerReady(t *testing.T) { + ready := []string{ + "LOG : Network f:0 st:81,093,763> *** SERVER STARTED ****", + "*** SERVER STARTED ****", + "Server started", + } + for _, line := range ready { + if !isServerReady(line) { + t.Errorf("строка о готовности не распознана: %q", line) + } + } + + // Регистрацию в Steam и загрузку мира сервер печатает задолго до того, + // как начнёт принимать подключения. + notReady := []string{ + "Server Steam ID 90271993034366481", + "znet: Java_zombie_core_znet_SteamGameServer_Init", + "LOG : General f:0 st:1,234> loading world", + "Loading: SERVER settings", + } + for _, line := range notReady { + if isServerReady(line) { + t.Errorf("строка принята за готовность: %q", line) + } + } +} diff --git a/web/static/app.js b/web/static/app.js index eab9242..9399d27 100644 --- a/web/static/app.js +++ b/web/static/app.js @@ -68,7 +68,9 @@ function formatBytes(bytes) { return `${value.toFixed(value < 10 && unit > 0 ? 1 : 0)} ${units[unit]}`; } -function formatUptime(seconds) { +// key разделяет два смысла одного счётчика: сервер либо уже работает, либо +// всё ещё загружается. +function formatUptime(seconds, key = "overview.uptime") { if (!seconds) return ""; const days = Math.floor(seconds / 86400); const hours = Math.floor((seconds % 86400) / 3600); @@ -77,7 +79,7 @@ function formatUptime(seconds) { if (days) parts.push(t("unit.days", { value: days })); if (hours) parts.push(t("unit.hours", { value: hours })); parts.push(t("unit.minutes", { value: minutes })); - return t("overview.uptime", { value: parts.join(" ") }); + return t(key, { value: parts.join(" ") }); } function formatDate(unixSeconds) { @@ -253,7 +255,14 @@ function renderStatus(status) { $("#ov-profile").textContent = status.profile_title ? t("overview.profile", { title: status.profile_title }) : ""; - $("#ov-uptime").textContent = formatUptime(status.uptime_sec); + // Процесс уже запущен, но мир грузится минутами, и всё это время сервер + // никого не пустит: пишем об этом прямо, иначе «Работает» вводит в + // заблуждение. + const starting = status.state === "starting"; + $("#ov-note").textContent = starting ? t("overview.starting") : ""; + $("#status-pill").title = starting ? t("overview.starting") : ""; + $("#ov-uptime").textContent = + formatUptime(status.uptime_sec, starting ? "overview.loading" : "overview.uptime"); $("#ov-pid").textContent = status.pid ? `PID ${status.pid}` : ""; $("#ov-lastexit").textContent = status.state === "stopped" && status.last_exit diff --git a/web/static/i18n.js b/web/static/i18n.js index aab6dfe..8ac965c 100644 --- a/web/static/i18n.js +++ b/web/static/i18n.js @@ -133,6 +133,8 @@ const STRINGS = { "overview.load": "Load average", "overview.diskFree": "Диск свободно", "overview.uptime": "Работает: {value}", + "overview.loading": "Загружается: {value}", + "overview.starting": "Мир загружается — сервер ещё не принимает подключения", "overview.profile": "Профиль: {title}", "overview.lastExit": "Последнее завершение: {reason}", "overview.memoryOf": "{free} из {total}", @@ -428,6 +430,8 @@ const STRINGS = { "overview.load": "Load average", "overview.diskFree": "Disk free", "overview.uptime": "Uptime: {value}", + "overview.loading": "Loading: {value}", + "overview.starting": "The world is loading — the server does not accept connections yet", "overview.profile": "Profile: {title}", "overview.lastExit": "Last exit: {reason}", "overview.memoryOf": "{free} of {total}", diff --git a/web/static/index.html b/web/static/index.html index 91d3e42..bc3c938 100644 --- a/web/static/index.html +++ b/web/static/index.html @@ -101,6 +101,7 @@

+
diff --git a/web/static/style.css b/web/static/style.css index 16cbdee..f0a54e1 100644 --- a/web/static/style.css +++ b/web/static/style.css @@ -109,6 +109,10 @@ code { } .dot.running { background: var(--accent); box-shadow: 0 0 8px var(--accent); } .dot.starting, .dot.stopping, .dot.restarting { background: var(--warn); animation: pulse 1.2s infinite; } + +/* Пояснение под состоянием — тем же цветом, что и пульсирующий индикатор: + пока он мигает, сервер ещё не принимает игроков. */ +.starting-note { color: var(--warn); } .dot.stopped { background: #5b636f; } @keyframes pulse { 50% { opacity: .35; } }