«Работает» — только после строки о готовности сервера
Регистрацию в Steam сервер печатает задолго до того, как мир загружен, и по ней панель показывала «Работает» на сервере, который ещё никого не пустит. Теперь готовность распознаётся только по «*** SERVER STARTED ****», а пока идёт загрузка, состояние остаётся «Запускается» с пояснением под ним и счётчиком времени загрузки. Если строки о готовности так и не будет (своя версия, свои моды), через пятнадцать минут менеджер сочтёт сервер запущенным и скажет об этом в консоли: иначе панель навсегда осталась бы без опроса игроков. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,9 @@
|
||||
песочница, моды и лимит памяти) с переключением одной кнопкой.
|
||||
- **Управление сервером** — запуск, остановка (через команду `quit`, мир
|
||||
сохраняется), перезапуск, автоподъём после падения, автозапуск вместе с
|
||||
менеджером.
|
||||
менеджером. «Работает» панель показывает не раньше, чем сервер напечатает
|
||||
`*** SERVER STARTED ****`: пока идёт загрузка мира, состояние остаётся
|
||||
«Запускается».
|
||||
- **Живая консоль** — вывод сервера в реальном времени и отправка любых команд
|
||||
(`players`, `save`, `servermsg`, `kickuser`, …).
|
||||
- **Мониторинг** — состояние, аптайм, CPU и память процесса сервера, свободные
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
-3
@@ -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
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
<div class="card">
|
||||
<h3 data-i18n="overview.stateCard"></h3>
|
||||
<div class="stat" id="ov-state">—</div>
|
||||
<div class="starting-note" id="ov-note"></div>
|
||||
<div class="muted" id="ov-profile"></div>
|
||||
<div class="muted" id="ov-uptime"></div>
|
||||
<div class="muted" id="ov-pid"></div>
|
||||
|
||||
@@ -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; } }
|
||||
|
||||
Reference in New Issue
Block a user