| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160 |
- package xray
- import (
- "regexp"
- "runtime"
- "strings"
- "sync"
- "github.com/mhsanaei/3x-ui/v3/internal/logger"
- )
- // Compiled once at package load: Write runs on every line Xray emits, so
- // recompiling these per write is wasted work.
- var (
- crashStartRegex = regexp.MustCompile(`^(panic|fatal error): `)
- logLineRegex = regexp.MustCompile(`^(\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}\.\d{6}) \[([^\]]+)\] (.+)$`)
- )
- // NewLogWriter returns a new LogWriter for processing Xray log output.
- func NewLogWriter() *LogWriter {
- return &LogWriter{}
- }
- // LogWriter splits Xray's stdout/stderr into lines and forwards them to the
- // panel log; once a Go panic or fatal error starts, all output is kept for one crash report.
- type LogWriter struct {
- mu sync.RWMutex
- pending string
- crash strings.Builder
- lastLine string
- }
- // LastLine returns the most recently processed Xray log line. It is safe for
- // concurrent use: Process.GetResult reads it from a different goroutine than the
- // one Xray drives Write from.
- func (lw *LogWriter) LastLine() string {
- lw.mu.RLock()
- defer lw.mu.RUnlock()
- return lw.lastLine
- }
- // Write buffers chunks into whole lines: the pipe splits output at arbitrary
- // points, e.g. the Go runtime writes "panic: " on its own before the message.
- func (lw *LogWriter) Write(m []byte) (n int, err error) {
- lw.mu.Lock()
- defer lw.mu.Unlock()
- if lw.crash.Len() > 0 {
- lw.crash.Write(m)
- return len(m), nil
- }
- lw.pending += string(m)
- for {
- i := strings.IndexByte(lw.pending, '\n')
- if i < 0 {
- break
- }
- line := lw.pending[:i]
- lw.pending = lw.pending[i+1:]
- if lw.startCrashLocked(line + "\n" + lw.pending) {
- lw.pending = ""
- break
- }
- lw.processLineLocked(line)
- }
- return len(m), nil
- }
- // Flush handles an unterminated final line and writes the crash report, if any;
- // it must run after cmd.Wait, which guarantees every output chunk was delivered.
- func (lw *LogWriter) Flush() {
- lw.mu.Lock()
- defer lw.mu.Unlock()
- if rest := lw.pending; rest != "" {
- lw.pending = ""
- if !lw.startCrashLocked(rest) {
- lw.processLineLocked(rest)
- }
- }
- if lw.crash.Len() == 0 {
- return
- }
- report := lw.crash.String()
- lw.crash.Reset()
- logger.Debug("Core crash detected:\n", report)
- if err := writeCrashReport([]byte(report)); err != nil {
- logger.Error("Unable to write crash report:", err)
- }
- }
- // startCrashLocked begins capturing a crash when output opens with a Go runtime
- // crash header; the header line becomes LastLine so GetResult shows the cause.
- func (lw *LogWriter) startCrashLocked(output string) bool {
- if lw.crash.Len() > 0 || !crashStartRegex.MatchString(output) {
- return false
- }
- header, _, _ := strings.Cut(output, "\n")
- lw.lastLine = strings.TrimSpace(header)
- lw.crash.WriteString(output)
- return true
- }
- func (lw *LogWriter) processLineLocked(line string) {
- msg := strings.TrimSpace(line)
- if msg == "" {
- return
- }
- msgLower := strings.ToLower(msg)
- // Suppress noisy Windows process-kill signal that surfaces as exit status 1
- if runtime.GOOS == "windows" && strings.Contains(msgLower, "exit status 1") {
- return
- }
- matches := logLineRegex.FindStringSubmatch(msg)
- if len(matches) > 3 {
- level := matches[2]
- msgBody := matches[3]
- msgBodyLower := strings.ToLower(msgBody)
- if strings.Contains(msgBodyLower, "tls handshake error") ||
- strings.Contains(msgBodyLower, "connection ends") {
- logger.Debug("XRAY: " + msgBody)
- lw.lastLine = ""
- return
- }
- if strings.Contains(msgBodyLower, "failed") {
- logger.Error("XRAY: " + msgBody)
- } else {
- switch level {
- case "Debug":
- logger.Debug("XRAY: " + msgBody)
- case "Info":
- logger.Info("XRAY: " + msgBody)
- case "Warning":
- logger.Warning("XRAY: " + msgBody)
- case "Error":
- logger.Error("XRAY: " + msgBody)
- default:
- logger.Debug("XRAY: " + msg)
- }
- }
- lw.lastLine = ""
- return
- }
- if strings.Contains(msgLower, "tls handshake error") ||
- strings.Contains(msgLower, "connection ends") {
- logger.Debug("XRAY: " + msg)
- lw.lastLine = msg
- return
- }
- if strings.Contains(msgLower, "failed") {
- logger.Error("XRAY: " + msg)
- } else {
- logger.Debug("XRAY: " + msg)
- }
- lw.lastLine = msg
- }
|