package api import ( "io" "log" "net/http" "os" "path/filepath" "slices" "sort" "strings" "time" "github.com/gin-gonic/gin" "github.com/gorilla/websocket" ) 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 { log.Printf("websocket upgrade error: %v", err) return } 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 { select { case line, ok := <-ch: if !ok { return } if err := conn.WriteMessage(websocket.TextMessage, []byte(line)); err != nil { return } case <-done: return } } } func (h *Handler) ListLogs(c *gin.Context) { profilesDir := h.process.ProfilesDir() files := h.collectLogFiles(profilesDir, "") logsDir := filepath.Join(profilesDir, "logs") if fi, err := os.Stat(logsDir); err == nil && fi.IsDir() { files = append(files, h.collectLogFiles(logsDir, "logs")...) } slices.Sort(files) if files == nil { files = []string{} } c.JSON(http.StatusOK, files) } func (h *Handler) collectLogFiles(dir, prefix string) []string { entries, err := os.ReadDir(dir) if err != nil { return nil } var files []string for _, e := range entries { if !e.IsDir() { ext := filepath.Ext(e.Name()) if ext == ".log" || ext == ".rpt" || e.Name() == "script.log" { if prefix != "" { files = append(files, prefix+"/"+e.Name()) } else { files = append(files, e.Name()) } } } } return files } func (h *Handler) StreamSteamCMDLogs(c *gin.Context) { conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) if err != nil { log.Printf("websocket upgrade error: %v", err) return } 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 { select { case line, ok := <-ch: if !ok { return } if err := conn.WriteMessage(websocket.TextMessage, []byte(line)); err != nil { return } case <-done: return } } } func (h *Handler) StreamRPTLogs(c *gin.Context) { conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) if err != nil { log.Printf("websocket upgrade error: %v", err) return } 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 { select { case <-done: return case <-ticker.C: } path := findLatestRPT(profilesDir) if path != currentPath { currentPath = path currentOffset = 0 if currentPath == "" { continue } if err := conn.WriteMessage(websocket.TextMessage, []byte("--- tailing: "+filepath.Base(currentPath)+" ---")); err != nil { return } } if currentPath == "" { continue } fi, err := os.Stat(currentPath) if err != nil { currentPath = "" continue } if fi.Size() <= currentOffset { continue } if err := func() error { f, err := os.Open(currentPath) if err != nil { currentPath = "" return nil } defer f.Close() if _, err := f.Seek(currentOffset, io.SeekStart); err != nil { return nil } 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 } } } func findLatestRPT(dir string) string { pattern := filepath.Join(dir, "arma3server_x64_*.rpt") matches, err := filepath.Glob(pattern) if err != nil || len(matches) == 0 { return "" } 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 infos[0].path } func (h *Handler) GetLog(c *gin.Context) { filename := filepath.Base(c.Param("file")) if filename == "" || filename == "." || strings.ContainsAny(filename, "/\\") { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid filename"}) return } profilesDir := h.process.ProfilesDir() path := filepath.Join(profilesDir, filename) if _, err := os.Stat(path); os.IsNotExist(err) { logsPath := filepath.Join(profilesDir, "logs", filename) if _, err2 := os.Stat(logsPath); err2 == nil { path = logsPath } else { c.JSON(http.StatusNotFound, gin.H{"error": "file not found"}) return } } data, err := os.ReadFile(path) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.String(http.StatusOK, string(data)) }