Files
mrleo1nidandClaude Opus 5 e0c33706b3
CI / test (push) Successful in 25s
Release / release (push) Successful in 37s
Консоль не пустеет после перезапуска менеджера
Номера строк консоли живут в памяти менеджера и после его перезапуска
начинаются заново, а открытая вкладка отбрасывает всё, чей номер не больше
последнего увиденного. После обновления панели консоль оставалась пустой при
живом сервере — до перезагрузки страницы.

У буфера появилась метка запуска: поток отдаёт её отдельным событием, и по
её смене вкладка сбрасывает счётчик, чистит экран и забирает историю нового
запуска заново.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 21:05:10 +03:00

130 lines
4.4 KiB
Go

package pzserver
import (
"crypto/rand"
"encoding/hex"
"regexp"
"strconv"
"sync"
"time"
)
// 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
epoch string
subs map[int]chan LogLine
nextSub int
}
func newLogBuffer(capacity int) *logBuffer {
return &logBuffer{
lines: make([]LogLine, 0, capacity),
capacity: capacity,
nextSeq: 1,
epoch: newEpoch(),
subs: make(map[int]chan LogLine),
}
}
// newEpoch помечает этот запуск менеджера. Номера строк живут только в памяти
// и после перезапуска начинаются заново — открытая вкладка приняла бы новые
// строки за уже показанные и молчала бы до перезагрузки страницы. По смене
// метки браузер понимает, что нумерацию надо начать с нуля.
func newEpoch() string {
var raw [8]byte
if _, err := rand.Read(raw[:]); err != nil {
// Случайность здесь не нужна для безопасности: метку достаточно
// отличать от предыдущей, и время запуска с этим справляется.
return strconv.FormatInt(time.Now().UnixNano(), 36)
}
return hex.EncodeToString(raw[:])
}
// 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()
}
}