From 27a2fd63866825315e4a47ad7441283025c80842 Mon Sep 17 00:00:00 2001 From: mrleo1nid Date: Sat, 5 Sep 2026 01:56:55 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9A=D0=BE=D0=BD=D1=81=D0=BE=D0=BB=D1=8C=20?= =?UTF-8?q?=D0=BD=D0=B5=20=D0=BF=D1=80=D0=B8=D0=BD=D0=B8=D0=BC=D0=B0=D0=B5?= =?UTF-8?q?=D1=82=20=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4=D1=8B=20=D1=83=20?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D0=B0=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=BD=D0=BE=D0=B3=D0=BE=20=D1=81=D0=B5=D1=80=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Поле ввода и кнопка «Отправить» были активны всегда, хотя команда уходит в stdin процесса: у остановленного сервера его просто нет, и панель отвечала ошибкой уже после отправки. Теперь консоль блокируется, а подсказка объясняет, почему. Заодно из вывода убираются управляющие последовательности терминала. SteamCMD раскрашивает свои сообщения, и в браузере это выглядело как «[0mWaiting for user info...[0mOK». Чистка стоит на общем пути записи в буфер: через него идёт и вывод сервера, и SteamCMD, и сообщения панели. Co-Authored-By: Claude Opus 5 --- internal/pzserver/logbuf.go | 20 +++++++++++++++- internal/pzserver/logbuf_test.go | 41 ++++++++++++++++++++++++++++++++ web/static/app.js | 10 ++++++++ web/static/index.html | 4 ++-- 4 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 internal/pzserver/logbuf_test.go diff --git a/internal/pzserver/logbuf.go b/internal/pzserver/logbuf.go index 0289582..b59484e 100644 --- a/internal/pzserver/logbuf.go +++ b/internal/pzserver/logbuf.go @@ -1,6 +1,20 @@ package pzserver -import "sync" +import ( + "regexp" + "sync" +) + +// ansiRe ловит управляющие последовательности терминала: SteamCMD и сам +// сервер раскрашивают вывод, а в браузере эти коды превращаются в мусор +// вида "[0mOK". Первый вариант — CSI (цвета, перемещение курсора), второй — +// всё остальное, что начинается с ESC. +var ansiRe = regexp.MustCompile("\x1b\\[[0-9;?]*[ -/]*[@-~]|\x1b.") + +// stripANSI убирает управляющие последовательности из строки вывода. +func stripANSI(text string) string { + return ansiRe.ReplaceAllString(text, "") +} // LogLine — одна строка консоли сервера с монотонным номером, по которому // веб-клиент понимает, что он пропустил, и откуда догружать. @@ -33,6 +47,10 @@ func newLogBuffer(capacity int) *logBuffer { // append кладёт строку в буфер и рассылает подписчикам. func (b *logBuffer) append(line LogLine) { + // Чистим здесь, а не у каждого источника: через буфер проходят и вывод + // сервера, и SteamCMD, и сообщения самой панели. + line.Text = stripANSI(line.Text) + b.mu.Lock() line.Seq = b.nextSeq b.nextSeq++ diff --git a/internal/pzserver/logbuf_test.go b/internal/pzserver/logbuf_test.go new file mode 100644 index 0000000..1e0953c --- /dev/null +++ b/internal/pzserver/logbuf_test.go @@ -0,0 +1,41 @@ +package pzserver + +import "testing" + +// TestStripANSI берёт строки ровно в том виде, в каком их печатает SteamCMD: +// в браузере коды раскраски превращались в мусор вроде "[0mOK". +func TestStripANSI(t *testing.T) { + cases := map[string]string{ + "\x1b[0mConnecting anonymously to Steam Public...\x1b[0mOK": "Connecting anonymously to Steam Public...OK", + "\x1b[0m Update state (0x3) reconfiguring, progress: 0.00": " Update state (0x3) reconfiguring, progress: 0.00", + "\x1b[1;32mSuccess!\x1b[0m": "Success!", + // Перемещение курсора и очистка строки — тоже CSI. + "\x1b[2K\x1b[1GЗагрузка": "Загрузка", + // Обычные строки не должны меняться. + "[----] Installing update...": "[----] Installing update...", + "Steam Console Client (c) Valve": "Steam Console Client (c) Valve", + "Мир сохранён, игроков онлайн: 3": "Мир сохранён, игроков онлайн: 3", + "": "", + } + + for input, want := range cases { + if got := stripANSI(input); got != want { + t.Errorf("stripANSI(%q) = %q, ожидалось %q", input, got, want) + } + } +} + +// TestAppendStripsANSI проверяет, что чистка стоит на общем пути: через буфер +// идёт и вывод сервера, и SteamCMD, и сообщения самой панели. +func TestAppendStripsANSI(t *testing.T) { + buf := newLogBuffer(10) + buf.append(LogLine{Stream: "task", Text: "\x1b[0mWaiting for user info...\x1b[0mOK"}) + + lines := buf.since(0) + if len(lines) != 1 { + t.Fatalf("строк в буфере: %d", len(lines)) + } + if lines[0].Text != "Waiting for user info...OK" { + t.Errorf("в буфер попала строка с кодами: %q", lines[0].Text) + } +} diff --git a/web/static/app.js b/web/static/app.js index 41e78bd..5f0774d 100644 --- a/web/static/app.js +++ b/web/static/app.js @@ -271,6 +271,16 @@ function renderStatus(status) { $("#btn-restart").disabled = !status.installed; $("#btn-install").disabled = running; + // Консоль есть только у живого процесса: у остановленного сервера нет stdin, + // и команда всё равно вернулась бы ошибкой. + const consoleInput = $("#console-command"); + const consoleButton = $("#console-form").querySelector("button[type=submit]"); + consoleInput.disabled = !running; + consoleButton.disabled = !running; + consoleInput.placeholder = running + ? "Команда сервера, например: players, save, quit" + : "Сервер остановлен — консоль принимает команды только на работающем"; + // Профили делят игровые порты и файл запуска, поэтому переключать их можно // только на остановленном сервере. const picker = $("#profile-select"); diff --git a/web/static/index.html b/web/static/index.html index 54056a8..63ce2ff 100644 --- a/web/static/index.html +++ b/web/static/index.html @@ -129,8 +129,8 @@

       
- + autocomplete="off" spellcheck="false" disabled> +

Полезные команды: players, save, servermsg "текст", kickuser "ник", banuser "ник", checkModsNeedUpdate.