Files
2026-07-11 22:23:50 +08:00

378 lines
11 KiB
Go

//go:build darwin || linux || windows
package libbox
import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"time"
"github.com/sagernet/sing-box/common/trafficcontrol"
"github.com/sagernet/sing-box/daemon"
"github.com/sagernet/sing-box/experimental/libbox/internal/oomprofile"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/service/oomkiller"
"github.com/sagernet/sing/common/byteformats"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/memory"
)
var oomReportProfiles = []string{
"allocs",
"block",
"goroutine",
"heap",
"mutex",
"threadcreate",
}
type oomReportMetadata struct {
reportMetadata
RecordedAt string `json:"recordedAt"`
MemoryUsage string `json:"memoryUsage"`
AvailableMemory string `json:"availableMemory,omitempty"`
// Heap
HeapAlloc string `json:"heapAlloc,omitempty"`
HeapObjects uint64 `json:"heapObjects,omitempty,string"`
HeapInuse string `json:"heapInuse,omitempty"`
HeapIdle string `json:"heapIdle,omitempty"`
HeapReleased string `json:"heapReleased,omitempty"`
HeapSys string `json:"heapSys,omitempty"`
// Stack
StackInuse string `json:"stackInuse,omitempty"`
StackSys string `json:"stackSys,omitempty"`
// Runtime metadata
MSpanInuse string `json:"mSpanInuse,omitempty"`
MSpanSys string `json:"mSpanSys,omitempty"`
MCacheSys string `json:"mCacheSys,omitempty"`
BuckHashSys string `json:"buckHashSys,omitempty"`
GCSys string `json:"gcSys,omitempty"`
OtherSys string `json:"otherSys,omitempty"`
Sys string `json:"sys,omitempty"`
// GC & runtime
TotalAlloc string `json:"totalAlloc,omitempty"`
NumGC uint32 `json:"numGC,omitempty,string"`
NumGoroutine int `json:"numGoroutine,omitempty,string"`
NextGC string `json:"nextGC,omitempty"`
LastGC string `json:"lastGC,omitempty"`
}
type oomReporter struct {
startedService *daemon.StartedService
}
var _ oomkiller.OOMReporter = (*oomReporter)(nil)
func NewOOMReporter(startedService *daemon.StartedService) oomkiller.OOMReporter {
return &oomReporter{startedService: startedService}
}
func (r *oomReporter) WriteReport(memoryUsage uint64) error {
draftPath := filepath.Join(sWorkingPath, "oom_draft")
draftInfo, err := os.Stat(draftPath)
if err != nil {
if !os.IsNotExist(err) {
return err
}
draftInfo = nil
}
reportsDir := filepath.Join(sWorkingPath, "oom_reports")
err = os.MkdirAll(reportsDir, 0o777)
if err != nil {
return err
}
chownReport(reportsDir)
destPath, err := nextAvailableReportPath(reportsDir, time.Now().UTC())
if err != nil {
return err
}
err = r.writeSnapshot(destPath, memoryUsage)
if err != nil {
return err
}
return discardDraftIfCurrent(draftPath, draftInfo)
}
func (r *oomReporter) WriteDraft(memoryUsage uint64) error {
draftPath := filepath.Join(sWorkingPath, "oom_draft")
os.RemoveAll(draftPath)
return r.writeSnapshot(draftPath, memoryUsage)
}
func (r *oomReporter) DiscardDraft() error {
draftPath := filepath.Join(sWorkingPath, "oom_draft")
return os.RemoveAll(draftPath)
}
func discardDraftIfCurrent(draftPath string, draftInfo os.FileInfo) error {
if draftInfo == nil {
return nil
}
currentInfo, err := os.Stat(draftPath)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
if !os.SameFile(draftInfo, currentInfo) {
return nil
}
return os.RemoveAll(draftPath)
}
func (r *oomReporter) writeSnapshot(destPath string, memoryUsage uint64) error {
now := time.Now().UTC()
err := os.MkdirAll(destPath, 0o777)
if err != nil {
return err
}
chownReport(destPath)
for _, name := range oomReportProfiles {
writeOOMProfile(destPath, name)
}
writeReportFile(destPath, "cmdline", []byte(strings.Join(os.Args, "\000")))
var memStats runtime.MemStats
runtime.ReadMemStats(&memStats)
metadata := oomReportMetadata{
reportMetadata: baseReportMetadata(),
RecordedAt: now.Format(time.RFC3339),
MemoryUsage: byteformats.FormatMemoryBytes(memoryUsage),
// Heap
HeapAlloc: byteformats.FormatMemoryBytes(memStats.HeapAlloc),
HeapObjects: memStats.HeapObjects,
HeapInuse: byteformats.FormatMemoryBytes(memStats.HeapInuse),
HeapIdle: byteformats.FormatMemoryBytes(memStats.HeapIdle),
HeapReleased: byteformats.FormatMemoryBytes(memStats.HeapReleased),
HeapSys: byteformats.FormatMemoryBytes(memStats.HeapSys),
// Stack
StackInuse: byteformats.FormatMemoryBytes(memStats.StackInuse),
StackSys: byteformats.FormatMemoryBytes(memStats.StackSys),
// Runtime metadata
MSpanInuse: byteformats.FormatMemoryBytes(memStats.MSpanInuse),
MSpanSys: byteformats.FormatMemoryBytes(memStats.MSpanSys),
MCacheSys: byteformats.FormatMemoryBytes(memStats.MCacheSys),
BuckHashSys: byteformats.FormatMemoryBytes(memStats.BuckHashSys),
GCSys: byteformats.FormatMemoryBytes(memStats.GCSys),
OtherSys: byteformats.FormatMemoryBytes(memStats.OtherSys),
Sys: byteformats.FormatMemoryBytes(memStats.Sys),
// GC & runtime
TotalAlloc: byteformats.FormatMemoryBytes(memStats.TotalAlloc),
NumGC: memStats.NumGC,
NumGoroutine: runtime.NumGoroutine(),
NextGC: byteformats.FormatMemoryBytes(memStats.NextGC),
}
if memStats.LastGC > 0 {
metadata.LastGC = time.Unix(0, int64(memStats.LastGC)).UTC().Format(time.RFC3339)
}
availableMemory := memory.Available()
if availableMemory > 0 {
metadata.AvailableMemory = byteformats.FormatMemoryBytes(availableMemory)
}
writeReportMetadata(destPath, metadata)
copyConfigSnapshot(destPath)
writeOOMLog(destPath, r.startedService.SavedLog())
r.writeOOMConnections(destPath)
return nil
}
type oomConnectionsInfo struct {
UploadTotal string `json:"uploadTotal,omitempty"`
DownloadTotal string `json:"downloadTotal,omitempty"`
Connections []oomConnectionInfo `json:"connections"`
ClosedConnections []oomConnectionInfo `json:"closedConnections,omitempty"`
}
type oomConnectionInfo struct {
ID string `json:"id"`
CreatedAt string `json:"createdAt"`
ClosedAt string `json:"closedAt,omitempty"`
Inbound string `json:"inbound,omitempty"`
Network string `json:"network,omitempty"`
Source string `json:"source,omitempty"`
Destination string `json:"destination,omitempty"`
Host string `json:"host,omitempty"`
User string `json:"user,omitempty"`
Process string `json:"process,omitempty"`
Rule string `json:"rule,omitempty"`
Chain []string `json:"chain,omitempty"`
Outbound string `json:"outbound,omitempty"`
OutboundType string `json:"outboundType,omitempty"`
Upload string `json:"upload,omitempty"`
Download string `json:"download,omitempty"`
}
func (r *oomReporter) writeOOMConnections(destPath string) {
instance := r.startedService.Instance()
if instance == nil {
return
}
trafficManager := instance.TrafficManager()
if trafficManager == nil {
return
}
connections := trafficManager.Connections()
sort.Slice(connections, func(i, j int) bool {
return connections[i].CreatedAt.Before(connections[j].CreatedAt)
})
uploadTotal, downloadTotal := trafficManager.Total()
info := oomConnectionsInfo{
UploadTotal: byteformats.FormatBytes(uint64(uploadTotal)),
DownloadTotal: byteformats.FormatBytes(uint64(downloadTotal)),
Connections: buildOOMConnections(connections),
ClosedConnections: buildOOMConnections(trafficManager.ClosedConnections()),
}
data, err := json.MarshalIndent(info, "", " ")
if err != nil {
return
}
writeReportFile(destPath, "connections.json", data)
}
func buildOOMConnections(connections []*trafficcontrol.TrackerMetadata) []oomConnectionInfo {
result := make([]oomConnectionInfo, 0, len(connections))
for _, connection := range connections {
result = append(result, buildOOMConnection(connection))
}
return result
}
func buildOOMConnection(connection *trafficcontrol.TrackerMetadata) oomConnectionInfo {
metadata := connection.Metadata
var inbound string
if metadata.Inbound != "" {
inbound = metadata.InboundType + "/" + metadata.Inbound
} else {
inbound = metadata.InboundType
}
var process string
if processInfo := metadata.ProcessInfo; processInfo != nil {
if processInfo.ProcessPath != "" {
process = processInfo.ProcessPath
} else if len(processInfo.AndroidPackageNames) > 0 {
process = processInfo.AndroidPackageNames[0]
}
if process == "" {
if processInfo.UserId != -1 {
process = F.ToString(processInfo.UserId)
}
} else if processInfo.UserName != "" {
process = F.ToString(process, " (", processInfo.UserName, ")")
} else if processInfo.UserId != -1 {
process = F.ToString(process, " (", processInfo.UserId, ")")
}
}
var rule string
if connection.Rule != nil {
rule = F.ToString(connection.Rule, " => ", connection.Rule.Action())
} else {
rule = "final"
}
info := oomConnectionInfo{
ID: connection.ID.String(),
CreatedAt: connection.CreatedAt.UTC().Format(time.RFC3339),
Inbound: inbound,
Network: metadata.Network,
Source: metadata.Source.String(),
Destination: metadata.Destination.String(),
Host: metadata.Domain,
User: metadata.User,
Process: process,
Rule: rule,
Chain: connection.Chain,
Outbound: connection.Outbound,
OutboundType: connection.OutboundType,
Upload: byteformats.FormatBytes(uint64(connection.Upload.Load())),
Download: byteformats.FormatBytes(uint64(connection.Download.Load())),
}
if !connection.ClosedAt.IsZero() {
info.ClosedAt = connection.ClosedAt.UTC().Format(time.RFC3339)
}
return info
}
func writeOOMLog(destPath string, entries []*log.Entry) {
if len(entries) == 0 {
return
}
var buffer bytes.Buffer
for _, entry := range entries {
writeWithoutColors(&buffer, entry.Message)
buffer.WriteByte('\n')
}
writeReportFile(destPath, "go.log", buffer.Bytes())
}
func writeWithoutColors(buffer *bytes.Buffer, message string) {
start := 0
for index := 0; index < len(message); {
if message[index] != '\x1b' || index+1 >= len(message) || message[index+1] != '[' {
index++
continue
}
end := index + 2
for end < len(message) && message[end] != 'm' {
end++
}
if end >= len(message) {
break
}
buffer.WriteString(message[start:index])
index = end + 1
start = index
}
buffer.WriteString(message[start:])
}
func writeOOMProfile(destPath string, name string) {
filePath, err := oomprofile.WriteFile(destPath, name)
if err != nil {
return
}
chownReport(filePath)
}
func promoteOOMDraftAt(workingPath string) {
draftPath := filepath.Join(workingPath, "oom_draft")
info, err := os.Stat(draftPath)
if err != nil || !info.IsDir() {
return
}
reportsDir := filepath.Join(workingPath, "oom_reports")
initReportDir(reportsDir)
destPath, err := nextAvailableReportPath(reportsDir, info.ModTime().UTC())
if err != nil {
os.RemoveAll(draftPath)
return
}
err = os.Rename(draftPath, destPath)
if err != nil {
os.RemoveAll(draftPath)
return
}
chownReport(destPath)
}
func promoteOOMDraft() {
promoteOOMDraftAt(sWorkingPath)
}
func PromoteOOMDraft() {
promoteOOMDraft()
}
func PromoteOOMDraftAt(workingPath string) {
promoteOOMDraftAt(workingPath)
}