diff --git a/internal/web/service/server.go b/internal/web/service/server.go index f5dad3f94..6c782b67c 100644 --- a/internal/web/service/server.go +++ b/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."} } diff --git a/internal/web/service/server_syslog_test.go b/internal/web/service/server_syslog_test.go new file mode 100644 index 000000000..64766f42b --- /dev/null +++ b/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) + } +}