Add power report service

This commit is contained in:
世界
2026-08-30 17:41:46 +08:00
parent 6d431a2c43
commit 82098a8760
32 changed files with 1772 additions and 86 deletions
+70 -12
View File
@@ -12,7 +12,9 @@ import (
"github.com/sagernet/sing-box/common/listener"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/service/powerreport"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/bufio"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
@@ -39,6 +41,7 @@ type DefaultDialer struct {
autoDetectBindFunc control.Func
connectionManager adapter.ConnectionManager
networkManager adapter.NetworkManager
powerManager *powerreport.Manager
networkStrategy *C.NetworkStrategy
defaultNetworkStrategy bool
networkType []C.InterfaceType
@@ -231,6 +234,7 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
autoDetectBindFunc: autoDetectBindFunc,
connectionManager: connectionManager,
networkManager: networkManager,
powerManager: service.FromContext[*powerreport.Manager](ctx),
networkStrategy: networkStrategy,
defaultNetworkStrategy: defaultNetworkStrategy,
networkType: networkType,
@@ -262,7 +266,7 @@ func (d *DefaultDialer) DialContext(ctx context.Context, network string, address
return nil, E.New("domain not resolved")
}
if d.networkStrategy == nil {
return d.trackConn(listener.ListenNetworkNamespace[net.Conn](ctx, d.netns, func() (net.Conn, error) {
conn, err := listener.ListenNetworkNamespace[net.Conn](ctx, d.netns, func() (net.Conn, error) {
switch N.NetworkName(network) {
case N.NetworkUDP:
if !address.IsIPv6() {
@@ -276,7 +280,8 @@ func (d *DefaultDialer) DialContext(ctx context.Context, network string, address
} else {
return DialSlowContext(&d.dialer6, ctx, network, address)
}
}))
})
return d.trackConn(ctx, address, conn, err)
} else {
return d.DialParallelInterface(ctx, network, address, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay)
}
@@ -327,12 +332,12 @@ func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network strin
if !fastFallback && !isPrimary {
d.networkLastFallback.Store(time.Now())
}
return d.trackConn(conn, nil)
return d.trackConn(ctx, address, conn, nil)
}
func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
if d.networkStrategy == nil {
return d.trackPacketConn(listener.ListenNetworkNamespace[net.PacketConn](ctx, d.netns, func() (net.PacketConn, error) {
packetConn, err := listener.ListenNetworkNamespace[net.PacketConn](ctx, d.netns, func() (net.PacketConn, error) {
listenConfig := d.udpListener
if d.autoDetectBindFunc != nil {
listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error {
@@ -349,7 +354,8 @@ func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksadd
} else {
return listenConfig.ListenPacket(ctx, N.NetworkUDP, d.udpAddr4)
}
}))
})
return d.trackPacketConn(ctx, destination, packetConn, err)
} else {
return d.ListenSerialInterfacePacket(ctx, destination, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay)
}
@@ -393,7 +399,7 @@ func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destina
return nil, err
}
}
return d.trackPacketConn(packetConn, nil)
return d.trackPacketConn(ctx, destination, packetConn, nil)
}
func (d *DefaultDialer) UDPListenerControl() (control.Func, bool) {
@@ -405,16 +411,68 @@ func (d *DefaultDialer) UDPListenerControl() (control.Func, bool) {
return listenerControl, egressEnabled
}
func (d *DefaultDialer) trackConn(conn net.Conn, err error) (net.Conn, error) {
if d.connectionManager == nil || err != nil {
func (d *DefaultDialer) trackConn(ctx context.Context, destination M.Socksaddr, conn net.Conn, err error) (net.Conn, error) {
if err != nil {
return conn, err
}
return d.connectionManager.TrackConn(conn), nil
if d.connectionManager != nil {
conn = d.connectionManager.TrackConn(conn)
}
if d.powerManager != nil {
recorder := d.powerManager.Recorder()
if recorder != nil {
recorder.CountConnectionOpened()
attribution := dialAttribution(ctx, destination)
conn = bufio.NewCounterConn(conn, []N.CountFunc{func(n int64) {
recorder.Touch(powerreport.DirectionInbound, int(n), attribution)
}}, []N.CountFunc{func(n int64) {
recorder.Touch(powerreport.DirectionOutbound, int(n), attribution)
}})
}
}
return conn, nil
}
func (d *DefaultDialer) trackPacketConn(conn net.PacketConn, err error) (net.PacketConn, error) {
if d.connectionManager == nil || err != nil {
func (d *DefaultDialer) trackPacketConn(ctx context.Context, destination M.Socksaddr, conn net.PacketConn, err error) (net.PacketConn, error) {
if err != nil {
return conn, err
}
return d.connectionManager.TrackPacketConn(conn), nil
if d.connectionManager != nil {
conn = d.connectionManager.TrackPacketConn(conn)
}
if d.powerManager != nil {
recorder := d.powerManager.Recorder()
if recorder != nil {
recorder.CountConnectionOpened()
attribution := dialAttribution(ctx, destination)
conn = bufio.NewNetPacketConn(bufio.NewCounterPacketConn(bufio.NewPacketConn(conn), []N.CountFunc{func(n int64) {
recorder.Touch(powerreport.DirectionInbound, int(n), attribution)
}}, []N.CountFunc{func(n int64) {
recorder.Touch(powerreport.DirectionOutbound, int(n), attribution)
}}))
}
}
return conn, nil
}
func dialAttribution(ctx context.Context, destination M.Socksaddr) *powerreport.Attribution {
metadata := adapter.ContextFrom(ctx)
if metadata == nil {
return &powerreport.Attribution{Destination: destination.String()}
}
attribution := &powerreport.Attribution{
Domain: metadata.Domain,
Outbound: metadata.Outbound,
}
if metadata.Inbound != "" {
attribution.Inbound = metadata.InboundType + "/" + metadata.Inbound
} else {
attribution.Inbound = metadata.InboundType
}
if metadata.Destination.IsValid() {
attribution.Destination = metadata.Destination.String()
} else {
attribution.Destination = destination.String()
}
return attribution
}
+9
View File
@@ -16,6 +16,7 @@ import (
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
R "github.com/sagernet/sing-box/route/rule"
"github.com/sagernet/sing-box/service/powerreport"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
@@ -39,6 +40,7 @@ type Router struct {
logger logger.ContextLogger
transport adapter.DNSTransportManager
outbound adapter.OutboundManager
powerManager *powerreport.Manager
client adapter.DNSClient
rawRules []option.DNSRule
rules []adapter.DNSRule
@@ -57,6 +59,7 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOp
logger: logFactory.NewLogger("dns"),
transport: service.FromContext[adapter.DNSTransportManager](ctx),
outbound: service.FromContext[adapter.OutboundManager](ctx),
powerManager: service.FromContext[*powerreport.Manager](ctx),
rawRules: make([]option.DNSRule, 0, len(options.Rules)),
rules: make([]adapter.DNSRule, 0, len(options.Rules)),
defaultDomainStrategy: C.DomainStrategy(options.Strategy),
@@ -1053,6 +1056,12 @@ type dnsExchangeContext struct {
}
func (r *Router) prepareExchange(ctx context.Context, message *mDNS.Msg) (*dnsExchangeContext, *mDNS.Msg, error) {
if r.powerManager != nil {
recorder := r.powerManager.Recorder()
if recorder != nil {
recorder.CountDNSQuery()
}
}
if len(message.Question) != 1 {
r.logger.WarnContext(ctx, "bad question size: ", len(message.Question))
return nil, &mDNS.Msg{
+1
View File
@@ -65,6 +65,7 @@ func prepareWorkingDirectory() error {
return err
}
libbox.PromoteOOMDraft()
libbox.PromotePowerReportDraft()
return nil
}
+1
View File
@@ -87,6 +87,7 @@ func (s *desktopService) StartService(ctx context.Context, request *StartService
mergedOptions.OOMKillerEnabled = request.Options.OomKillerEnabled
mergedOptions.OOMKillerDisabled = request.Options.OomKillerDisabled
mergedOptions.OOMMemoryLimit = request.Options.OomMemoryLimit
mergedOptions.PowerReportEnabled = request.Options.PowerReportEnabled
}
err = s.daemon.startServiceLocked(ctx, identity.UserID, request.ConfigContent, mergedOptions)
if err != nil {
+81 -54
View File
@@ -449,12 +449,13 @@ func (x *StartServiceRequest) GetOptions() *StartOptions {
}
type StartOptions struct {
state protoimpl.MessageState `protogen:"open.v1"`
OomKillerEnabled bool `protobuf:"varint,1,opt,name=oom_killer_enabled,json=oomKillerEnabled,proto3" json:"oom_killer_enabled,omitempty"`
OomKillerDisabled bool `protobuf:"varint,2,opt,name=oom_killer_disabled,json=oomKillerDisabled,proto3" json:"oom_killer_disabled,omitempty"`
OomMemoryLimit int64 `protobuf:"varint,3,opt,name=oom_memory_limit,json=oomMemoryLimit,proto3" json:"oom_memory_limit,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
OomKillerEnabled bool `protobuf:"varint,1,opt,name=oom_killer_enabled,json=oomKillerEnabled,proto3" json:"oom_killer_enabled,omitempty"`
OomKillerDisabled bool `protobuf:"varint,2,opt,name=oom_killer_disabled,json=oomKillerDisabled,proto3" json:"oom_killer_disabled,omitempty"`
OomMemoryLimit int64 `protobuf:"varint,3,opt,name=oom_memory_limit,json=oomMemoryLimit,proto3" json:"oom_memory_limit,omitempty"`
PowerReportEnabled bool `protobuf:"varint,4,opt,name=power_report_enabled,json=powerReportEnabled,proto3" json:"power_report_enabled,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *StartOptions) Reset() {
@@ -508,6 +509,13 @@ func (x *StartOptions) GetOomMemoryLimit() int64 {
return 0
}
func (x *StartOptions) GetPowerReportEnabled() bool {
if x != nil {
return x.PowerReportEnabled
}
return false
}
type ConfigContent struct {
state protoimpl.MessageState `protogen:"open.v1"`
Content string `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"`
@@ -1676,11 +1684,12 @@ const file_experimental_boxdd_desktop_service_proto_rawDesc = "" +
"\townership\x18\x02 \x01(\x0e2\x18.desktop.DaemonOwnershipR\townership\"m\n" +
"\x13StartServiceRequest\x12%\n" +
"\x0econfig_content\x18\x01 \x01(\tR\rconfigContent\x12/\n" +
"\aoptions\x18\x02 \x01(\v2\x15.desktop.StartOptionsR\aoptions\"\x96\x01\n" +
"\aoptions\x18\x02 \x01(\v2\x15.desktop.StartOptionsR\aoptions\"\xc8\x01\n" +
"\fStartOptions\x12,\n" +
"\x12oom_killer_enabled\x18\x01 \x01(\bR\x10oomKillerEnabled\x12.\n" +
"\x13oom_killer_disabled\x18\x02 \x01(\bR\x11oomKillerDisabled\x12(\n" +
"\x10oom_memory_limit\x18\x03 \x01(\x03R\x0eoomMemoryLimit\")\n" +
"\x10oom_memory_limit\x18\x03 \x01(\x03R\x0eoomMemoryLimit\x120\n" +
"\x14power_report_enabled\x18\x04 \x01(\bR\x12powerReportEnabled\")\n" +
"\rConfigContent\x12\x18\n" +
"\acontent\x18\x01 \x01(\tR\acontent\"\xb0\x02\n" +
"\x0eProfileContent\x120\n" +
@@ -1767,7 +1776,7 @@ const file_experimental_boxdd_desktop_service_proto_rawDesc = "" +
"!INSTALL_UPDATE_RESULT_UNSPECIFIED\x10\x00\x12!\n" +
"\x1dINSTALL_UPDATE_RESULT_STARTED\x10\x01\x12)\n" +
"%INSTALL_UPDATE_RESULT_SIGNER_MISMATCH\x10\x02\x12#\n" +
"\x1fINSTALL_UPDATE_RESULT_NOT_NEWER\x10\x032\x86\r\n" +
"\x1fINSTALL_UPDATE_RESULT_NOT_NEWER\x10\x032\xcd\x10\n" +
"\x0eDesktopService\x12>\n" +
"\rGetDaemonInfo\x12\x16.google.protobuf.Empty\x1a\x13.desktop.DaemonInfo\"\x00\x12@\n" +
"\fClaimService\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12C\n" +
@@ -1786,7 +1795,13 @@ const file_experimental_boxdd_desktop_service_proto_rawDesc = "" +
"\x11MarkOOMReportRead\x12\x19.desktop.OOMReportRequest\x1a\x16.google.protobuf.Empty\"\x00\x12Q\n" +
"\x0fExportOOMReport\x12\x1f.desktop.OOMReportExportRequest\x1a\x1b.desktop.CrashReportArchive\"\x00\x12F\n" +
"\x0fDeleteOOMReport\x12\x19.desktop.OOMReportRequest\x1a\x16.google.protobuf.Empty\"\x00\x12G\n" +
"\x13DeleteAllOOMReports\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12P\n" +
"\x13DeleteAllOOMReports\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12D\n" +
"\x10ListPowerReports\x12\x16.google.protobuf.Empty\x1a\x16.desktop.OOMReportList\"\x00\x12I\n" +
"\x0fReadPowerReport\x12\x19.desktop.OOMReportRequest\x1a\x19.desktop.OOMReportContent\"\x00\x12J\n" +
"\x13MarkPowerReportRead\x12\x19.desktop.OOMReportRequest\x1a\x16.google.protobuf.Empty\"\x00\x12S\n" +
"\x11ExportPowerReport\x12\x1f.desktop.OOMReportExportRequest\x1a\x1b.desktop.CrashReportArchive\"\x00\x12H\n" +
"\x11DeletePowerReport\x12\x19.desktop.OOMReportRequest\x1a\x16.google.protobuf.Empty\"\x00\x12I\n" +
"\x15DeleteAllPowerReports\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12P\n" +
"\rInstallUpdate\x12\x1d.desktop.InstallUpdateRequest\x1a\x1e.desktop.InstallUpdateResponse\"\x00\x12J\n" +
"\x13GetSecuritySettings\x12\x16.google.protobuf.Empty\x1a\x19.desktop.SecuritySettings\"\x00\x12Z\n" +
"\x16SetInsecureModeEnabled\x12&.desktop.SetInsecureModeEnabledRequest\x1a\x16.google.protobuf.Empty\"\x00\x12@\n" +
@@ -1881,50 +1896,62 @@ var file_experimental_boxdd_desktop_service_proto_depIdxs = []int32{
23, // 23: desktop.DesktopService.ExportOOMReport:input_type -> desktop.OOMReportExportRequest
22, // 24: desktop.DesktopService.DeleteOOMReport:input_type -> desktop.OOMReportRequest
31, // 25: desktop.DesktopService.DeleteAllOOMReports:input_type -> google.protobuf.Empty
29, // 26: desktop.DesktopService.InstallUpdate:input_type -> desktop.InstallUpdateRequest
31, // 27: desktop.DesktopService.GetSecuritySettings:input_type -> google.protobuf.Empty
27, // 28: desktop.DesktopService.SetInsecureModeEnabled:input_type -> desktop.SetInsecureModeEnabledRequest
28, // 29: desktop.DesktopService.SetLocale:input_type -> desktop.SetLocaleRequest
9, // 30: desktop.ApplicationService.CheckConfig:input_type -> desktop.ConfigContent
9, // 31: desktop.ApplicationService.FormatConfig:input_type -> desktop.ConfigContent
31, // 32: desktop.ApplicationService.GenerateConfigSchema:input_type -> google.protobuf.Empty
10, // 33: desktop.ApplicationService.EncodeProfile:input_type -> desktop.ProfileContent
11, // 34: desktop.ApplicationService.DecodeProfile:input_type -> desktop.ProfileData
3, // 35: desktop.ApplicationService.ArchiveReport:input_type -> desktop.ArchiveReportRequest
4, // 36: desktop.ApplicationService.StartStandaloneNetworkQualityTest:input_type -> desktop.StandaloneNetworkQualityTestRequest
5, // 37: desktop.ApplicationService.StartStandaloneSTUNTest:input_type -> desktop.StandaloneSTUNTestRequest
6, // 38: desktop.DesktopService.GetDaemonInfo:output_type -> desktop.DaemonInfo
31, // 39: desktop.DesktopService.ClaimService:output_type -> google.protobuf.Empty
31, // 40: desktop.DesktopService.TakeOverService:output_type -> google.protobuf.Empty
31, // 41: desktop.DesktopService.StartService:output_type -> google.protobuf.Empty
12, // 42: desktop.DesktopService.GetWorkingDirectory:output_type -> desktop.WorkingDirectoryInfo
31, // 43: desktop.DesktopService.DestroyWorkingDirectory:output_type -> google.protobuf.Empty
13, // 44: desktop.DesktopService.ListCrashReports:output_type -> desktop.CrashReportList
17, // 45: desktop.DesktopService.ReadCrashReport:output_type -> desktop.CrashReportContent
31, // 46: desktop.DesktopService.MarkCrashReportRead:output_type -> google.protobuf.Empty
19, // 47: desktop.DesktopService.ExportCrashReport:output_type -> desktop.CrashReportArchive
31, // 48: desktop.DesktopService.DeleteCrashReport:output_type -> google.protobuf.Empty
31, // 49: desktop.DesktopService.DeleteAllCrashReports:output_type -> google.protobuf.Empty
20, // 50: desktop.DesktopService.ListOOMReports:output_type -> desktop.OOMReportList
24, // 51: desktop.DesktopService.ReadOOMReport:output_type -> desktop.OOMReportContent
31, // 52: desktop.DesktopService.MarkOOMReportRead:output_type -> google.protobuf.Empty
19, // 53: desktop.DesktopService.ExportOOMReport:output_type -> desktop.CrashReportArchive
31, // 54: desktop.DesktopService.DeleteOOMReport:output_type -> google.protobuf.Empty
31, // 55: desktop.DesktopService.DeleteAllOOMReports:output_type -> google.protobuf.Empty
30, // 56: desktop.DesktopService.InstallUpdate:output_type -> desktop.InstallUpdateResponse
26, // 57: desktop.DesktopService.GetSecuritySettings:output_type -> desktop.SecuritySettings
31, // 58: desktop.DesktopService.SetInsecureModeEnabled:output_type -> google.protobuf.Empty
31, // 59: desktop.DesktopService.SetLocale:output_type -> google.protobuf.Empty
31, // 60: desktop.ApplicationService.CheckConfig:output_type -> google.protobuf.Empty
9, // 61: desktop.ApplicationService.FormatConfig:output_type -> desktop.ConfigContent
9, // 62: desktop.ApplicationService.GenerateConfigSchema:output_type -> desktop.ConfigContent
11, // 63: desktop.ApplicationService.EncodeProfile:output_type -> desktop.ProfileData
10, // 64: desktop.ApplicationService.DecodeProfile:output_type -> desktop.ProfileContent
31, // 65: desktop.ApplicationService.ArchiveReport:output_type -> google.protobuf.Empty
32, // 66: desktop.ApplicationService.StartStandaloneNetworkQualityTest:output_type -> daemon.NetworkQualityTestProgress
33, // 67: desktop.ApplicationService.StartStandaloneSTUNTest:output_type -> daemon.STUNTestProgress
38, // [38:68] is the sub-list for method output_type
8, // [8:38] is the sub-list for method input_type
31, // 26: desktop.DesktopService.ListPowerReports:input_type -> google.protobuf.Empty
22, // 27: desktop.DesktopService.ReadPowerReport:input_type -> desktop.OOMReportRequest
22, // 28: desktop.DesktopService.MarkPowerReportRead:input_type -> desktop.OOMReportRequest
23, // 29: desktop.DesktopService.ExportPowerReport:input_type -> desktop.OOMReportExportRequest
22, // 30: desktop.DesktopService.DeletePowerReport:input_type -> desktop.OOMReportRequest
31, // 31: desktop.DesktopService.DeleteAllPowerReports:input_type -> google.protobuf.Empty
29, // 32: desktop.DesktopService.InstallUpdate:input_type -> desktop.InstallUpdateRequest
31, // 33: desktop.DesktopService.GetSecuritySettings:input_type -> google.protobuf.Empty
27, // 34: desktop.DesktopService.SetInsecureModeEnabled:input_type -> desktop.SetInsecureModeEnabledRequest
28, // 35: desktop.DesktopService.SetLocale:input_type -> desktop.SetLocaleRequest
9, // 36: desktop.ApplicationService.CheckConfig:input_type -> desktop.ConfigContent
9, // 37: desktop.ApplicationService.FormatConfig:input_type -> desktop.ConfigContent
31, // 38: desktop.ApplicationService.GenerateConfigSchema:input_type -> google.protobuf.Empty
10, // 39: desktop.ApplicationService.EncodeProfile:input_type -> desktop.ProfileContent
11, // 40: desktop.ApplicationService.DecodeProfile:input_type -> desktop.ProfileData
3, // 41: desktop.ApplicationService.ArchiveReport:input_type -> desktop.ArchiveReportRequest
4, // 42: desktop.ApplicationService.StartStandaloneNetworkQualityTest:input_type -> desktop.StandaloneNetworkQualityTestRequest
5, // 43: desktop.ApplicationService.StartStandaloneSTUNTest:input_type -> desktop.StandaloneSTUNTestRequest
6, // 44: desktop.DesktopService.GetDaemonInfo:output_type -> desktop.DaemonInfo
31, // 45: desktop.DesktopService.ClaimService:output_type -> google.protobuf.Empty
31, // 46: desktop.DesktopService.TakeOverService:output_type -> google.protobuf.Empty
31, // 47: desktop.DesktopService.StartService:output_type -> google.protobuf.Empty
12, // 48: desktop.DesktopService.GetWorkingDirectory:output_type -> desktop.WorkingDirectoryInfo
31, // 49: desktop.DesktopService.DestroyWorkingDirectory:output_type -> google.protobuf.Empty
13, // 50: desktop.DesktopService.ListCrashReports:output_type -> desktop.CrashReportList
17, // 51: desktop.DesktopService.ReadCrashReport:output_type -> desktop.CrashReportContent
31, // 52: desktop.DesktopService.MarkCrashReportRead:output_type -> google.protobuf.Empty
19, // 53: desktop.DesktopService.ExportCrashReport:output_type -> desktop.CrashReportArchive
31, // 54: desktop.DesktopService.DeleteCrashReport:output_type -> google.protobuf.Empty
31, // 55: desktop.DesktopService.DeleteAllCrashReports:output_type -> google.protobuf.Empty
20, // 56: desktop.DesktopService.ListOOMReports:output_type -> desktop.OOMReportList
24, // 57: desktop.DesktopService.ReadOOMReport:output_type -> desktop.OOMReportContent
31, // 58: desktop.DesktopService.MarkOOMReportRead:output_type -> google.protobuf.Empty
19, // 59: desktop.DesktopService.ExportOOMReport:output_type -> desktop.CrashReportArchive
31, // 60: desktop.DesktopService.DeleteOOMReport:output_type -> google.protobuf.Empty
31, // 61: desktop.DesktopService.DeleteAllOOMReports:output_type -> google.protobuf.Empty
20, // 62: desktop.DesktopService.ListPowerReports:output_type -> desktop.OOMReportList
24, // 63: desktop.DesktopService.ReadPowerReport:output_type -> desktop.OOMReportContent
31, // 64: desktop.DesktopService.MarkPowerReportRead:output_type -> google.protobuf.Empty
19, // 65: desktop.DesktopService.ExportPowerReport:output_type -> desktop.CrashReportArchive
31, // 66: desktop.DesktopService.DeletePowerReport:output_type -> google.protobuf.Empty
31, // 67: desktop.DesktopService.DeleteAllPowerReports:output_type -> google.protobuf.Empty
30, // 68: desktop.DesktopService.InstallUpdate:output_type -> desktop.InstallUpdateResponse
26, // 69: desktop.DesktopService.GetSecuritySettings:output_type -> desktop.SecuritySettings
31, // 70: desktop.DesktopService.SetInsecureModeEnabled:output_type -> google.protobuf.Empty
31, // 71: desktop.DesktopService.SetLocale:output_type -> google.protobuf.Empty
31, // 72: desktop.ApplicationService.CheckConfig:output_type -> google.protobuf.Empty
9, // 73: desktop.ApplicationService.FormatConfig:output_type -> desktop.ConfigContent
9, // 74: desktop.ApplicationService.GenerateConfigSchema:output_type -> desktop.ConfigContent
11, // 75: desktop.ApplicationService.EncodeProfile:output_type -> desktop.ProfileData
10, // 76: desktop.ApplicationService.DecodeProfile:output_type -> desktop.ProfileContent
31, // 77: desktop.ApplicationService.ArchiveReport:output_type -> google.protobuf.Empty
32, // 78: desktop.ApplicationService.StartStandaloneNetworkQualityTest:output_type -> daemon.NetworkQualityTestProgress
33, // 79: desktop.ApplicationService.StartStandaloneSTUNTest:output_type -> daemon.STUNTestProgress
44, // [44:80] is the sub-list for method output_type
8, // [8:44] is the sub-list for method input_type
8, // [8:8] is the sub-list for extension type_name
8, // [8:8] is the sub-list for extension extendee
0, // [0:8] is the sub-list for field type_name
+7
View File
@@ -25,6 +25,12 @@ service DesktopService {
rpc ExportOOMReport(OOMReportExportRequest) returns (CrashReportArchive) {}
rpc DeleteOOMReport(OOMReportRequest) returns (google.protobuf.Empty) {}
rpc DeleteAllOOMReports(google.protobuf.Empty) returns (google.protobuf.Empty) {}
rpc ListPowerReports(google.protobuf.Empty) returns (OOMReportList) {}
rpc ReadPowerReport(OOMReportRequest) returns (OOMReportContent) {}
rpc MarkPowerReportRead(OOMReportRequest) returns (google.protobuf.Empty) {}
rpc ExportPowerReport(OOMReportExportRequest) returns (CrashReportArchive) {}
rpc DeletePowerReport(OOMReportRequest) returns (google.protobuf.Empty) {}
rpc DeleteAllPowerReports(google.protobuf.Empty) returns (google.protobuf.Empty) {}
rpc InstallUpdate(InstallUpdateRequest) returns (InstallUpdateResponse) {}
rpc GetSecuritySettings(google.protobuf.Empty) returns (SecuritySettings) {}
rpc SetInsecureModeEnabled(SetInsecureModeEnabledRequest) returns (google.protobuf.Empty) {}
@@ -80,6 +86,7 @@ message StartOptions {
bool oom_killer_enabled = 1;
bool oom_killer_disabled = 2;
int64 oom_memory_limit = 3;
bool power_report_enabled = 4;
}
message ConfigContent {
@@ -35,6 +35,12 @@ const (
DesktopService_ExportOOMReport_FullMethodName = "/desktop.DesktopService/ExportOOMReport"
DesktopService_DeleteOOMReport_FullMethodName = "/desktop.DesktopService/DeleteOOMReport"
DesktopService_DeleteAllOOMReports_FullMethodName = "/desktop.DesktopService/DeleteAllOOMReports"
DesktopService_ListPowerReports_FullMethodName = "/desktop.DesktopService/ListPowerReports"
DesktopService_ReadPowerReport_FullMethodName = "/desktop.DesktopService/ReadPowerReport"
DesktopService_MarkPowerReportRead_FullMethodName = "/desktop.DesktopService/MarkPowerReportRead"
DesktopService_ExportPowerReport_FullMethodName = "/desktop.DesktopService/ExportPowerReport"
DesktopService_DeletePowerReport_FullMethodName = "/desktop.DesktopService/DeletePowerReport"
DesktopService_DeleteAllPowerReports_FullMethodName = "/desktop.DesktopService/DeleteAllPowerReports"
DesktopService_InstallUpdate_FullMethodName = "/desktop.DesktopService/InstallUpdate"
DesktopService_GetSecuritySettings_FullMethodName = "/desktop.DesktopService/GetSecuritySettings"
DesktopService_SetInsecureModeEnabled_FullMethodName = "/desktop.DesktopService/SetInsecureModeEnabled"
@@ -63,6 +69,12 @@ type DesktopServiceClient interface {
ExportOOMReport(ctx context.Context, in *OOMReportExportRequest, opts ...grpc.CallOption) (*CrashReportArchive, error)
DeleteOOMReport(ctx context.Context, in *OOMReportRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
DeleteAllOOMReports(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
ListPowerReports(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*OOMReportList, error)
ReadPowerReport(ctx context.Context, in *OOMReportRequest, opts ...grpc.CallOption) (*OOMReportContent, error)
MarkPowerReportRead(ctx context.Context, in *OOMReportRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
ExportPowerReport(ctx context.Context, in *OOMReportExportRequest, opts ...grpc.CallOption) (*CrashReportArchive, error)
DeletePowerReport(ctx context.Context, in *OOMReportRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
DeleteAllPowerReports(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
InstallUpdate(ctx context.Context, in *InstallUpdateRequest, opts ...grpc.CallOption) (*InstallUpdateResponse, error)
GetSecuritySettings(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SecuritySettings, error)
SetInsecureModeEnabled(ctx context.Context, in *SetInsecureModeEnabledRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
@@ -257,6 +269,66 @@ func (c *desktopServiceClient) DeleteAllOOMReports(ctx context.Context, in *empt
return out, nil
}
func (c *desktopServiceClient) ListPowerReports(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*OOMReportList, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(OOMReportList)
err := c.cc.Invoke(ctx, DesktopService_ListPowerReports_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *desktopServiceClient) ReadPowerReport(ctx context.Context, in *OOMReportRequest, opts ...grpc.CallOption) (*OOMReportContent, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(OOMReportContent)
err := c.cc.Invoke(ctx, DesktopService_ReadPowerReport_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *desktopServiceClient) MarkPowerReportRead(ctx context.Context, in *OOMReportRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, DesktopService_MarkPowerReportRead_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *desktopServiceClient) ExportPowerReport(ctx context.Context, in *OOMReportExportRequest, opts ...grpc.CallOption) (*CrashReportArchive, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CrashReportArchive)
err := c.cc.Invoke(ctx, DesktopService_ExportPowerReport_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *desktopServiceClient) DeletePowerReport(ctx context.Context, in *OOMReportRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, DesktopService_DeletePowerReport_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *desktopServiceClient) DeleteAllPowerReports(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, DesktopService_DeleteAllPowerReports_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *desktopServiceClient) InstallUpdate(ctx context.Context, in *InstallUpdateRequest, opts ...grpc.CallOption) (*InstallUpdateResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(InstallUpdateResponse)
@@ -319,6 +391,12 @@ type DesktopServiceServer interface {
ExportOOMReport(context.Context, *OOMReportExportRequest) (*CrashReportArchive, error)
DeleteOOMReport(context.Context, *OOMReportRequest) (*emptypb.Empty, error)
DeleteAllOOMReports(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
ListPowerReports(context.Context, *emptypb.Empty) (*OOMReportList, error)
ReadPowerReport(context.Context, *OOMReportRequest) (*OOMReportContent, error)
MarkPowerReportRead(context.Context, *OOMReportRequest) (*emptypb.Empty, error)
ExportPowerReport(context.Context, *OOMReportExportRequest) (*CrashReportArchive, error)
DeletePowerReport(context.Context, *OOMReportRequest) (*emptypb.Empty, error)
DeleteAllPowerReports(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
InstallUpdate(context.Context, *InstallUpdateRequest) (*InstallUpdateResponse, error)
GetSecuritySettings(context.Context, *emptypb.Empty) (*SecuritySettings, error)
SetInsecureModeEnabled(context.Context, *SetInsecureModeEnabledRequest) (*emptypb.Empty, error)
@@ -405,6 +483,30 @@ func (UnimplementedDesktopServiceServer) DeleteAllOOMReports(context.Context, *e
return nil, status.Error(codes.Unimplemented, "method DeleteAllOOMReports not implemented")
}
func (UnimplementedDesktopServiceServer) ListPowerReports(context.Context, *emptypb.Empty) (*OOMReportList, error) {
return nil, status.Error(codes.Unimplemented, "method ListPowerReports not implemented")
}
func (UnimplementedDesktopServiceServer) ReadPowerReport(context.Context, *OOMReportRequest) (*OOMReportContent, error) {
return nil, status.Error(codes.Unimplemented, "method ReadPowerReport not implemented")
}
func (UnimplementedDesktopServiceServer) MarkPowerReportRead(context.Context, *OOMReportRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method MarkPowerReportRead not implemented")
}
func (UnimplementedDesktopServiceServer) ExportPowerReport(context.Context, *OOMReportExportRequest) (*CrashReportArchive, error) {
return nil, status.Error(codes.Unimplemented, "method ExportPowerReport not implemented")
}
func (UnimplementedDesktopServiceServer) DeletePowerReport(context.Context, *OOMReportRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method DeletePowerReport not implemented")
}
func (UnimplementedDesktopServiceServer) DeleteAllPowerReports(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method DeleteAllPowerReports not implemented")
}
func (UnimplementedDesktopServiceServer) InstallUpdate(context.Context, *InstallUpdateRequest) (*InstallUpdateResponse, error) {
return nil, status.Error(codes.Unimplemented, "method InstallUpdate not implemented")
}
@@ -765,6 +867,114 @@ func _DesktopService_DeleteAllOOMReports_Handler(srv interface{}, ctx context.Co
return interceptor(ctx, in, info, handler)
}
func _DesktopService_ListPowerReports_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DesktopServiceServer).ListPowerReports(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: DesktopService_ListPowerReports_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DesktopServiceServer).ListPowerReports(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _DesktopService_ReadPowerReport_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(OOMReportRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DesktopServiceServer).ReadPowerReport(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: DesktopService_ReadPowerReport_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DesktopServiceServer).ReadPowerReport(ctx, req.(*OOMReportRequest))
}
return interceptor(ctx, in, info, handler)
}
func _DesktopService_MarkPowerReportRead_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(OOMReportRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DesktopServiceServer).MarkPowerReportRead(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: DesktopService_MarkPowerReportRead_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DesktopServiceServer).MarkPowerReportRead(ctx, req.(*OOMReportRequest))
}
return interceptor(ctx, in, info, handler)
}
func _DesktopService_ExportPowerReport_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(OOMReportExportRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DesktopServiceServer).ExportPowerReport(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: DesktopService_ExportPowerReport_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DesktopServiceServer).ExportPowerReport(ctx, req.(*OOMReportExportRequest))
}
return interceptor(ctx, in, info, handler)
}
func _DesktopService_DeletePowerReport_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(OOMReportRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DesktopServiceServer).DeletePowerReport(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: DesktopService_DeletePowerReport_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DesktopServiceServer).DeletePowerReport(ctx, req.(*OOMReportRequest))
}
return interceptor(ctx, in, info, handler)
}
func _DesktopService_DeleteAllPowerReports_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DesktopServiceServer).DeleteAllPowerReports(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: DesktopService_DeleteAllPowerReports_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DesktopServiceServer).DeleteAllPowerReports(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _DesktopService_InstallUpdate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(InstallUpdateRequest)
if err := dec(in); err != nil {
@@ -916,6 +1126,30 @@ var DesktopService_ServiceDesc = grpc.ServiceDesc{
MethodName: "DeleteAllOOMReports",
Handler: _DesktopService_DeleteAllOOMReports_Handler,
},
{
MethodName: "ListPowerReports",
Handler: _DesktopService_ListPowerReports_Handler,
},
{
MethodName: "ReadPowerReport",
Handler: _DesktopService_ReadPowerReport_Handler,
},
{
MethodName: "MarkPowerReportRead",
Handler: _DesktopService_MarkPowerReportRead_Handler,
},
{
MethodName: "ExportPowerReport",
Handler: _DesktopService_ExportPowerReport_Handler,
},
{
MethodName: "DeletePowerReport",
Handler: _DesktopService_DeletePowerReport_Handler,
},
{
MethodName: "DeleteAllPowerReports",
Handler: _DesktopService_DeleteAllPowerReports_Handler,
},
{
MethodName: "InstallUpdate",
Handler: _DesktopService_InstallUpdate_Handler,
+184
View File
@@ -0,0 +1,184 @@
package main
import (
"context"
"os"
"path/filepath"
"slices"
"sort"
"strings"
"google.golang.org/protobuf/types/known/emptypb"
)
const (
powerReportsDirectoryName = "power_reports"
powerTimelineFileName = "timeline.jsonl"
powerEventsFileName = "events.jsonl"
powerReportFileContentLimit = 1024 * 1024
)
// File order follows the client convention:
// sing-box-for-apple Library/Shared/PowerReportManager.swift (availableFiles).
var powerReportLeadingFileOrder = []string{metadataFileName, configSnapshotFileName, powerTimelineFileName, powerEventsFileName, goLogFileName}
func (s *desktopService) ListPowerReports(ctx context.Context, empty *emptypb.Empty) (*OOMReportList, error) {
reportsDirectory, userID, err := s.daemon.reportCaller(ctx, powerReportsDirectoryName)
if err != nil {
return nil, err
}
entries, err := os.ReadDir(reportsDirectory)
if err != nil {
if os.IsNotExist(err) {
return &OOMReportList{}, nil
}
return nil, err
}
reports := make([]*OOMReportEntry, 0, len(entries))
for _, entry := range entries {
if !entry.IsDir() {
continue
}
fullPath := filepath.Join(reportsDirectory, entry.Name())
if !reportOwnedBy(fullPath, userID) {
continue
}
reports = append(reports, &OOMReportEntry{
Name: entry.Name(),
RecordedAt: reportTime(fullPath, "startedAt").UnixMilli(),
IsRead: reportIsRead(fullPath),
})
}
sort.Slice(reports, func(i, j int) bool {
return reports[i].RecordedAt > reports[j].RecordedAt
})
return &OOMReportList{Reports: reports}, nil
}
func (s *desktopService) ReadPowerReport(ctx context.Context, request *OOMReportRequest) (*OOMReportContent, error) {
reportsDirectory, userID, err := s.daemon.reportCaller(ctx, powerReportsDirectoryName)
if err != nil {
return nil, err
}
fullPath, err := reportPathForUser(reportsDirectory, request.Name, userID)
if err != nil {
return nil, err
}
files := make([]*OOMReportFile, 0, len(powerReportLeadingFileOrder))
for _, fileName := range powerReportLeadingFileOrder {
content, readError := readFileTail(filepath.Join(fullPath, fileName), powerReportFileContentLimit)
if readError != nil {
if os.IsNotExist(readError) {
continue
}
return nil, readError
}
files = append(files, &OOMReportFile{
Name: fileName,
Content: content,
})
}
entries, err := os.ReadDir(fullPath)
if err != nil {
return nil, err
}
profileNames := make([]string, 0, len(entries))
for _, entry := range entries {
name := entry.Name()
if entry.IsDir() || strings.HasPrefix(name, ".") {
continue
}
if slices.Contains(powerReportLeadingFileOrder, name) {
continue
}
profileNames = append(profileNames, name)
}
sort.Strings(profileNames)
for _, name := range profileNames {
files = append(files, &OOMReportFile{
Name: name,
IsProfile: true,
})
}
return &OOMReportContent{Files: files}, nil
}
func readFileTail(path string, limit int64) ([]byte, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
info, err := file.Stat()
if err != nil {
return nil, err
}
if info.Size() <= limit {
return os.ReadFile(path)
}
_, err = file.Seek(info.Size()-limit, 0)
if err != nil {
return nil, err
}
content := make([]byte, 0, limit+4)
content = append(content, "…\n"...)
buffer := make([]byte, limit)
n, err := file.Read(buffer)
if err != nil {
return nil, err
}
return append(content, buffer[:n]...), nil
}
func (s *desktopService) MarkPowerReportRead(ctx context.Context, request *OOMReportRequest) (*emptypb.Empty, error) {
reportsDirectory, userID, err := s.daemon.reportCaller(ctx, powerReportsDirectoryName)
if err != nil {
return nil, err
}
fullPath, err := reportPathForUser(reportsDirectory, request.Name, userID)
if err != nil {
return nil, err
}
err = os.WriteFile(filepath.Join(fullPath, readMarkerFileName), nil, 0o600)
if err != nil {
return nil, err
}
return &emptypb.Empty{}, nil
}
func (s *desktopService) ExportPowerReport(ctx context.Context, request *OOMReportExportRequest) (*CrashReportArchive, error) {
reportsDirectory, userID, err := s.daemon.reportCaller(ctx, powerReportsDirectoryName)
if err != nil {
return nil, err
}
return exportReportArchive(reportsDirectory, request.Name, userID, request.WithConfiguration, request.WithLog, request.Encrypt)
}
func (s *desktopService) DeletePowerReport(ctx context.Context, request *OOMReportRequest) (*emptypb.Empty, error) {
reportsDirectory, userID, err := s.daemon.reportCaller(ctx, powerReportsDirectoryName)
if err != nil {
return nil, err
}
fullPath, err := reportPathForUser(reportsDirectory, request.Name, userID)
if err != nil {
return nil, err
}
err = os.RemoveAll(fullPath)
if err != nil {
return nil, err
}
return &emptypb.Empty{}, nil
}
func (s *desktopService) DeleteAllPowerReports(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {
reportsDirectory, userID, err := s.daemon.reportCaller(ctx, powerReportsDirectoryName)
if err != nil {
return nil, err
}
err = deleteReportsForUser(reportsDirectory, userID)
if err != nil {
return nil, err
}
return &emptypb.Empty{}, nil
}
+23 -3
View File
@@ -15,6 +15,7 @@ import (
"github.com/sagernet/sing-box/include"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/service/oomkiller"
"github.com/sagernet/sing-box/service/powerreport"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/service"
@@ -30,6 +31,7 @@ type Daemon struct {
ctx context.Context
logger log.ContextLogger
startedService *daemon.StartedService
powerManager *powerreport.Manager
server *grpc.Server
runtimeWorkingDirectory string
lifecycleAccess sync.Mutex
@@ -62,6 +64,8 @@ func newDaemon() (*Daemon, error) {
})
reporter := libbox.NewOOMReporter(d.startedService)
service.MustRegister[oomkiller.OOMReporter](ctx, reporter)
d.powerManager = powerreport.NewManager()
service.MustRegister[*powerreport.Manager](ctx, d.powerManager)
managedService := daemon.NewManagedService(daemon.ManagedServiceOptions{
Handler: &managedHandler{d},
Debug: debugEnabled,
@@ -195,6 +199,7 @@ func (d *Daemon) configureWorkingDirectoryLocked(directory string) error {
return err
}
libbox.PromoteOOMDraft()
libbox.PromotePowerReportDraft()
d.runtimeWorkingDirectory = directory
return nil
}
@@ -207,11 +212,20 @@ func (d *Daemon) startServiceLocked(ctx context.Context, ownerUserID string, con
}
_ = os.WriteFile(filepath.Join(directory, configSnapshotFileName), []byte(configContent), 0o600)
libbox.ReloadSetupOptions(&libbox.SetupOptions{
OomKillerEnabled: options.OOMKillerEnabled,
OomKillerDisabled: options.OOMKillerDisabled,
OomMemoryLimit: options.OOMMemoryLimit,
OomKillerEnabled: options.OOMKillerEnabled,
OomKillerDisabled: options.OOMKillerDisabled,
OomMemoryLimit: options.OOMMemoryLimit,
PowerReportEnabled: options.PowerReportEnabled,
})
d.startedService.SetOOMKillerOptions(options.OOMKillerEnabled, options.OOMKillerDisabled, uint64(options.OOMMemoryLimit))
if options.PowerReportEnabled {
err = d.powerManager.Start(libbox.PowerReportOptions(d.startedService))
if err != nil {
d.logger.Warn("start power report recorder: ", err)
}
} else {
d.powerManager.Close()
}
if d.platform != nil {
d.platform.SetSystemProxyPreference(options.systemProxyEnabled())
err = d.platform.ResetPlatformOptions()
@@ -243,6 +257,8 @@ func (d *Daemon) stopServiceLocked(ownerUserID string) error {
return err
}
}
d.powerManager.Close()
libbox.PromotePowerReportDraft()
directory := userWorkingDirectory(ownerUserID)
crashReportError := tagUnownedReports(filepath.Join(directory, crashReportsDirectoryName), ownerUserID)
if crashReportError != nil {
@@ -252,6 +268,10 @@ func (d *Daemon) stopServiceLocked(ownerUserID string) error {
if oomReportError != nil {
return oomReportError
}
powerReportError := tagUnownedReports(filepath.Join(directory, powerReportsDirectoryName), ownerUserID)
if powerReportError != nil {
return powerReportError
}
options.WasRunning = false
return saveStartOptions(ownerUserID, options)
}
+1
View File
@@ -22,6 +22,7 @@ type startOptions struct {
OOMKillerEnabled bool `json:"oom_killer_enabled"`
OOMKillerDisabled bool `json:"oom_killer_disabled"`
OOMMemoryLimit int64 `json:"oom_memory_limit"`
PowerReportEnabled bool `json:"power_report_enabled,omitempty"`
SystemProxyEnabled *bool `json:"system_proxy_enabled,omitempty"`
}
+26 -2
View File
@@ -15,6 +15,7 @@ import (
"github.com/sagernet/sing-box/daemon"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/service/oomkiller"
"github.com/sagernet/sing-box/service/powerreport"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/service"
@@ -34,6 +35,7 @@ type CommandServer struct {
handler CommandServerHandler
platformInterface PlatformInterface
platformWrapper *platformInterfaceWrapper
powerManager *powerreport.Manager
grpcServer *grpc.Server
listener net.Listener
endPauseTimer *time.Timer
@@ -51,9 +53,12 @@ type CommandServerHandler interface {
func NewCommandServer(handler CommandServerHandler, platformInterface PlatformInterface) (*CommandServer, error) {
ctx := baseContext(platformInterface)
powerManager := powerreport.NewManager()
service.MustRegister[*powerreport.Manager](ctx, powerManager)
platformWrapper := &platformInterfaceWrapper{
iif: platformInterface,
useProcFS: platformInterface.UseProcFS(),
iif: platformInterface,
useProcFS: platformInterface.UseProcFS(),
powerManager: powerManager,
}
service.MustRegister[adapter.PlatformInterface](ctx, platformWrapper)
server := &CommandServer{
@@ -61,6 +66,7 @@ func NewCommandServer(handler CommandServerHandler, platformInterface PlatformIn
handler: handler,
platformInterface: platformInterface,
platformWrapper: platformWrapper,
powerManager: powerManager,
}
server.StartedService = daemon.NewStartedService(daemon.ServiceOptions{
Context: ctx,
@@ -84,6 +90,12 @@ func NewCommandServer(handler CommandServerHandler, platformInterface PlatformIn
Debug: sDebug,
OOMReporter: reporter,
})
if sPowerReportEnabled {
err := powerManager.Start(PowerReportOptions(server.StartedService))
if err != nil {
log.StdLogger().Error(E.Cause(err, "start power report recorder"))
}
}
return server, nil
}
@@ -183,6 +195,7 @@ func (s *CommandServer) Close() {
}
common.Close(s.listener)
s.StartedService.Close()
s.powerManager.Close()
}
type OverrideOptions struct {
@@ -193,6 +206,9 @@ type OverrideOptions struct {
func (s *CommandServer) StartOrReloadService(configContent string, options *OverrideOptions) error {
saveConfigSnapshot(configContent)
if s.powerManager.Recorder() != nil {
copyConfigSnapshot(filepath.Join(sWorkingPath, powerreport.DraftDirectoryName))
}
err := s.StartedService.StartOrReloadService(s.ctx, configContent, &daemon.OverrideOptions{
AutoRedirect: options.AutoRedirect,
IncludePackage: iteratorToArray(options.IncludePackage),
@@ -233,6 +249,10 @@ func (s *CommandServer) NeedFindProcess() bool {
}
func (s *CommandServer) Pause() {
recorder := s.powerManager.Recorder()
if recorder != nil {
recorder.RecordPlatformEvent("ne-sleep")
}
instance := s.StartedService.Instance()
if instance == nil || instance.PauseManager() == nil {
return
@@ -248,6 +268,10 @@ func (s *CommandServer) Pause() {
}
func (s *CommandServer) Wake() {
recorder := s.powerManager.Recorder()
if recorder != nil {
recorder.RecordPlatformEvent("ne-wake")
}
instance := s.StartedService.Instance()
if instance == nil || instance.PauseManager() == nil {
return
+19
View File
@@ -1,6 +1,7 @@
package libbox
import (
"github.com/sagernet/sing-box/service/powerreport"
tun "github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
@@ -69,6 +70,24 @@ func (m *platformDefaultInterfaceMonitor) UpdateDefaultInterface(interfaceName s
}
func (m *platformDefaultInterfaceMonitor) updateDefaultInterface(interfaceName string, interfaceIndex32 int32, isExpensive bool, isConstrained bool) {
var recorder *powerreport.Recorder
if m.powerManager != nil {
recorder = m.powerManager.Recorder()
}
if recorder != nil {
networkType := interfaceName
if interfaceIndex32 == -1 {
networkType = "none"
} else {
if isExpensive {
networkType += ",expensive"
}
if isConstrained {
networkType += ",constrained"
}
}
recorder.UpdateNetworkType(networkType)
}
m.isExpensive = isExpensive
m.isConstrained = isConstrained
err := m.networkManager.UpdateInterfaces()
+11 -3
View File
@@ -302,16 +302,24 @@ func buildOOMConnection(connection *trafficcontrol.TrackerMetadata) oomConnectio
return info
}
func writeOOMLog(destPath string, entries []*log.Entry) {
func formatLogEntries(entries []*log.Entry) []byte {
if len(entries) == 0 {
return
return nil
}
var buffer bytes.Buffer
for _, entry := range entries {
writeWithoutColors(&buffer, entry.Message)
buffer.WriteByte('\n')
}
writeReportFile(destPath, "go.log", buffer.Bytes())
return buffer.Bytes()
}
func writeOOMLog(destPath string, entries []*log.Entry) {
content := formatLogEntries(entries)
if content == nil {
return
}
writeReportFile(destPath, "go.log", content)
}
func writeWithoutColors(buffer *bytes.Buffer, message string) {
+35
View File
@@ -0,0 +1,35 @@
//go:build darwin || linux || windows
package libbox
import (
"time"
"github.com/sagernet/sing-box/daemon"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/service/powerreport"
)
type powerReportMetadata struct {
reportMetadata
StartedAt string `json:"startedAt"`
}
func PowerReportOptions(startedService *daemon.StartedService) powerreport.Options {
return powerreport.Options{
BasePath: sWorkingPath,
Logger: log.StdLogger(),
Metadata: powerReportMetadata{
reportMetadata: baseReportMetadata(),
StartedAt: time.Now().UTC().Format(time.RFC3339),
},
OwnerCallback: chownReport,
LogCallback: func() []byte {
return formatLogEntries(startedService.SavedLog())
},
}
}
func PromotePowerReportDraft() {
powerreport.PromoteDraft(sWorkingPath)
}
+2
View File
@@ -16,6 +16,7 @@ import (
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/libbox/internal/procfs"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/service/powerreport"
tun "github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/control"
@@ -29,6 +30,7 @@ type platformInterfaceWrapper struct {
iif PlatformInterface
useProcFS bool
networkManager adapter.NetworkManager
powerManager *powerreport.Manager
myTunName string
myTunAddress []netip.Addr
defaultInterfaceAccess sync.Mutex
+3
View File
@@ -34,6 +34,7 @@ var (
sOOMKillerEnabled bool
sOOMKillerDisabled bool
sOOMMemoryLimit int64
sPowerReportEnabled bool
)
func init() {
@@ -54,6 +55,7 @@ type SetupOptions struct {
OomKillerEnabled bool
OomKillerDisabled bool
OomMemoryLimit int64
PowerReportEnabled bool
}
func applySetupOptions(options *SetupOptions) {
@@ -80,6 +82,7 @@ func ReloadSetupOptions(options *SetupOptions) {
sOOMKillerEnabled = options.OomKillerEnabled
sOOMKillerDisabled = options.OomKillerDisabled
sOOMMemoryLimit = options.OomMemoryLimit
sPowerReportEnabled = options.PowerReportEnabled
if sOOMKillerEnabled {
if sOOMMemoryLimit == 0 && C.IsIos {
sOOMMemoryLimit = oomkiller.DefaultAppleNetworkExtensionMemoryLimit
+4 -4
View File
@@ -44,13 +44,13 @@ require (
github.com/sagernet/gvisor v0.0.0-20260727.0-sing-box-mod.1
github.com/sagernet/netlink v0.0.0-20260814022025-64455d367bbf
github.com/sagernet/nftables v0.3.0-mod.4
github.com/sagernet/quic-go v0.61.0-sing-box-mod.4
github.com/sagernet/sing v0.9.0-beta.3
github.com/sagernet/quic-go v0.61.0-sing-box-mod.5
github.com/sagernet/sing v0.9.0-beta.4
github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3
github.com/sagernet/sing-mux v0.3.5
github.com/sagernet/sing-openconnect v0.0.0-20260810065514-53aa8058f8df
github.com/sagernet/sing-openvpn v0.0.0-20260729104525-103eb5fe5eb6
github.com/sagernet/sing-quic v0.7.0-beta.2
github.com/sagernet/sing-quic v0.7.0-beta.3
github.com/sagernet/sing-shadowsocks v0.2.8
github.com/sagernet/sing-shadowsocks2 v0.2.1
github.com/sagernet/sing-shadowtls v0.2.1
@@ -60,7 +60,7 @@ require (
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1
github.com/sagernet/smux v1.5.50-sing-box-mod.1
github.com/sagernet/tailscale v1.102.1-sing-box-1.14-mod.3
github.com/sagernet/wireguard-go v0.0.5-0.20260810121456-c6c8a831ef70
github.com/sagernet/wireguard-go v0.0.5-0.20260823125007-8bd032a91a30
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.12.0
+8 -8
View File
@@ -316,10 +316,10 @@ github.com/sagernet/netlink v0.0.0-20260814022025-64455d367bbf h1:b/3rm+KvxzuYRc
github.com/sagernet/netlink v0.0.0-20260814022025-64455d367bbf/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
github.com/sagernet/nftables v0.3.0-mod.4 h1:vnOtcDYeSXv2e5RoRuGH0lrpttQFJ8iC4ICS2nhlDSo=
github.com/sagernet/nftables v0.3.0-mod.4/go.mod h1:8kslHG4VvYNihcco+i6uxIX7qbT8A56T0y5q7U44ZaQ=
github.com/sagernet/quic-go v0.61.0-sing-box-mod.4 h1:XoFf8KoBEsamWo3ePlcmS/xs8B7KDurJQfI2U7q/H1c=
github.com/sagernet/quic-go v0.61.0-sing-box-mod.4/go.mod h1:hmLC8GJPp+BrpBgrgJnacvI4fZGIhasuy1ROJJ8GR4E=
github.com/sagernet/sing v0.9.0-beta.3 h1:DH9B94S82daJyJHz2oeirLwa/zh03AJ3xiXLJrM95Fo=
github.com/sagernet/sing v0.9.0-beta.3/go.mod h1:K3Owt3xPhHugvlnlPPxZJ/exXdaJfEPOTNorGk4AXjo=
github.com/sagernet/quic-go v0.61.0-sing-box-mod.5 h1:D41e0fUHgdiYV2r4eGF3LgnBWNjYHm781dNW87IDzWc=
github.com/sagernet/quic-go v0.61.0-sing-box-mod.5/go.mod h1:hmLC8GJPp+BrpBgrgJnacvI4fZGIhasuy1ROJJ8GR4E=
github.com/sagernet/sing v0.9.0-beta.4 h1:DYoEzb3FeCZ9bA66hJmL0NFDXckZpa4oYF/VpbFJEkE=
github.com/sagernet/sing v0.9.0-beta.4/go.mod h1:K3Owt3xPhHugvlnlPPxZJ/exXdaJfEPOTNorGk4AXjo=
github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 h1:3y6++yIa8XlDhxPkpR4p+7RUHVY2KTP9CPIGnWmOlO8=
github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3/go.mod h1:XEqEDYRCAYLaoPjZ1ifVWJg5iWAJHL2gOAXe/PM28Cg=
github.com/sagernet/sing-mux v0.3.5 h1:RHnhVEc+SFqkrK4xMygYjDwwLhzp2Bj3lztSukONfhI=
@@ -328,8 +328,8 @@ github.com/sagernet/sing-openconnect v0.0.0-20260810065514-53aa8058f8df h1:ogyC7
github.com/sagernet/sing-openconnect v0.0.0-20260810065514-53aa8058f8df/go.mod h1:4AKZLVcvY3r54UaK2Gbnm7aN8pOwdLz+y4EP0QFZ5Eg=
github.com/sagernet/sing-openvpn v0.0.0-20260729104525-103eb5fe5eb6 h1:ZAvhkor0prJ8KEX9EmTEH+gJ1WlA3ffjS8GZT6KKq9c=
github.com/sagernet/sing-openvpn v0.0.0-20260729104525-103eb5fe5eb6/go.mod h1:PWX7WygD8jpwfqfaGNySXpJYTn0SOwjBI1BKHHC2+Bw=
github.com/sagernet/sing-quic v0.7.0-beta.2 h1:qfMz4IyGIj6WhqhrR5ZU+d4Cqppi6RM6BpGXG6phfy8=
github.com/sagernet/sing-quic v0.7.0-beta.2/go.mod h1:Z+ZpYBLJ7WkPPrGSCYFxna4Dos2XrywEy6YH6SY5L6Q=
github.com/sagernet/sing-quic v0.7.0-beta.3 h1:jQA8vurQ2c8uKebdSq5G1N4EnSLpq2l6rEGJ7Dv6hXA=
github.com/sagernet/sing-quic v0.7.0-beta.3/go.mod h1:FPgVlDoSLWJlCjtT8WrFIAll4t2OutSP9nq0Iq74JLg=
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=
github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo=
@@ -348,8 +348,8 @@ github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1h
github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8=
github.com/sagernet/tailscale v1.102.1-sing-box-1.14-mod.3 h1:c7jWyEt7n3WdbDxkc7EqmPQtPT+Hct2l1CtmEDztMuo=
github.com/sagernet/tailscale v1.102.1-sing-box-1.14-mod.3/go.mod h1:WLUSOPmTcf7VN9gLCe01qUSIvD+/cKC177neENyZPkI=
github.com/sagernet/wireguard-go v0.0.5-0.20260810121456-c6c8a831ef70 h1:WTVgbkDDGnZqxIUnBsE9QyJz2dtSaeoh9mFwxmQqfgI=
github.com/sagernet/wireguard-go v0.0.5-0.20260810121456-c6c8a831ef70/go.mod h1:er10sELpmzLXq7S7Pbc1Zsbyapcr+/gxNAHKTo6fzVA=
github.com/sagernet/wireguard-go v0.0.5-0.20260823125007-8bd032a91a30 h1:Z9QAr893OvR9qDHvxYTsYZhpvqwcc0LWbJ4ECg/eBgg=
github.com/sagernet/wireguard-go v0.0.5-0.20260823125007-8bd032a91a30/go.mod h1:er10sELpmzLXq7S7Pbc1Zsbyapcr+/gxNAHKTo6fzVA=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA=
github.com/smallstep/pkcs7 v0.1.1 h1:x+rPdt2W088V9Vkjho4KtoggyktZJlMduZAtRHm68LU=
+1
View File
@@ -105,6 +105,7 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
},
}))
},
Tag: tag,
Name: options.Name,
MTU: options.MTU,
Address: options.Address,
+44
View File
@@ -0,0 +1,44 @@
package powerreport
import (
"sync"
"sync/atomic"
)
type Manager struct {
access sync.Mutex
recorder atomic.Pointer[Recorder]
}
func NewManager() *Manager {
return &Manager{}
}
func (m *Manager) Start(options Options) error {
m.access.Lock()
defer m.access.Unlock()
if m.recorder.Load() != nil {
return nil
}
recorder := NewRecorder(options)
err := recorder.Start()
if err != nil {
return err
}
m.recorder.Store(recorder)
return nil
}
func (m *Manager) Close() error {
m.access.Lock()
defer m.access.Unlock()
recorder := m.recorder.Swap(nil)
if recorder == nil {
return nil
}
return recorder.Close()
}
func (m *Manager) Recorder() *Recorder {
return m.recorder.Load()
}
+48
View File
@@ -0,0 +1,48 @@
package powerreport
import (
"os"
"path/filepath"
"strconv"
)
func PromoteDraft(basePath string) {
promoteDirectory(filepath.Join(basePath, DraftDirectoryName), filepath.Join(basePath, ReportsDirectoryName))
}
func finalizeDraft(draftPath string) {
promoteDirectory(draftPath, filepath.Join(filepath.Dir(draftPath), ReportsDirectoryName))
}
func promoteDirectory(draftPath string, reportsPath string) {
info, err := os.Stat(draftPath)
if err != nil || !info.IsDir() {
return
}
entries, err := os.ReadDir(draftPath)
if err != nil || len(entries) == 0 {
os.RemoveAll(draftPath)
return
}
err = os.MkdirAll(reportsPath, 0o777)
if err != nil {
return
}
destName := info.ModTime().UTC().Format("2006-01-02T15-04-05")
destPath := filepath.Join(reportsPath, destName)
for i := 1; ; i++ {
_, err = os.Stat(destPath)
if os.IsNotExist(err) {
break
}
if i > 1000 {
os.RemoveAll(draftPath)
return
}
destPath = filepath.Join(reportsPath, destName+"-"+strconv.Itoa(i))
}
err = os.Rename(draftPath, destPath)
if err != nil {
os.RemoveAll(draftPath)
}
}
+94
View File
@@ -0,0 +1,94 @@
package powerreport
type Direction uint8
const (
DirectionOutbound Direction = iota
DirectionInbound
)
func (d Direction) String() string {
if d == DirectionInbound {
return "in"
}
return "out"
}
type Attribution struct {
Inbound string `json:"inbound,omitempty"`
Domain string `json:"domain,omitempty"`
Destination string `json:"destination,omitempty"`
Outbound string `json:"outbound,omitempty"`
Endpoint string `json:"endpoint,omitempty"`
}
type timelineRow struct {
From string `json:"from"`
To string `json:"to"`
CPUUserMS int64 `json:"cpuUserMS,omitempty"`
CPUSystemMS int64 `json:"cpuSystemMS,omitempty"`
CPUPerformanceMS int64 `json:"cpuPerformanceMS,omitempty"`
CPUGCMS int64 `json:"cpuGCMS,omitempty"`
QoSMS *qosBreakdown `json:"qosMS,omitempty"`
PackageIdleWakeups uint64 `json:"packageIdleWakeups,omitempty"`
InterruptWakeups uint64 `json:"interruptWakeups,omitempty"`
EnergyNanojoules uint64 `json:"energyNJ,omitempty"`
PerformanceEnergyNanojoules uint64 `json:"performanceEnergyNJ,omitempty"`
DiskBytesWritten uint64 `json:"diskWriteBytes,omitempty"`
SleptMS int64 `json:"sleptMS,omitempty"`
Goroutines uint64 `json:"goroutines,omitempty"`
DNSQueries uint64 `json:"dnsQueries,omitempty"`
ConnectionsOpened uint64 `json:"connectionsOpened,omitempty"`
InterfacePackets map[string]uint64 `json:"interfacePackets,omitempty"`
NetworkType string `json:"network,omitempty"`
}
type qosBreakdown struct {
DefaultMS int64 `json:"default,omitempty"`
MaintenanceMS int64 `json:"maintenance,omitempty"`
BackgroundMS int64 `json:"background,omitempty"`
UtilityMS int64 `json:"utility,omitempty"`
LegacyMS int64 `json:"legacy,omitempty"`
UserInitiatedMS int64 `json:"userInitiated,omitempty"`
UserInteractiveMS int64 `json:"userInteractive,omitempty"`
}
const (
eventTypeBreak = "break"
eventTypeNetwork = "network"
)
type eventRecord struct {
Type string `json:"t"`
At string `json:"at"`
IdleMS int64 `json:"idleMS,omitempty"`
Direction string `json:"direction,omitempty"`
Size int `json:"size,omitempty"`
NetworkType string `json:"network,omitempty"`
By *Attribution `json:"by,omitempty"`
}
type systemUsage struct {
valid bool
userTime int64
systemTime int64
performanceUserTime int64
performanceSystemTime int64
qosDefaultTime int64
qosMaintenanceTime int64
qosBackgroundTime int64
qosUtilityTime int64
qosLegacyTime int64
qosUserInitiatedTime int64
qosUserInteractiveTime int64
packageIdleWakeups uint64
interruptWakeups uint64
diskBytesWritten uint64
energyNanojoules uint64
performanceEnergyNanojoules uint64
}
type interfaceCounters struct {
inPackets uint32
outPackets uint32
}
+472
View File
@@ -0,0 +1,472 @@
package powerreport
import (
"encoding/json"
"os"
"path/filepath"
"runtime/metrics"
"runtime/pprof"
"sync"
"sync/atomic"
"time"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
)
const (
DraftDirectoryName = "power_draft"
ReportsDirectoryName = "power_reports"
timelineFileName = "timeline.jsonl"
eventsFileName = "events.jsonl"
metadataFileName = "metadata.json"
logFileName = "go.log"
goroutineProfileFileName = "goroutine.pb.gz"
defaultGateInterval = 5 * time.Second
defaultSampleInterval = time.Minute
defaultFlushInterval = 15 * time.Minute
defaultFallbackInterval = 10 * time.Minute
activityRefreshNano = int64(time.Second)
rowCapacity = 4096
eventCapacity = 8192
)
type Options struct {
BasePath string
Logger logger.Logger
Metadata any
OwnerCallback func(path string)
LogCallback func() []byte
GateInterval time.Duration
SampleInterval time.Duration
FlushInterval time.Duration
FallbackInterval time.Duration
}
type Recorder struct {
draftPath string
logger logger.Logger
metadata any
ownerCallback func(path string)
logCallback func() []byte
gateNano int64
sampleNano int64
flushInterval time.Duration
fallbackInterval time.Duration
baseTime time.Time
_ [64]byte
lastActivity atomic.Int64
_ [64]byte
lastSampleAt atomic.Int64
pendingBreak atomic.Pointer[breakRecord]
notify chan struct{}
dnsQueries atomic.Uint64
connectionsOpened atomic.Uint64
access sync.Mutex
networkType string
rows []timelineRow
events []eventRecord
previous previousSample
lastFlushAt time.Time
started bool
closed bool
metricsSamples []metrics.Sample
done chan struct{}
workerDone chan struct{}
}
type breakRecord struct {
at time.Time
idleMS int64
direction Direction
size int
by *Attribution
}
type previousSample struct {
at time.Time
usage systemUsage
gcSeconds float64
absoluteTime int64
continuousTime int64
interfaces map[string]interfaceCounters
dnsQueries uint64
connectionsOpened uint64
}
func NewRecorder(options Options) *Recorder {
recorderLogger := options.Logger
if recorderLogger == nil {
recorderLogger = logger.NOP()
}
gateInterval := options.GateInterval
if gateInterval == 0 {
gateInterval = defaultGateInterval
}
sampleInterval := options.SampleInterval
if sampleInterval == 0 {
sampleInterval = defaultSampleInterval
}
flushInterval := options.FlushInterval
if flushInterval == 0 {
flushInterval = defaultFlushInterval
}
fallbackInterval := options.FallbackInterval
if fallbackInterval == 0 {
fallbackInterval = defaultFallbackInterval
}
return &Recorder{
draftPath: filepath.Join(options.BasePath, DraftDirectoryName),
logger: recorderLogger,
metadata: options.Metadata,
ownerCallback: options.OwnerCallback,
logCallback: options.LogCallback,
gateNano: int64(gateInterval),
sampleNano: int64(sampleInterval),
flushInterval: flushInterval,
fallbackInterval: fallbackInterval,
baseTime: time.Now(),
notify: make(chan struct{}, 1),
metricsSamples: []metrics.Sample{
{Name: "/cpu/classes/gc/total:cpu-seconds"},
{Name: "/sched/goroutines:goroutines"},
},
done: make(chan struct{}),
workerDone: make(chan struct{}),
}
}
func (r *Recorder) Start() error {
r.access.Lock()
defer r.access.Unlock()
if r.started {
return nil
}
PromoteDraft(filepath.Dir(r.draftPath))
err := os.MkdirAll(r.draftPath, 0o777)
if err != nil {
return E.Cause(err, "create power report draft directory")
}
r.chown(r.draftPath)
if r.metadata != nil {
metadataContent, marshalErr := json.Marshal(r.metadata)
if marshalErr == nil {
metadataPath := filepath.Join(r.draftPath, metadataFileName)
os.WriteFile(metadataPath, metadataContent, 0o666)
r.chown(metadataPath)
}
}
now := time.Now()
r.resetPreviousLocked(now)
r.lastSampleAt.Store(int64(now.Sub(r.baseTime)))
r.lastFlushAt = now
r.started = true
go r.worker()
return nil
}
func (r *Recorder) Close() error {
r.access.Lock()
if !r.started || r.closed {
r.access.Unlock()
return nil
}
r.closed = true
r.access.Unlock()
close(r.done)
<-r.workerDone
now := time.Now()
r.access.Lock()
r.consumeBreakLocked()
r.sampleLocked(now)
r.flushLocked(now)
r.access.Unlock()
r.writeGoroutineProfile()
r.writeLog()
finalizeDraft(r.draftPath)
return nil
}
func (r *Recorder) writeLog() {
if r.logCallback == nil {
return
}
content := r.logCallback()
if len(content) == 0 {
return
}
logPath := filepath.Join(r.draftPath, logFileName)
err := os.WriteFile(logPath, content, 0o666)
if err != nil {
return
}
r.chown(logPath)
}
// Touch reports one I/O activity: one read or write call, or one batched receive or send
// syscall on paths that batch packets. size is the size of the first packet of the activity
// and characterizes what ended an idle period; it is not accumulated. Volume totals come from
// the sampled interface counters instead.
func (r *Recorder) Touch(direction Direction, size int, by *Attribution) {
nowNano := int64(time.Since(r.baseTime))
lastNano := r.lastActivity.Load()
if nowNano-lastNano < activityRefreshNano {
return
}
previousNano := r.lastActivity.Swap(nowNano)
if previousNano != 0 && nowNano-previousNano >= r.gateNano {
r.pendingBreak.Store(&breakRecord{
at: time.Now(),
idleMS: (nowNano - previousNano) / int64(time.Millisecond),
direction: direction,
size: size,
by: by,
})
r.notifyWorker()
} else if nowNano-r.lastSampleAt.Load() >= r.sampleNano {
r.notifyWorker()
}
}
func (r *Recorder) CountDNSQuery() {
r.dnsQueries.Add(1)
}
func (r *Recorder) CountConnectionOpened() {
r.connectionsOpened.Add(1)
}
func (r *Recorder) RecordPlatformEvent(eventType string) {
now := time.Now()
r.access.Lock()
if !r.started || r.closed {
r.access.Unlock()
return
}
r.events = append(r.events, eventRecord{
Type: eventType,
At: now.UTC().Format(time.RFC3339),
})
r.access.Unlock()
r.notifyWorker()
}
func (r *Recorder) UpdateNetworkType(networkType string) {
now := time.Now()
r.access.Lock()
if r.closed || r.networkType == networkType {
r.access.Unlock()
return
}
r.networkType = networkType
r.events = append(r.events, eventRecord{
Type: eventTypeNetwork,
At: now.UTC().Format(time.RFC3339),
NetworkType: networkType,
})
r.access.Unlock()
r.notifyWorker()
}
func (r *Recorder) notifyWorker() {
select {
case r.notify <- struct{}{}:
default:
}
}
func (r *Recorder) worker() {
defer close(r.workerDone)
timer := time.NewTimer(r.fallbackInterval)
defer timer.Stop()
for {
select {
case <-r.done:
return
case <-r.notify:
case <-timer.C:
timer.Reset(r.fallbackInterval)
}
r.process()
}
}
func (r *Recorder) process() {
now := time.Now()
r.access.Lock()
defer r.access.Unlock()
if !r.started || r.closed {
return
}
r.consumeBreakLocked()
nowNano := int64(now.Sub(r.baseTime))
if nowNano-r.lastSampleAt.Load() >= r.sampleNano {
r.lastSampleAt.Store(nowNano)
r.sampleLocked(now)
}
if now.Sub(r.lastFlushAt) >= r.flushInterval || len(r.rows) >= rowCapacity || len(r.events) >= eventCapacity {
r.flushLocked(now)
}
}
func (r *Recorder) consumeBreakLocked() {
record := r.pendingBreak.Swap(nil)
if record == nil {
return
}
r.events = append(r.events, eventRecord{
Type: eventTypeBreak,
At: record.at.UTC().Format(time.RFC3339),
IdleMS: record.idleMS,
Direction: record.direction.String(),
Size: record.size,
NetworkType: r.networkType,
By: record.by,
})
}
func (r *Recorder) resetPreviousLocked(now time.Time) {
metrics.Read(r.metricsSamples)
r.previous = previousSample{
at: now,
usage: readSystemUsage(),
gcSeconds: r.metricsSamples[0].Value.Float64(),
interfaces: readInterfaceCounters(),
dnsQueries: r.dnsQueries.Load(),
connectionsOpened: r.connectionsOpened.Load(),
}
r.previous.absoluteTime, r.previous.continuousTime = readClocks()
}
func (r *Recorder) sampleLocked(now time.Time) {
previous := r.previous
r.resetPreviousLocked(now)
current := &r.previous
row := timelineRow{
From: previous.at.UTC().Format(time.RFC3339),
To: now.UTC().Format(time.RFC3339),
CPUGCMS: int64((current.gcSeconds - previous.gcSeconds) * 1000),
Goroutines: r.metricsSamples[1].Value.Uint64(),
DNSQueries: current.dnsQueries - previous.dnsQueries,
ConnectionsOpened: current.connectionsOpened - previous.connectionsOpened,
NetworkType: r.networkType,
}
if current.usage.valid && previous.usage.valid {
row.CPUUserMS = (current.usage.userTime - previous.usage.userTime) / int64(time.Millisecond)
row.CPUSystemMS = (current.usage.systemTime - previous.usage.systemTime) / int64(time.Millisecond)
row.CPUPerformanceMS = (current.usage.performanceUserTime - previous.usage.performanceUserTime +
current.usage.performanceSystemTime - previous.usage.performanceSystemTime) / int64(time.Millisecond)
row.PackageIdleWakeups = current.usage.packageIdleWakeups - previous.usage.packageIdleWakeups
row.InterruptWakeups = current.usage.interruptWakeups - previous.usage.interruptWakeups
row.EnergyNanojoules = current.usage.energyNanojoules - previous.usage.energyNanojoules
row.PerformanceEnergyNanojoules = current.usage.performanceEnergyNanojoules - previous.usage.performanceEnergyNanojoules
row.DiskBytesWritten = current.usage.diskBytesWritten - previous.usage.diskBytesWritten
qos := qosBreakdown{
DefaultMS: (current.usage.qosDefaultTime - previous.usage.qosDefaultTime) / int64(time.Millisecond),
MaintenanceMS: (current.usage.qosMaintenanceTime - previous.usage.qosMaintenanceTime) / int64(time.Millisecond),
BackgroundMS: (current.usage.qosBackgroundTime - previous.usage.qosBackgroundTime) / int64(time.Millisecond),
UtilityMS: (current.usage.qosUtilityTime - previous.usage.qosUtilityTime) / int64(time.Millisecond),
LegacyMS: (current.usage.qosLegacyTime - previous.usage.qosLegacyTime) / int64(time.Millisecond),
UserInitiatedMS: (current.usage.qosUserInitiatedTime - previous.usage.qosUserInitiatedTime) / int64(time.Millisecond),
UserInteractiveMS: (current.usage.qosUserInteractiveTime - previous.usage.qosUserInteractiveTime) / int64(time.Millisecond),
}
if qos != (qosBreakdown{}) {
row.QoSMS = &qos
}
}
if current.absoluteTime != 0 && previous.absoluteTime != 0 && current.absoluteTime >= previous.absoluteTime {
sleptNano := (current.continuousTime - previous.continuousTime) - (current.absoluteTime - previous.absoluteTime)
wallNano := now.Sub(previous.at).Nanoseconds()
if sleptNano > wallNano {
sleptNano = wallNano
}
if sleptNano > 0 {
row.SleptMS = sleptNano / int64(time.Millisecond)
}
}
if len(current.interfaces) > 0 && len(previous.interfaces) > 0 {
interfacePackets := make(map[string]uint64)
for name, counters := range current.interfaces {
previousCounters, found := previous.interfaces[name]
if !found {
continue
}
delta := uint64(counters.inPackets-previousCounters.inPackets) + uint64(counters.outPackets-previousCounters.outPackets)
if delta > 0 {
interfacePackets[name] = delta
}
}
if len(interfacePackets) > 0 {
row.InterfacePackets = interfacePackets
}
}
r.rows = append(r.rows, row)
}
func (r *Recorder) chown(path string) {
if r.ownerCallback != nil {
r.ownerCallback(path)
}
}
func (r *Recorder) flushLocked(now time.Time) {
err := appendRecords(r, filepath.Join(r.draftPath, timelineFileName), r.rows)
if err == nil {
r.rows = r.rows[:0]
} else {
r.logger.Error(E.Cause(err, "power report: write timeline"))
if len(r.rows) >= rowCapacity {
r.rows = r.rows[len(r.rows)-rowCapacity/2:]
}
}
err = appendRecords(r, filepath.Join(r.draftPath, eventsFileName), r.events)
if err == nil {
r.events = r.events[:0]
} else {
r.logger.Error(E.Cause(err, "power report: write events"))
if len(r.events) >= eventCapacity {
r.events = r.events[len(r.events)-eventCapacity/2:]
}
}
r.lastFlushAt = now
}
func appendRecords[T any](r *Recorder, path string, records []T) error {
if len(records) == 0 {
return nil
}
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666)
if err != nil {
return err
}
defer file.Close()
r.chown(path)
encoder := json.NewEncoder(file)
for _, record := range records {
err = encoder.Encode(record)
if err != nil {
return err
}
}
return nil
}
func (r *Recorder) writeGoroutineProfile() {
profilePath := filepath.Join(r.draftPath, goroutineProfileFileName)
file, err := os.OpenFile(profilePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o666)
if err != nil {
return
}
defer file.Close()
r.chown(profilePath)
pprof.Lookup("goroutine").WriteTo(file, 0)
}
+21
View File
@@ -0,0 +1,21 @@
package powerreport
//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go syscall_windows.go
type processIOCounters struct {
readOperationCount uint64
writeOperationCount uint64
otherOperationCount uint64
readTransferCount uint64
writeTransferCount uint64
otherTransferCount uint64
}
// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getprocessiocounters
//sys getProcessIoCounters(process windows.Handle, ioCounters *processIOCounters) (err error) = kernel32.GetProcessIoCounters
// https://learn.microsoft.com/en-us/windows/win32/api/realtimeapiset/nf-realtimeapiset-queryunbiasedinterrupttime
//sys queryUnbiasedInterruptTime(unbiasedTime *uint64) (err error) = kernel32.QueryUnbiasedInterruptTime
// https://learn.microsoft.com/en-us/windows/win32/api/realtimeapiset/nf-realtimeapiset-queryinterrupttime
//sys queryInterruptTime(interruptTime *uint64) = api-ms-win-core-realtime-l1-1-1.QueryInterruptTime
+123
View File
@@ -0,0 +1,123 @@
package powerreport
/*
#include <ifaddrs.h>
#include <mach/mach_time.h>
#include <net/if.h>
#include <net/if_var.h>
#include <sys/resource.h>
#include <sys/socket.h>
#include <unistd.h>
// The iOS SDK does not ship libproc.h; the symbol is exported by libSystem
// on all darwin platforms.
int proc_pid_rusage(int pid, int flavor, rusage_info_t *buffer);
*/
import "C"
import (
"os"
"strings"
"sync"
"unsafe"
)
var (
timebaseOnce sync.Once
timebaseNumer int64
timebaseDenom int64
)
func machTimebase() (int64, int64) {
timebaseOnce.Do(func() {
var timebase C.struct_mach_timebase_info
C.mach_timebase_info(&timebase)
timebaseNumer = int64(timebase.numer)
timebaseDenom = int64(timebase.denom)
})
return timebaseNumer, timebaseDenom
}
func machToNano(value uint64) int64 {
numer, denom := machTimebase()
if denom == 0 {
return int64(value)
}
return int64(value) * numer / denom
}
// The time fields of rusage_info are in mach_absolute_time units on arm64,
// not nanoseconds; the header does not document this.
func readSystemUsage() systemUsage {
var info C.struct_rusage_info_v6
result := C.proc_pid_rusage(C.int(os.Getpid()), C.RUSAGE_INFO_V6, (*C.rusage_info_t)(unsafe.Pointer(&info)))
if result == 0 {
return systemUsage{
valid: true,
userTime: machToNano(uint64(info.ri_user_time)),
systemTime: machToNano(uint64(info.ri_system_time)),
performanceUserTime: machToNano(uint64(info.ri_user_ptime)),
performanceSystemTime: machToNano(uint64(info.ri_system_ptime)),
qosDefaultTime: machToNano(uint64(info.ri_cpu_time_qos_default)),
qosMaintenanceTime: machToNano(uint64(info.ri_cpu_time_qos_maintenance)),
qosBackgroundTime: machToNano(uint64(info.ri_cpu_time_qos_background)),
qosUtilityTime: machToNano(uint64(info.ri_cpu_time_qos_utility)),
qosLegacyTime: machToNano(uint64(info.ri_cpu_time_qos_legacy)),
qosUserInitiatedTime: machToNano(uint64(info.ri_cpu_time_qos_user_initiated)),
qosUserInteractiveTime: machToNano(uint64(info.ri_cpu_time_qos_user_interactive)),
packageIdleWakeups: uint64(info.ri_pkg_idle_wkups),
interruptWakeups: uint64(info.ri_interrupt_wkups),
diskBytesWritten: uint64(info.ri_diskio_byteswritten),
energyNanojoules: uint64(info.ri_energy_nj),
performanceEnergyNanojoules: uint64(info.ri_penergy_nj),
}
}
var infoV4 C.struct_rusage_info_v4
result = C.proc_pid_rusage(C.int(os.Getpid()), C.RUSAGE_INFO_V4, (*C.rusage_info_t)(unsafe.Pointer(&infoV4)))
if result != 0 {
return systemUsage{}
}
return systemUsage{
valid: true,
userTime: machToNano(uint64(infoV4.ri_user_time)),
systemTime: machToNano(uint64(infoV4.ri_system_time)),
qosDefaultTime: machToNano(uint64(infoV4.ri_cpu_time_qos_default)),
qosMaintenanceTime: machToNano(uint64(infoV4.ri_cpu_time_qos_maintenance)),
qosBackgroundTime: machToNano(uint64(infoV4.ri_cpu_time_qos_background)),
qosUtilityTime: machToNano(uint64(infoV4.ri_cpu_time_qos_utility)),
qosLegacyTime: machToNano(uint64(infoV4.ri_cpu_time_qos_legacy)),
qosUserInitiatedTime: machToNano(uint64(infoV4.ri_cpu_time_qos_user_initiated)),
qosUserInteractiveTime: machToNano(uint64(infoV4.ri_cpu_time_qos_user_interactive)),
packageIdleWakeups: uint64(infoV4.ri_pkg_idle_wkups),
interruptWakeups: uint64(infoV4.ri_interrupt_wkups),
diskBytesWritten: uint64(infoV4.ri_diskio_byteswritten),
}
}
func readClocks() (absoluteTime int64, continuousTime int64) {
return machToNano(uint64(C.mach_absolute_time())), machToNano(uint64(C.mach_continuous_time()))
}
func readInterfaceCounters() map[string]interfaceCounters {
var list *C.struct_ifaddrs
if C.getifaddrs(&list) != 0 {
return nil
}
defer C.freeifaddrs(list)
result := make(map[string]interfaceCounters)
for entry := list; entry != nil; entry = entry.ifa_next {
if entry.ifa_addr == nil || entry.ifa_addr.sa_family != C.AF_LINK || entry.ifa_data == nil {
continue
}
name := C.GoString(entry.ifa_name)
if !strings.HasPrefix(name, "en") && !strings.HasPrefix(name, "pdp_ip") {
continue
}
data := (*C.struct_if_data)(entry.ifa_data)
result[name] = interfaceCounters{
inPackets: uint32(data.ifi_ipackets),
outPackets: uint32(data.ifi_opackets),
}
}
return result
}
@@ -0,0 +1,15 @@
//go:build darwin && !cgo
package powerreport
func readSystemUsage() systemUsage {
panic("power report requires CGO on darwin, rebuild with CGO_ENABLED=1")
}
func readClocks() (absoluteTime int64, continuousTime int64) {
panic("power report requires CGO on darwin, rebuild with CGO_ENABLED=1")
}
func readInterfaceCounters() map[string]interfaceCounters {
panic("power report requires CGO on darwin, rebuild with CGO_ENABLED=1")
}
+92
View File
@@ -0,0 +1,92 @@
package powerreport
import (
"os"
"strconv"
"strings"
"golang.org/x/sys/unix"
)
func readSystemUsage() systemUsage {
var rusage unix.Rusage
err := unix.Getrusage(unix.RUSAGE_SELF, &rusage)
if err != nil {
return systemUsage{}
}
return systemUsage{
valid: true,
userTime: rusage.Utime.Nano(),
systemTime: rusage.Stime.Nano(),
diskBytesWritten: readWriteBytes(),
}
}
func readWriteBytes() uint64 {
content, err := os.ReadFile("/proc/self/io")
if err != nil {
return 0
}
for line := range strings.SplitSeq(string(content), "\n") {
value, found := strings.CutPrefix(line, "write_bytes: ")
if !found {
continue
}
parsed, parseErr := strconv.ParseUint(value, 10, 64)
if parseErr != nil {
return 0
}
return parsed
}
return 0
}
func readClocks() (absoluteTime int64, continuousTime int64) {
var monotonicTime unix.Timespec
err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &monotonicTime)
if err != nil {
return 0, 0
}
var bootTime unix.Timespec
err = unix.ClockGettime(unix.CLOCK_BOOTTIME, &bootTime)
if err != nil {
return 0, 0
}
return monotonicTime.Nano(), bootTime.Nano()
}
func readInterfaceCounters() map[string]interfaceCounters {
content, err := os.ReadFile("/proc/net/dev")
if err != nil {
return nil
}
lines := strings.Split(string(content), "\n")
if len(lines) <= 2 {
return nil
}
result := make(map[string]interfaceCounters)
for _, line := range lines[2:] {
name, counters, found := strings.Cut(line, ":")
if !found {
continue
}
name = strings.TrimSpace(name)
if name == "lo" || strings.HasPrefix(name, "tun") || strings.HasPrefix(name, "utun") || strings.HasPrefix(name, "dummy") {
continue
}
fields := strings.Fields(counters)
if len(fields) < 10 {
continue
}
inPackets, inErr := strconv.ParseUint(fields[1], 10, 64)
outPackets, outErr := strconv.ParseUint(fields[9], 10, 64)
if inErr != nil || outErr != nil {
continue
}
result[name] = interfaceCounters{
inPackets: uint32(inPackets),
outPackets: uint32(outPackets),
}
}
return result
}
+15
View File
@@ -0,0 +1,15 @@
//go:build !darwin && !linux && !windows
package powerreport
func readSystemUsage() systemUsage {
return systemUsage{}
}
func readClocks() (absoluteTime int64, continuousTime int64) {
return 0, 0
}
func readInterfaceCounters() map[string]interfaceCounters {
return nil
}
+46
View File
@@ -0,0 +1,46 @@
package powerreport
import (
"golang.org/x/sys/windows"
)
func filetimeDuration(value windows.Filetime) int64 {
return (int64(value.HighDateTime)<<32 | int64(value.LowDateTime)) * 100
}
func readSystemUsage() systemUsage {
var creationTime, exitTime, kernelTime, userTime windows.Filetime
err := windows.GetProcessTimes(windows.CurrentProcess(), &creationTime, &exitTime, &kernelTime, &userTime)
if err != nil {
return systemUsage{}
}
usage := systemUsage{
valid: true,
userTime: filetimeDuration(userTime),
systemTime: filetimeDuration(kernelTime),
}
var ioCounters processIOCounters
err = getProcessIoCounters(windows.CurrentProcess(), &ioCounters)
if err == nil {
usage.diskBytesWritten = ioCounters.writeTransferCount
}
return usage
}
func readClocks() (absoluteTime int64, continuousTime int64) {
if procQueryInterruptTime.Find() != nil {
return 0, 0
}
var unbiasedTime uint64
err := queryUnbiasedInterruptTime(&unbiasedTime)
if err != nil {
return 0, 0
}
var interruptTime uint64
queryInterruptTime(&interruptTime)
return int64(unbiasedTime) * 100, int64(interruptTime) * 100
}
func readInterfaceCounters() map[string]interfaceCounters {
return nil
}
+68
View File
@@ -0,0 +1,68 @@
// Code generated by 'go generate'; DO NOT EDIT.
package powerreport
import (
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
var _ unsafe.Pointer
// Do the interface allocations only once for common
// Errno values.
const (
errnoERROR_IO_PENDING = 997
)
var (
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
errERROR_EINVAL error = syscall.EINVAL
)
// errnoErr returns common boxed Errno values, to prevent
// allocations at runtime.
func errnoErr(e syscall.Errno) error {
switch e {
case 0:
return errERROR_EINVAL
case errnoERROR_IO_PENDING:
return errERROR_IO_PENDING
}
// TODO: add more here, after collecting data on the common
// error values see on Windows. (perhaps when running
// all.bat?)
return e
}
var (
modapi_ms_win_core_realtime_l1_1_1 = windows.NewLazySystemDLL("api-ms-win-core-realtime-l1-1-1.dll")
modkernel32 = windows.NewLazySystemDLL("kernel32.dll")
procQueryInterruptTime = modapi_ms_win_core_realtime_l1_1_1.NewProc("QueryInterruptTime")
procGetProcessIoCounters = modkernel32.NewProc("GetProcessIoCounters")
procQueryUnbiasedInterruptTime = modkernel32.NewProc("QueryUnbiasedInterruptTime")
)
func queryInterruptTime(interruptTime *uint64) {
syscall.SyscallN(procQueryInterruptTime.Addr(), uintptr(unsafe.Pointer(interruptTime)))
return
}
func getProcessIoCounters(process windows.Handle, ioCounters *processIOCounters) (err error) {
r1, _, e1 := syscall.SyscallN(procGetProcessIoCounters.Addr(), uintptr(process), uintptr(unsafe.Pointer(ioCounters)))
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func queryUnbiasedInterruptTime(unbiasedTime *uint64) (err error) {
r1, _, e1 := syscall.SyscallN(procQueryUnbiasedInterruptTime.Addr(), uintptr(unsafe.Pointer(unbiasedTime)))
if r1 == 0 {
err = errnoErr(e1)
}
return
}
+13
View File
@@ -11,6 +11,7 @@ import (
"strings"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/service/powerreport"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
@@ -152,6 +153,18 @@ func (e *Endpoint) Start(postStart bool) error {
e.egressPool = tun.NewUDPEgressPool(egressPoolOptions)
standardBind.SetEgressProvider(e.egressPool)
}
powerManager := service.FromContext[*powerreport.Manager](e.options.Context)
if powerManager != nil {
recorder := powerManager.Recorder()
if recorder != nil {
attribution := &powerreport.Attribution{Endpoint: e.options.Tag}
standardBind.SetIOActivityFuncs(func(size int) {
recorder.Touch(powerreport.DirectionInbound, size, attribution)
}, func(size int) {
recorder.Touch(powerreport.DirectionOutbound, size, attribution)
})
}
}
bind = standardBind
} else {
var (
+1
View File
@@ -27,6 +27,7 @@ type EndpointOptions struct {
EgressPoolOptions tun.UDPEgressPoolOptions
Dialer N.Dialer
CreateDialer func(interfaceName string) N.Dialer
Tag string
Name string
MTU uint32
Address []netip.Prefix