|
@@ -12,8 +12,8 @@ import (
|
|
|
// Compiled once at package load: Write runs on every line Xray emits, so
|
|
// Compiled once at package load: Write runs on every line Xray emits, so
|
|
|
// recompiling these per write is wasted work.
|
|
// recompiling these per write is wasted work.
|
|
|
var (
|
|
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}) \[([^\]]+)\] (.+)$`)
|
|
|
|
|
|
|
+ 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.
|
|
// NewLogWriter returns a new LogWriter for processing Xray log output.
|
|
@@ -21,9 +21,12 @@ func NewLogWriter() *LogWriter {
|
|
|
return &LogWriter{}
|
|
return &LogWriter{}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// LogWriter processes and filters log output from the Xray process, handling crash detection and message filtering.
|
|
|
|
|
|
|
+// 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 {
|
|
type LogWriter struct {
|
|
|
mu sync.RWMutex
|
|
mu sync.RWMutex
|
|
|
|
|
+ pending string
|
|
|
|
|
+ crash strings.Builder
|
|
|
lastLine string
|
|
lastLine string
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -36,86 +39,122 @@ func (lw *LogWriter) LastLine() string {
|
|
|
return lw.lastLine
|
|
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.
|
|
|
|
|
|
|
+// 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) {
|
|
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") {
|
|
|
|
|
|
|
+ lw.mu.Lock()
|
|
|
|
|
+ defer lw.mu.Unlock()
|
|
|
|
|
+ if lw.crash.Len() > 0 {
|
|
|
|
|
+ lw.crash.Write(m)
|
|
|
return len(m), nil
|
|
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
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- // 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)
|
|
|
|
|
|
|
+// 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)
|
|
|
}
|
|
}
|
|
|
- return len(m), nil
|
|
|
|
|
}
|
|
}
|
|
|
|
|
+ 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
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- messages := strings.SplitSeq(message, "\n")
|
|
|
|
|
|
|
+func (lw *LogWriter) processLineLocked(line string) {
|
|
|
|
|
+ msg := strings.TrimSpace(line)
|
|
|
|
|
+ if msg == "" {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ msgLower := strings.ToLower(msg)
|
|
|
|
|
|
|
|
- for msg := range messages {
|
|
|
|
|
- matches := logLineRegex.FindStringSubmatch(msg)
|
|
|
|
|
|
|
+ // Suppress noisy Windows process-kill signal that surfaces as exit status 1
|
|
|
|
|
+ if runtime.GOOS == "windows" && strings.Contains(msgLower, "exit status 1") {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- if len(matches) > 3 {
|
|
|
|
|
- level := matches[2]
|
|
|
|
|
- msgBody := matches[3]
|
|
|
|
|
- msgBodyLower := strings.ToLower(msgBody)
|
|
|
|
|
|
|
+ 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, "tls handshake error") ||
|
|
|
|
|
- strings.Contains(msgBodyLower, "connection ends") {
|
|
|
|
|
|
|
+ if strings.Contains(msgBodyLower, "failed") {
|
|
|
|
|
+ logger.Error("XRAY: " + msgBody)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ switch level {
|
|
|
|
|
+ case "Debug":
|
|
|
logger.Debug("XRAY: " + msgBody)
|
|
logger.Debug("XRAY: " + msgBody)
|
|
|
- lw.setLastLine("")
|
|
|
|
|
- continue
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if strings.Contains(msgBodyLower, "failed") {
|
|
|
|
|
|
|
+ case "Info":
|
|
|
|
|
+ logger.Info("XRAY: " + msgBody)
|
|
|
|
|
+ case "Warning":
|
|
|
|
|
+ logger.Warning("XRAY: " + msgBody)
|
|
|
|
|
+ case "Error":
|
|
|
logger.Error("XRAY: " + msgBody)
|
|
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 {
|
|
|
|
|
|
|
+ default:
|
|
|
logger.Debug("XRAY: " + msg)
|
|
logger.Debug("XRAY: " + msg)
|
|
|
}
|
|
}
|
|
|
- lw.setLastLine(msg)
|
|
|
|
|
}
|
|
}
|
|
|
|
|
+ lw.lastLine = ""
|
|
|
|
|
+ return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- return len(m), nil
|
|
|
|
|
|
|
+ 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
|
|
|
}
|
|
}
|