Files
sing-box-extended-mirror/experimental/libbox/oom_report.go
T

259 lines
7.8 KiB
Go

//go:build darwin || linux || windows
package libbox
import (
"bytes"
"encoding/json"
"path/filepath"
"sort"
"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/experimental/libbox/internal/runtimeinfo"
"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"
)
var oomReportProfiles = []string{
"allocs",
"block",
"goroutine",
"heap",
"mutex",
"threadcreate",
}
type oomReportMetadata struct {
reportMetadata
RecordedAt string `json:"recordedAt"`
EndedAt string `json:"endedAt,omitempty"`
MemoryLimit string `json:"memoryLimit,omitempty"`
MemoryUsage string `json:"memoryUsage"`
AvailableMemory string `json:"availableMemory,omitempty"`
Snapshots int `json:"snapshots,omitempty,string"`
}
func OOMRecorderOptions(startedService *daemon.StartedService) oomkiller.RecorderOptions {
return oomkiller.RecorderOptions{
BasePath: sWorkingPath,
Logger: log.StdLogger(),
AcceptDraft: acceptOOMDraft,
MetadataCallback: func(status oomkiller.ReportStatus) any {
metadata := oomReportMetadata{
reportMetadata: baseReportMetadata(),
RecordedAt: status.RecordedAt.UTC().Format(time.RFC3339),
MemoryUsage: byteformats.FormatMemoryBytes(status.PeakMemory),
Snapshots: status.Snapshots,
}
metadata.StartedAt = status.StartedAt.UTC().Format(time.RFC3339)
if !status.EndedAt.IsZero() {
metadata.EndedAt = status.EndedAt.UTC().Format(time.RFC3339)
}
if status.MemoryLimit > 0 {
metadata.MemoryLimit = byteformats.FormatMemoryBytes(status.MemoryLimit)
}
if status.AvailableKnown {
metadata.AvailableMemory = byteformats.FormatMemoryBytes(status.MinAvailable)
}
return metadata
},
OwnerCallback: chownReport,
LogCallback: func() []byte {
return formatLogEntries(startedService.SavedLog())
},
SnapshotCallback: func(directory string, prefix string) {
for _, name := range oomReportProfiles {
writeOOMProfile(filepath.Join(directory, prefix+"."+name+".pb"), name)
}
runtimeInfoPath := filepath.Join(directory, prefix+".runtime.json")
err := runtimeinfo.WriteFile(runtimeInfoPath)
if err == nil {
chownReport(runtimeInfoPath)
}
copyConfigSnapshot(directory)
content := oomConnectionsContent(startedService)
if content != nil {
writeReportFile(directory, prefix+".connections.json", content)
}
},
}
}
func acceptOOMDraft(metadataContent []byte) bool {
var draftMetadata reportMetadata
err := json.Unmarshal(metadataContent, &draftMetadata)
if err != nil {
return false
}
return draftMetadata.AppVersion == sAppVersion && draftMetadata.AppMarketingVersion == sAppMarketingVersion
}
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 oomConnectionsContent(startedService *daemon.StartedService) []byte {
instance := startedService.Instance()
if instance == nil {
return nil
}
trafficManager := instance.TrafficManager()
if trafficManager == nil {
return nil
}
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 nil
}
return 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 formatLogEntries(entries []*log.Entry) []byte {
if len(entries) == 0 {
return nil
}
var buffer bytes.Buffer
for _, entry := range entries {
writeWithoutColors(&buffer, entry.Message)
buffer.WriteByte('\n')
}
return 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(filePath string, name string) {
err := oomprofile.WriteFile(filePath, name)
if err != nil {
return
}
chownReport(filePath)
}
func PromoteOOMDraft() {
oomkiller.PromoteDraft(sWorkingPath, acceptOOMDraft)
}
func PromoteOOMDraftAt(workingPath string) {
oomkiller.PromoteDraft(workingPath, acceptOOMDraft)
}