Поле ввода и кнопка «Отправить» были активны всегда, хотя команда уходит в stdin процесса: у остановленного сервера его просто нет, и панель отвечала ошибкой уже после отправки. Теперь консоль блокируется, а подсказка объясняет, почему. Заодно из вывода убираются управляющие последовательности терминала. SteamCMD раскрашивает свои сообщения, и в браузере это выглядело как «[0mWaiting for user info...[0mOK». Чистка стоит на общем пути записи в буфер: через него идёт и вывод сервера, и SteamCMD, и сообщения панели. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
110 lines
3.4 KiB
Go
110 lines
3.4 KiB
Go
package pzserver
|
|
|
|
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 — одна строка консоли сервера с монотонным номером, по которому
|
|
// веб-клиент понимает, что он пропустил, и откуда догружать.
|
|
type LogLine struct {
|
|
Seq int64 `json:"seq"`
|
|
Time int64 `json:"time"` // Unix-миллисекунды
|
|
Stream string `json:"stream"`
|
|
Text string `json:"text"`
|
|
}
|
|
|
|
// logBuffer — кольцевой буфер последних строк консоли плюс рассылка их
|
|
// живым подписчикам (SSE-соединениям веб-панели).
|
|
type logBuffer struct {
|
|
mu sync.RWMutex
|
|
lines []LogLine
|
|
capacity int
|
|
nextSeq int64
|
|
subs map[int]chan LogLine
|
|
nextSub int
|
|
}
|
|
|
|
func newLogBuffer(capacity int) *logBuffer {
|
|
return &logBuffer{
|
|
lines: make([]LogLine, 0, capacity),
|
|
capacity: capacity,
|
|
nextSeq: 1,
|
|
subs: make(map[int]chan LogLine),
|
|
}
|
|
}
|
|
|
|
// append кладёт строку в буфер и рассылает подписчикам.
|
|
func (b *logBuffer) append(line LogLine) {
|
|
// Чистим здесь, а не у каждого источника: через буфер проходят и вывод
|
|
// сервера, и SteamCMD, и сообщения самой панели.
|
|
line.Text = stripANSI(line.Text)
|
|
|
|
b.mu.Lock()
|
|
line.Seq = b.nextSeq
|
|
b.nextSeq++
|
|
if len(b.lines) == b.capacity {
|
|
copy(b.lines, b.lines[1:])
|
|
b.lines = b.lines[:len(b.lines)-1]
|
|
}
|
|
b.lines = append(b.lines, line)
|
|
|
|
subs := make([]chan LogLine, 0, len(b.subs))
|
|
for _, ch := range b.subs {
|
|
subs = append(subs, ch)
|
|
}
|
|
b.mu.Unlock()
|
|
|
|
for _, ch := range subs {
|
|
// Неблокирующая отправка: подписчик, который не успевает читать,
|
|
// теряет строки, но не тормозит вывод сервера.
|
|
select {
|
|
case ch <- line:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
// since возвращает строки с номером больше seq (seq<=0 — весь буфер).
|
|
func (b *logBuffer) since(seq int64) []LogLine {
|
|
b.mu.RLock()
|
|
defer b.mu.RUnlock()
|
|
out := make([]LogLine, 0, len(b.lines))
|
|
for _, l := range b.lines {
|
|
if l.Seq > seq {
|
|
out = append(out, l)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// subscribe открывает канал живых строк и функцию отписки.
|
|
func (b *logBuffer) subscribe(bufferSize int) (<-chan LogLine, func()) {
|
|
ch := make(chan LogLine, bufferSize)
|
|
b.mu.Lock()
|
|
id := b.nextSub
|
|
b.nextSub++
|
|
b.subs[id] = ch
|
|
b.mu.Unlock()
|
|
|
|
return ch, func() {
|
|
b.mu.Lock()
|
|
if existing, ok := b.subs[id]; ok {
|
|
delete(b.subs, id)
|
|
close(existing)
|
|
}
|
|
b.mu.Unlock()
|
|
}
|
|
}
|