fix(backend): fix memory leaks and resource issues in logs system
CI / build (push) Successful in 1m18s

- Fix Subscribe overwriting channel without closing old one (goroutine leak)
- Add context.Context to Stream() for cancellation support
- Add WebSocket read pump to detect dead clients (StreamLogs, StreamSteamCMDLogs, StreamRPTLogs)
- Use defer f.Close() pattern in StreamRPTLogs file handling
- Fix findLatestRPT nil dereference on failed os.Stat
- Optimize findLatestRPT to collect stat results before sorting
- Apply gofmt formatting to modlists.go, settings.go
This commit is contained in:
MrFastwind
2026-07-24 16:25:09 +02:00
parent 6f3175ac9a
commit 8bf163a931
7 changed files with 242 additions and 63 deletions
+98 -27
View File
@@ -19,6 +19,25 @@ var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
const wsReadTimeout = 60 * time.Second
func readPump(conn *websocket.Conn, done chan struct{}) {
defer close(done)
conn.SetReadLimit(4096)
conn.SetReadDeadline(time.Now().Add(wsReadTimeout))
conn.SetPongHandler(func(string) error {
conn.SetReadDeadline(time.Now().Add(wsReadTimeout))
return nil
})
for {
_, _, err := conn.ReadMessage()
if err != nil {
return
}
conn.SetReadDeadline(time.Now().Add(wsReadTimeout))
}
}
func (h *Handler) StreamLogs(c *gin.Context) {
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
@@ -27,13 +46,24 @@ func (h *Handler) StreamLogs(c *gin.Context) {
}
defer conn.Close()
done := make(chan struct{})
go readPump(conn, done)
clientID := conn.RemoteAddr().String()
ch := h.streamer.Subscribe("server", clientID)
defer h.streamer.Unsubscribe("server", clientID)
for line := range ch {
if err := conn.WriteMessage(websocket.TextMessage, []byte(line)); err != nil {
break
for {
select {
case line, ok := <-ch:
if !ok {
return
}
if err := conn.WriteMessage(websocket.TextMessage, []byte(line)); err != nil {
return
}
case <-done:
return
}
}
}
@@ -83,13 +113,24 @@ func (h *Handler) StreamSteamCMDLogs(c *gin.Context) {
}
defer conn.Close()
done := make(chan struct{})
go readPump(conn, done)
clientID := conn.RemoteAddr().String()
ch := h.streamer.Subscribe("steamcmd", clientID)
defer h.streamer.Unsubscribe("steamcmd", clientID)
for line := range ch {
if err := conn.WriteMessage(websocket.TextMessage, []byte(line)); err != nil {
break
for {
select {
case line, ok := <-ch:
if !ok {
return
}
if err := conn.WriteMessage(websocket.TextMessage, []byte(line)); err != nil {
return
}
case <-done:
return
}
}
}
@@ -102,13 +143,22 @@ func (h *Handler) StreamRPTLogs(c *gin.Context) {
}
defer conn.Close()
done := make(chan struct{})
go readPump(conn, done)
profilesDir := h.process.ProfilesDir()
var currentPath string
var currentOffset int64
ticker := time.NewTicker(250 * time.Millisecond)
defer ticker.Stop()
for range ticker.C {
for {
select {
case <-done:
return
case <-ticker.C:
}
path := findLatestRPT(profilesDir)
if path != currentPath {
@@ -134,24 +184,32 @@ func (h *Handler) StreamRPTLogs(c *gin.Context) {
continue
}
f, err := os.Open(currentPath)
if err != nil {
currentPath = ""
continue
}
f.Seek(currentOffset, io.SeekStart)
buf := make([]byte, fi.Size()-currentOffset)
n, _ := io.ReadFull(f, buf)
f.Close()
if err := func() error {
f, err := os.Open(currentPath)
if err != nil {
currentPath = ""
return nil
}
defer f.Close()
currentOffset += int64(n)
for _, line := range strings.Split(string(buf[:n]), "\n") {
if line == "" {
continue
if _, err := f.Seek(currentOffset, io.SeekStart); err != nil {
return nil
}
if err := conn.WriteMessage(websocket.TextMessage, []byte(line)); err != nil {
return
buf := make([]byte, fi.Size()-currentOffset)
n, _ := io.ReadFull(f, buf)
currentOffset += int64(n)
for _, line := range strings.Split(string(buf[:n]), "\n") {
if line == "" {
continue
}
if err := conn.WriteMessage(websocket.TextMessage, []byte(line)); err != nil {
return err
}
}
return nil
}(); err != nil {
return
}
}
}
@@ -162,12 +220,25 @@ func findLatestRPT(dir string) string {
if err != nil || len(matches) == 0 {
return ""
}
sort.Slice(matches, func(i, j int) bool {
fi, _ := os.Stat(matches[i])
fj, _ := os.Stat(matches[j])
return fi.ModTime().After(fj.ModTime())
type rptInfo struct {
path string
time time.Time
}
var infos []rptInfo
for _, m := range matches {
fi, err := os.Stat(m)
if err != nil {
continue
}
infos = append(infos, rptInfo{path: m, time: fi.ModTime()})
}
if len(infos) == 0 {
return ""
}
sort.Slice(infos, func(i, j int) bool {
return infos[i].time.After(infos[j].time)
})
return matches[0]
return infos[0].path
}
func (h *Handler) GetLog(c *gin.Context) {