Files
3x-ui/internal/xray/log_writer.go
T
n0ctal 2bb29468d8 fix(xray): guard log-writer race and bound handler gRPC deadlines (#5442)
* perf(xray): compile log/traffic regexps once at package scope

GetTraffic recompiled two stats regexps on every traffic tick, and LogWriter.Write
recompiled two more on every log line. Hoist all four to package-level vars so they
compile once at load instead of per call on hot paths.

* fix(xray): guard LogWriter.lastLine against the GetResult reader race

Write is driven by the Xray process goroutine while Process.GetResult
reads lastLine from the caller's goroutine, so the unsynchronized field
is a data race under `go test -race`. Add an RWMutex and route every
write through setLastLine; GetResult reads via LastLine().

* fix(xray): bound handler gRPC calls with a deadline

AddInbound, DelInbound and the AddUser AlterInbound call used
context.Background(), so a hung core connection could block the caller
indefinitely (for example while the process restart lock is held). Give
them a 10s deadline (handlerRPCTimeout) and a nil-client guard, matching
the other handler operations.
2026-06-20 18:10:18 +02:00

122 lines
3.1 KiB
Go

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 (
crashRegex = regexp.MustCompile(`(?i)(panic|exception|stack trace|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 processes and filters log output from the Xray process, handling crash detection and message filtering.
type LogWriter struct {
mu sync.RWMutex
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
}
func (lw *LogWriter) setLastLine(line string) {
lw.mu.Lock()
lw.lastLine = line
lw.mu.Unlock()
}
// Write processes and filters log output from the Xray process, handling crash detection and message filtering.
func (lw *LogWriter) Write(m []byte) (n int, err error) {
// Convert the data to a string
message := strings.TrimSpace(string(m))
msgLowerAll := strings.ToLower(message)
// Suppress noisy Windows process-kill signal that surfaces as exit status 1
if runtime.GOOS == "windows" && strings.Contains(msgLowerAll, "exit status 1") {
return len(m), nil
}
// Check if the message contains a crash
if crashRegex.MatchString(message) {
logger.Debug("Core crash detected:\n", message)
lw.setLastLine(message)
err1 := writeCrashReport(m)
if err1 != nil {
logger.Error("Unable to write crash report:", err1)
}
return len(m), nil
}
messages := strings.SplitSeq(message, "\n")
for msg := range messages {
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.setLastLine("")
continue
}
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.setLastLine("")
} else if msg != "" {
msgLower := strings.ToLower(msg)
if strings.Contains(msgLower, "tls handshake error") ||
strings.Contains(msgLower, "connection ends") {
logger.Debug("XRAY: " + msg)
lw.setLastLine(msg)
continue
}
if strings.Contains(msgLower, "failed") {
logger.Error("XRAY: " + msg)
} else {
logger.Debug("XRAY: " + msg)
}
lw.setLastLine(msg)
}
}
return len(m), nil
}