فهرست منبع

Bound the panel syslog view with a journalctl timeout (#6689)

* Bound the syslog view with a journalctl timeout

* fix(syslog): bound the journal scan window and soften the timeout message

Limit journalctl to the last 30 days so a rare -p level cannot scan the whole
journal, and drop the guessed cause and the host-wide vacuum advice from the
timeout message.

* fix(syslog): drop --since from the journalctl call and test the timeout

On systemd 249/252 (Ubuntu 22.04, Debian 12) journalctl seeks to --since
and reads forward when both --since and -n are given, so the Syslog view
showed the oldest 200 lines of the 30-day window instead of the newest.
Reproduced in debian:12, ubuntu:22.04 and ubuntu:24.04 containers on a
synthetic journal; only 255 kept the newest lines. The window also bought
nothing: on a 340 MB journal every variant (with or without --since, rare
-p level or not) answered in ~10 ms on 252 and 255.

Keep the 15s deadline as the guard against a stalled journalctl and cover
it with a fake journalctl on PATH; the args test pinned the broken flag
and is removed.

---------

Co-authored-by: Sanaei <[email protected]>
kaveh 12 ساعت پیش
والد
کامیت
5366eb0d29
2فایلهای تغییر یافته به همراه42 افزوده شده و 1 حذف شده
  1. 9 1
      internal/web/service/server.go
  2. 33 0
      internal/web/service/server_syslog_test.go

+ 9 - 1
internal/web/service/server.go

@@ -1184,6 +1184,9 @@ func (s *ServerService) UpdateXray(version string) error {
 	return nil
 }
 
+// syslogTimeout keeps a stalled journalctl from hanging the Syslog request (#6629).
+var syslogTimeout = 15 * time.Second
+
 func (s *ServerService) GetLogs(count string, level string, syslog string) []string {
 	c, _ := strconv.Atoi(count)
 	var lines []string
@@ -1216,10 +1219,15 @@ func (s *ServerService) GetLogs(count string, level string, syslog string) []str
 		}
 
 		// Use hardcoded command with validated parameters
-		cmd := exec.CommandContext(context.Background(), "journalctl", "-u", "x-ui", "--no-pager", "-n", strconv.Itoa(countInt), "-p", level)
+		ctx, cancel := context.WithTimeout(context.Background(), syslogTimeout)
+		defer cancel()
+		cmd := exec.CommandContext(ctx, "journalctl", "-u", "x-ui", "--no-pager", "-n", strconv.Itoa(countInt), "-p", level)
 		var out bytes.Buffer
 		cmd.Stdout = &out
 		err = cmd.Run()
+		if errors.Is(ctx.Err(), context.DeadlineExceeded) {
+			return []string{"journalctl did not answer in time. Try a smaller line count or a less strict level."}
+		}
 		if err != nil {
 			return []string{"Failed to run journalctl command! Make sure systemd is available and x-ui service is registered."}
 		}

+ 33 - 0
internal/web/service/server_syslog_test.go

@@ -0,0 +1,33 @@
+//go:build !windows
+
+package service
+
+import (
+	"os"
+	"path/filepath"
+	"slices"
+	"testing"
+	"time"
+)
+
+// A journalctl that never answers must not hang the Syslog request (#6629).
+func TestGetLogsSyslogGivesUpOnAStalledJournalctl(t *testing.T) {
+	dir := t.TempDir()
+	if err := os.WriteFile(filepath.Join(dir, "journalctl"), []byte("#!/bin/sh\nexec sleep 5\n"), 0o755); err != nil {
+		t.Fatalf("write fake journalctl: %v", err)
+	}
+	t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
+	saved := syslogTimeout
+	syslogTimeout = 200 * time.Millisecond
+	t.Cleanup(func() { syslogTimeout = saved })
+
+	start := time.Now()
+	got := (&ServerService{}).GetLogs("10", "err", "true")
+	want := []string{"journalctl did not answer in time. Try a smaller line count or a less strict level."}
+	if !slices.Equal(got, want) {
+		t.Fatalf("GetLogs = %q, want %q", got, want)
+	}
+	if elapsed := time.Since(start); elapsed > 3*time.Second {
+		t.Fatalf("GetLogs waited %v for a stalled journalctl, want about %v", elapsed, syslogTimeout)
+	}
+}