log_writer.go 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. package xray
  2. import (
  3. "regexp"
  4. "runtime"
  5. "strings"
  6. "sync"
  7. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  8. )
  9. // Compiled once at package load: Write runs on every line Xray emits, so
  10. // recompiling these per write is wasted work.
  11. var (
  12. crashStartRegex = regexp.MustCompile(`^(panic|fatal error): `)
  13. logLineRegex = regexp.MustCompile(`^(\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}\.\d{6}) \[([^\]]+)\] (.+)$`)
  14. )
  15. // NewLogWriter returns a new LogWriter for processing Xray log output.
  16. func NewLogWriter() *LogWriter {
  17. return &LogWriter{}
  18. }
  19. // LogWriter splits Xray's stdout/stderr into lines and forwards them to the
  20. // panel log; once a Go panic or fatal error starts, all output is kept for one crash report.
  21. type LogWriter struct {
  22. mu sync.RWMutex
  23. pending string
  24. crash strings.Builder
  25. lastLine string
  26. }
  27. // LastLine returns the most recently processed Xray log line. It is safe for
  28. // concurrent use: Process.GetResult reads it from a different goroutine than the
  29. // one Xray drives Write from.
  30. func (lw *LogWriter) LastLine() string {
  31. lw.mu.RLock()
  32. defer lw.mu.RUnlock()
  33. return lw.lastLine
  34. }
  35. // Write buffers chunks into whole lines: the pipe splits output at arbitrary
  36. // points, e.g. the Go runtime writes "panic: " on its own before the message.
  37. func (lw *LogWriter) Write(m []byte) (n int, err error) {
  38. lw.mu.Lock()
  39. defer lw.mu.Unlock()
  40. if lw.crash.Len() > 0 {
  41. lw.crash.Write(m)
  42. return len(m), nil
  43. }
  44. lw.pending += string(m)
  45. for {
  46. i := strings.IndexByte(lw.pending, '\n')
  47. if i < 0 {
  48. break
  49. }
  50. line := lw.pending[:i]
  51. lw.pending = lw.pending[i+1:]
  52. if lw.startCrashLocked(line + "\n" + lw.pending) {
  53. lw.pending = ""
  54. break
  55. }
  56. lw.processLineLocked(line)
  57. }
  58. return len(m), nil
  59. }
  60. // Flush handles an unterminated final line and writes the crash report, if any;
  61. // it must run after cmd.Wait, which guarantees every output chunk was delivered.
  62. func (lw *LogWriter) Flush() {
  63. lw.mu.Lock()
  64. defer lw.mu.Unlock()
  65. if rest := lw.pending; rest != "" {
  66. lw.pending = ""
  67. if !lw.startCrashLocked(rest) {
  68. lw.processLineLocked(rest)
  69. }
  70. }
  71. if lw.crash.Len() == 0 {
  72. return
  73. }
  74. report := lw.crash.String()
  75. lw.crash.Reset()
  76. logger.Debug("Core crash detected:\n", report)
  77. if err := writeCrashReport([]byte(report)); err != nil {
  78. logger.Error("Unable to write crash report:", err)
  79. }
  80. }
  81. // startCrashLocked begins capturing a crash when output opens with a Go runtime
  82. // crash header; the header line becomes LastLine so GetResult shows the cause.
  83. func (lw *LogWriter) startCrashLocked(output string) bool {
  84. if lw.crash.Len() > 0 || !crashStartRegex.MatchString(output) {
  85. return false
  86. }
  87. header, _, _ := strings.Cut(output, "\n")
  88. lw.lastLine = strings.TrimSpace(header)
  89. lw.crash.WriteString(output)
  90. return true
  91. }
  92. func (lw *LogWriter) processLineLocked(line string) {
  93. msg := strings.TrimSpace(line)
  94. if msg == "" {
  95. return
  96. }
  97. msgLower := strings.ToLower(msg)
  98. // Suppress noisy Windows process-kill signal that surfaces as exit status 1
  99. if runtime.GOOS == "windows" && strings.Contains(msgLower, "exit status 1") {
  100. return
  101. }
  102. matches := logLineRegex.FindStringSubmatch(msg)
  103. if len(matches) > 3 {
  104. level := matches[2]
  105. msgBody := matches[3]
  106. msgBodyLower := strings.ToLower(msgBody)
  107. if strings.Contains(msgBodyLower, "tls handshake error") ||
  108. strings.Contains(msgBodyLower, "connection ends") {
  109. logger.Debug("XRAY: " + msgBody)
  110. lw.lastLine = ""
  111. return
  112. }
  113. if strings.Contains(msgBodyLower, "failed") {
  114. logger.Error("XRAY: " + msgBody)
  115. } else {
  116. switch level {
  117. case "Debug":
  118. logger.Debug("XRAY: " + msgBody)
  119. case "Info":
  120. logger.Info("XRAY: " + msgBody)
  121. case "Warning":
  122. logger.Warning("XRAY: " + msgBody)
  123. case "Error":
  124. logger.Error("XRAY: " + msgBody)
  125. default:
  126. logger.Debug("XRAY: " + msg)
  127. }
  128. }
  129. lw.lastLine = ""
  130. return
  131. }
  132. if strings.Contains(msgLower, "tls handshake error") ||
  133. strings.Contains(msgLower, "connection ends") {
  134. logger.Debug("XRAY: " + msg)
  135. lw.lastLine = msg
  136. return
  137. }
  138. if strings.Contains(msgLower, "failed") {
  139. logger.Error("XRAY: " + msg)
  140. } else {
  141. logger.Debug("XRAY: " + msg)
  142. }
  143. lw.lastLine = msg
  144. }