platform: Add OOM Report & Crash Rerport

This commit is contained in:
世界
2026-04-02 16:39:34 +08:00
parent ddc181f65a
commit 574852bdc1
34 changed files with 2491 additions and 603 deletions

View File

@@ -87,12 +87,17 @@ func (s *StartedService) newInstance(profileContent string, overrideOptions *Ove
}
}
}
if s.oomKiller && C.IsIos {
if s.oomKillerEnabled {
if !common.Any(options.Services, func(it option.Service) bool {
return it.Type == C.TypeOOMKiller
}) {
oomOptions := &option.OOMKillerServiceOptions{
KillerDisabled: s.oomKillerDisabled,
MemoryLimitOverride: s.oomMemoryLimit,
}
options.Services = append(options.Services, option.Service{
Type: C.TypeOOMKiller,
Options: oomOptions,
})
}
}

View File

@@ -5,5 +5,6 @@ type PlatformHandler interface {
ServiceReload() error
SystemProxyStatus() (*SystemProxyStatus, error)
SetSystemProxyEnabled(enabled bool) error
TriggerNativeCrash() error
WriteDebugMessage(message string)
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/protocol/group"
"github.com/sagernet/sing-box/service/oomkiller"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/batch"
E "github.com/sagernet/sing/common/exceptions"
@@ -24,6 +25,8 @@ import (
"github.com/gofrs/uuid/v5"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
@@ -35,7 +38,9 @@ type StartedService struct {
handler PlatformHandler
debug bool
logMaxLines int
oomKiller bool
oomKillerEnabled bool
oomKillerDisabled bool
oomMemoryLimit uint64
// workingDirectory string
// tempDirectory string
// userID int
@@ -67,7 +72,9 @@ type ServiceOptions struct {
Handler PlatformHandler
Debug bool
LogMaxLines int
OOMKiller bool
OOMKillerEnabled bool
OOMKillerDisabled bool
OOMMemoryLimit uint64
// WorkingDirectory string
// TempDirectory string
// UserID int
@@ -82,7 +89,9 @@ func NewStartedService(options ServiceOptions) *StartedService {
handler: options.Handler,
debug: options.Debug,
logMaxLines: options.LogMaxLines,
oomKiller: options.OOMKiller,
oomKillerEnabled: options.OOMKillerEnabled,
oomKillerDisabled: options.OOMKillerDisabled,
oomMemoryLimit: options.OOMMemoryLimit,
// workingDirectory: options.WorkingDirectory,
// tempDirectory: options.TempDirectory,
// userID: options.UserID,
@@ -685,6 +694,41 @@ func (s *StartedService) SetSystemProxyEnabled(ctx context.Context, request *Set
return nil, err
}
func (s *StartedService) TriggerDebugCrash(ctx context.Context, request *DebugCrashRequest) (*emptypb.Empty, error) {
if !s.debug {
return nil, status.Error(codes.PermissionDenied, "debug crash trigger unavailable")
}
if request == nil {
return nil, status.Error(codes.InvalidArgument, "missing debug crash request")
}
switch request.Type {
case DebugCrashRequest_GO:
time.AfterFunc(200*time.Millisecond, func() {
panic("debug go crash")
})
case DebugCrashRequest_NATIVE:
err := s.handler.TriggerNativeCrash()
if err != nil {
return nil, err
}
default:
return nil, status.Error(codes.InvalidArgument, "unknown debug crash type")
}
return &emptypb.Empty{}, nil
}
func (s *StartedService) TriggerOOMReport(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
instance := s.Instance()
if instance == nil {
return nil, status.Error(codes.FailedPrecondition, "service not started")
}
reporter := service.FromContext[oomkiller.OOMReporter](instance.ctx)
if reporter == nil {
return nil, status.Error(codes.Unavailable, "OOM reporter not available")
}
return &emptypb.Empty{}, reporter.WriteReport(memory.Total())
}
func (s *StartedService) SubscribeConnections(request *SubscribeConnectionsRequest, server grpc.ServerStreamingServer[ConnectionEvents]) error {
err := s.waitForStarted(server.Context())
if err != nil {

View File

@@ -182,6 +182,52 @@ func (ServiceStatus_Type) EnumDescriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{0, 0}
}
type DebugCrashRequest_Type int32
const (
DebugCrashRequest_GO DebugCrashRequest_Type = 0
DebugCrashRequest_NATIVE DebugCrashRequest_Type = 1
)
// Enum value maps for DebugCrashRequest_Type.
var (
DebugCrashRequest_Type_name = map[int32]string{
0: "GO",
1: "NATIVE",
}
DebugCrashRequest_Type_value = map[string]int32{
"GO": 0,
"NATIVE": 1,
}
)
func (x DebugCrashRequest_Type) Enum() *DebugCrashRequest_Type {
p := new(DebugCrashRequest_Type)
*p = x
return p
}
func (x DebugCrashRequest_Type) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (DebugCrashRequest_Type) Descriptor() protoreflect.EnumDescriptor {
return file_daemon_started_service_proto_enumTypes[3].Descriptor()
}
func (DebugCrashRequest_Type) Type() protoreflect.EnumType {
return &file_daemon_started_service_proto_enumTypes[3]
}
func (x DebugCrashRequest_Type) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use DebugCrashRequest_Type.Descriptor instead.
func (DebugCrashRequest_Type) EnumDescriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{16, 0}
}
type ServiceStatus struct {
state protoimpl.MessageState `protogen:"open.v1"`
Status ServiceStatus_Type `protobuf:"varint,1,opt,name=status,proto3,enum=daemon.ServiceStatus_Type" json:"status,omitempty"`
@@ -1062,6 +1108,50 @@ func (x *SetSystemProxyEnabledRequest) GetEnabled() bool {
return false
}
type DebugCrashRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Type DebugCrashRequest_Type `protobuf:"varint,1,opt,name=type,proto3,enum=daemon.DebugCrashRequest_Type" json:"type,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DebugCrashRequest) Reset() {
*x = DebugCrashRequest{}
mi := &file_daemon_started_service_proto_msgTypes[16]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DebugCrashRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DebugCrashRequest) ProtoMessage() {}
func (x *DebugCrashRequest) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[16]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DebugCrashRequest.ProtoReflect.Descriptor instead.
func (*DebugCrashRequest) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{16}
}
func (x *DebugCrashRequest) GetType() DebugCrashRequest_Type {
if x != nil {
return x.Type
}
return DebugCrashRequest_GO
}
type SubscribeConnectionsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Interval int64 `protobuf:"varint,1,opt,name=interval,proto3" json:"interval,omitempty"`
@@ -1071,7 +1161,7 @@ type SubscribeConnectionsRequest struct {
func (x *SubscribeConnectionsRequest) Reset() {
*x = SubscribeConnectionsRequest{}
mi := &file_daemon_started_service_proto_msgTypes[16]
mi := &file_daemon_started_service_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1083,7 +1173,7 @@ func (x *SubscribeConnectionsRequest) String() string {
func (*SubscribeConnectionsRequest) ProtoMessage() {}
func (x *SubscribeConnectionsRequest) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[16]
mi := &file_daemon_started_service_proto_msgTypes[17]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1096,7 +1186,7 @@ func (x *SubscribeConnectionsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use SubscribeConnectionsRequest.ProtoReflect.Descriptor instead.
func (*SubscribeConnectionsRequest) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{16}
return file_daemon_started_service_proto_rawDescGZIP(), []int{17}
}
func (x *SubscribeConnectionsRequest) GetInterval() int64 {
@@ -1120,7 +1210,7 @@ type ConnectionEvent struct {
func (x *ConnectionEvent) Reset() {
*x = ConnectionEvent{}
mi := &file_daemon_started_service_proto_msgTypes[17]
mi := &file_daemon_started_service_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1132,7 +1222,7 @@ func (x *ConnectionEvent) String() string {
func (*ConnectionEvent) ProtoMessage() {}
func (x *ConnectionEvent) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[17]
mi := &file_daemon_started_service_proto_msgTypes[18]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1145,7 +1235,7 @@ func (x *ConnectionEvent) ProtoReflect() protoreflect.Message {
// Deprecated: Use ConnectionEvent.ProtoReflect.Descriptor instead.
func (*ConnectionEvent) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{17}
return file_daemon_started_service_proto_rawDescGZIP(), []int{18}
}
func (x *ConnectionEvent) GetType() ConnectionEventType {
@@ -1200,7 +1290,7 @@ type ConnectionEvents struct {
func (x *ConnectionEvents) Reset() {
*x = ConnectionEvents{}
mi := &file_daemon_started_service_proto_msgTypes[18]
mi := &file_daemon_started_service_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1212,7 +1302,7 @@ func (x *ConnectionEvents) String() string {
func (*ConnectionEvents) ProtoMessage() {}
func (x *ConnectionEvents) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[18]
mi := &file_daemon_started_service_proto_msgTypes[19]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1225,7 +1315,7 @@ func (x *ConnectionEvents) ProtoReflect() protoreflect.Message {
// Deprecated: Use ConnectionEvents.ProtoReflect.Descriptor instead.
func (*ConnectionEvents) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{18}
return file_daemon_started_service_proto_rawDescGZIP(), []int{19}
}
func (x *ConnectionEvents) GetEvents() []*ConnectionEvent {
@@ -1272,7 +1362,7 @@ type Connection struct {
func (x *Connection) Reset() {
*x = Connection{}
mi := &file_daemon_started_service_proto_msgTypes[19]
mi := &file_daemon_started_service_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1284,7 +1374,7 @@ func (x *Connection) String() string {
func (*Connection) ProtoMessage() {}
func (x *Connection) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[19]
mi := &file_daemon_started_service_proto_msgTypes[20]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1297,7 +1387,7 @@ func (x *Connection) ProtoReflect() protoreflect.Message {
// Deprecated: Use Connection.ProtoReflect.Descriptor instead.
func (*Connection) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{19}
return file_daemon_started_service_proto_rawDescGZIP(), []int{20}
}
func (x *Connection) GetId() string {
@@ -1467,7 +1557,7 @@ type ProcessInfo struct {
func (x *ProcessInfo) Reset() {
*x = ProcessInfo{}
mi := &file_daemon_started_service_proto_msgTypes[20]
mi := &file_daemon_started_service_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1479,7 +1569,7 @@ func (x *ProcessInfo) String() string {
func (*ProcessInfo) ProtoMessage() {}
func (x *ProcessInfo) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[20]
mi := &file_daemon_started_service_proto_msgTypes[21]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1492,7 +1582,7 @@ func (x *ProcessInfo) ProtoReflect() protoreflect.Message {
// Deprecated: Use ProcessInfo.ProtoReflect.Descriptor instead.
func (*ProcessInfo) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{20}
return file_daemon_started_service_proto_rawDescGZIP(), []int{21}
}
func (x *ProcessInfo) GetProcessId() uint32 {
@@ -1539,7 +1629,7 @@ type CloseConnectionRequest struct {
func (x *CloseConnectionRequest) Reset() {
*x = CloseConnectionRequest{}
mi := &file_daemon_started_service_proto_msgTypes[21]
mi := &file_daemon_started_service_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1551,7 +1641,7 @@ func (x *CloseConnectionRequest) String() string {
func (*CloseConnectionRequest) ProtoMessage() {}
func (x *CloseConnectionRequest) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[21]
mi := &file_daemon_started_service_proto_msgTypes[22]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1564,7 +1654,7 @@ func (x *CloseConnectionRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CloseConnectionRequest.ProtoReflect.Descriptor instead.
func (*CloseConnectionRequest) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{21}
return file_daemon_started_service_proto_rawDescGZIP(), []int{22}
}
func (x *CloseConnectionRequest) GetId() string {
@@ -1583,7 +1673,7 @@ type DeprecatedWarnings struct {
func (x *DeprecatedWarnings) Reset() {
*x = DeprecatedWarnings{}
mi := &file_daemon_started_service_proto_msgTypes[22]
mi := &file_daemon_started_service_proto_msgTypes[23]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1595,7 +1685,7 @@ func (x *DeprecatedWarnings) String() string {
func (*DeprecatedWarnings) ProtoMessage() {}
func (x *DeprecatedWarnings) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[22]
mi := &file_daemon_started_service_proto_msgTypes[23]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1608,7 +1698,7 @@ func (x *DeprecatedWarnings) ProtoReflect() protoreflect.Message {
// Deprecated: Use DeprecatedWarnings.ProtoReflect.Descriptor instead.
func (*DeprecatedWarnings) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{22}
return file_daemon_started_service_proto_rawDescGZIP(), []int{23}
}
func (x *DeprecatedWarnings) GetWarnings() []*DeprecatedWarning {
@@ -1629,7 +1719,7 @@ type DeprecatedWarning struct {
func (x *DeprecatedWarning) Reset() {
*x = DeprecatedWarning{}
mi := &file_daemon_started_service_proto_msgTypes[23]
mi := &file_daemon_started_service_proto_msgTypes[24]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1641,7 +1731,7 @@ func (x *DeprecatedWarning) String() string {
func (*DeprecatedWarning) ProtoMessage() {}
func (x *DeprecatedWarning) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[23]
mi := &file_daemon_started_service_proto_msgTypes[24]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1654,7 +1744,7 @@ func (x *DeprecatedWarning) ProtoReflect() protoreflect.Message {
// Deprecated: Use DeprecatedWarning.ProtoReflect.Descriptor instead.
func (*DeprecatedWarning) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{23}
return file_daemon_started_service_proto_rawDescGZIP(), []int{24}
}
func (x *DeprecatedWarning) GetMessage() string {
@@ -1687,7 +1777,7 @@ type StartedAt struct {
func (x *StartedAt) Reset() {
*x = StartedAt{}
mi := &file_daemon_started_service_proto_msgTypes[24]
mi := &file_daemon_started_service_proto_msgTypes[25]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1699,7 +1789,7 @@ func (x *StartedAt) String() string {
func (*StartedAt) ProtoMessage() {}
func (x *StartedAt) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[24]
mi := &file_daemon_started_service_proto_msgTypes[25]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1712,7 +1802,7 @@ func (x *StartedAt) ProtoReflect() protoreflect.Message {
// Deprecated: Use StartedAt.ProtoReflect.Descriptor instead.
func (*StartedAt) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{24}
return file_daemon_started_service_proto_rawDescGZIP(), []int{25}
}
func (x *StartedAt) GetStartedAt() int64 {
@@ -1732,7 +1822,7 @@ type Log_Message struct {
func (x *Log_Message) Reset() {
*x = Log_Message{}
mi := &file_daemon_started_service_proto_msgTypes[25]
mi := &file_daemon_started_service_proto_msgTypes[26]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1744,7 +1834,7 @@ func (x *Log_Message) String() string {
func (*Log_Message) ProtoMessage() {}
func (x *Log_Message) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[25]
mi := &file_daemon_started_service_proto_msgTypes[26]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1845,7 +1935,13 @@ const file_daemon_started_service_proto_rawDesc = "" +
"\tavailable\x18\x01 \x01(\bR\tavailable\x12\x18\n" +
"\aenabled\x18\x02 \x01(\bR\aenabled\"8\n" +
"\x1cSetSystemProxyEnabledRequest\x12\x18\n" +
"\aenabled\x18\x01 \x01(\bR\aenabled\"9\n" +
"\aenabled\x18\x01 \x01(\bR\aenabled\"c\n" +
"\x11DebugCrashRequest\x122\n" +
"\x04type\x18\x01 \x01(\x0e2\x1e.daemon.DebugCrashRequest.TypeR\x04type\"\x1a\n" +
"\x04Type\x12\x06\n" +
"\x02GO\x10\x00\x12\n" +
"\n" +
"\x06NATIVE\x10\x01\"9\n" +
"\x1bSubscribeConnectionsRequest\x12\x1a\n" +
"\binterval\x18\x01 \x01(\x03R\binterval\"\xea\x01\n" +
"\x0fConnectionEvent\x12/\n" +
@@ -1912,7 +2008,7 @@ const file_daemon_started_service_proto_rawDesc = "" +
"\x13ConnectionEventType\x12\x18\n" +
"\x14CONNECTION_EVENT_NEW\x10\x00\x12\x1b\n" +
"\x17CONNECTION_EVENT_UPDATE\x10\x01\x12\x1b\n" +
"\x17CONNECTION_EVENT_CLOSED\x10\x022\xe5\v\n" +
"\x17CONNECTION_EVENT_CLOSED\x10\x022\xf5\f\n" +
"\x0eStartedService\x12=\n" +
"\vStopService\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12?\n" +
"\rReloadService\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12K\n" +
@@ -1929,7 +2025,9 @@ const file_daemon_started_service_proto_rawDesc = "" +
"\x0eSelectOutbound\x12\x1d.daemon.SelectOutboundRequest\x1a\x16.google.protobuf.Empty\"\x00\x12I\n" +
"\x0eSetGroupExpand\x12\x1d.daemon.SetGroupExpandRequest\x1a\x16.google.protobuf.Empty\"\x00\x12K\n" +
"\x14GetSystemProxyStatus\x12\x16.google.protobuf.Empty\x1a\x19.daemon.SystemProxyStatus\"\x00\x12W\n" +
"\x15SetSystemProxyEnabled\x12$.daemon.SetSystemProxyEnabledRequest\x1a\x16.google.protobuf.Empty\"\x00\x12Y\n" +
"\x15SetSystemProxyEnabled\x12$.daemon.SetSystemProxyEnabledRequest\x1a\x16.google.protobuf.Empty\"\x00\x12H\n" +
"\x11TriggerDebugCrash\x12\x19.daemon.DebugCrashRequest\x1a\x16.google.protobuf.Empty\"\x00\x12D\n" +
"\x10TriggerOOMReport\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12Y\n" +
"\x14SubscribeConnections\x12#.daemon.SubscribeConnectionsRequest\x1a\x18.daemon.ConnectionEvents\"\x000\x01\x12K\n" +
"\x0fCloseConnection\x12\x1e.daemon.CloseConnectionRequest\x1a\x16.google.protobuf.Empty\"\x00\x12G\n" +
"\x13CloseAllConnections\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12M\n" +
@@ -1949,101 +2047,108 @@ func file_daemon_started_service_proto_rawDescGZIP() []byte {
}
var (
file_daemon_started_service_proto_enumTypes = make([]protoimpl.EnumInfo, 3)
file_daemon_started_service_proto_msgTypes = make([]protoimpl.MessageInfo, 26)
file_daemon_started_service_proto_enumTypes = make([]protoimpl.EnumInfo, 4)
file_daemon_started_service_proto_msgTypes = make([]protoimpl.MessageInfo, 27)
file_daemon_started_service_proto_goTypes = []any{
(LogLevel)(0), // 0: daemon.LogLevel
(ConnectionEventType)(0), // 1: daemon.ConnectionEventType
(ServiceStatus_Type)(0), // 2: daemon.ServiceStatus.Type
(*ServiceStatus)(nil), // 3: daemon.ServiceStatus
(*ReloadServiceRequest)(nil), // 4: daemon.ReloadServiceRequest
(*SubscribeStatusRequest)(nil), // 5: daemon.SubscribeStatusRequest
(*Log)(nil), // 6: daemon.Log
(*DefaultLogLevel)(nil), // 7: daemon.DefaultLogLevel
(*Status)(nil), // 8: daemon.Status
(*Groups)(nil), // 9: daemon.Groups
(*Group)(nil), // 10: daemon.Group
(*GroupItem)(nil), // 11: daemon.GroupItem
(*URLTestRequest)(nil), // 12: daemon.URLTestRequest
(*SelectOutboundRequest)(nil), // 13: daemon.SelectOutboundRequest
(*SetGroupExpandRequest)(nil), // 14: daemon.SetGroupExpandRequest
(*ClashMode)(nil), // 15: daemon.ClashMode
(*ClashModeStatus)(nil), // 16: daemon.ClashModeStatus
(*SystemProxyStatus)(nil), // 17: daemon.SystemProxyStatus
(*SetSystemProxyEnabledRequest)(nil), // 18: daemon.SetSystemProxyEnabledRequest
(*SubscribeConnectionsRequest)(nil), // 19: daemon.SubscribeConnectionsRequest
(*ConnectionEvent)(nil), // 20: daemon.ConnectionEvent
(*ConnectionEvents)(nil), // 21: daemon.ConnectionEvents
(*Connection)(nil), // 22: daemon.Connection
(*ProcessInfo)(nil), // 23: daemon.ProcessInfo
(*CloseConnectionRequest)(nil), // 24: daemon.CloseConnectionRequest
(*DeprecatedWarnings)(nil), // 25: daemon.DeprecatedWarnings
(*DeprecatedWarning)(nil), // 26: daemon.DeprecatedWarning
(*StartedAt)(nil), // 27: daemon.StartedAt
(*Log_Message)(nil), // 28: daemon.Log.Message
(*emptypb.Empty)(nil), // 29: google.protobuf.Empty
(DebugCrashRequest_Type)(0), // 3: daemon.DebugCrashRequest.Type
(*ServiceStatus)(nil), // 4: daemon.ServiceStatus
(*ReloadServiceRequest)(nil), // 5: daemon.ReloadServiceRequest
(*SubscribeStatusRequest)(nil), // 6: daemon.SubscribeStatusRequest
(*Log)(nil), // 7: daemon.Log
(*DefaultLogLevel)(nil), // 8: daemon.DefaultLogLevel
(*Status)(nil), // 9: daemon.Status
(*Groups)(nil), // 10: daemon.Groups
(*Group)(nil), // 11: daemon.Group
(*GroupItem)(nil), // 12: daemon.GroupItem
(*URLTestRequest)(nil), // 13: daemon.URLTestRequest
(*SelectOutboundRequest)(nil), // 14: daemon.SelectOutboundRequest
(*SetGroupExpandRequest)(nil), // 15: daemon.SetGroupExpandRequest
(*ClashMode)(nil), // 16: daemon.ClashMode
(*ClashModeStatus)(nil), // 17: daemon.ClashModeStatus
(*SystemProxyStatus)(nil), // 18: daemon.SystemProxyStatus
(*SetSystemProxyEnabledRequest)(nil), // 19: daemon.SetSystemProxyEnabledRequest
(*DebugCrashRequest)(nil), // 20: daemon.DebugCrashRequest
(*SubscribeConnectionsRequest)(nil), // 21: daemon.SubscribeConnectionsRequest
(*ConnectionEvent)(nil), // 22: daemon.ConnectionEvent
(*ConnectionEvents)(nil), // 23: daemon.ConnectionEvents
(*Connection)(nil), // 24: daemon.Connection
(*ProcessInfo)(nil), // 25: daemon.ProcessInfo
(*CloseConnectionRequest)(nil), // 26: daemon.CloseConnectionRequest
(*DeprecatedWarnings)(nil), // 27: daemon.DeprecatedWarnings
(*DeprecatedWarning)(nil), // 28: daemon.DeprecatedWarning
(*StartedAt)(nil), // 29: daemon.StartedAt
(*Log_Message)(nil), // 30: daemon.Log.Message
(*emptypb.Empty)(nil), // 31: google.protobuf.Empty
}
)
var file_daemon_started_service_proto_depIdxs = []int32{
2, // 0: daemon.ServiceStatus.status:type_name -> daemon.ServiceStatus.Type
28, // 1: daemon.Log.messages:type_name -> daemon.Log.Message
30, // 1: daemon.Log.messages:type_name -> daemon.Log.Message
0, // 2: daemon.DefaultLogLevel.level:type_name -> daemon.LogLevel
10, // 3: daemon.Groups.group:type_name -> daemon.Group
11, // 4: daemon.Group.items:type_name -> daemon.GroupItem
1, // 5: daemon.ConnectionEvent.type:type_name -> daemon.ConnectionEventType
22, // 6: daemon.ConnectionEvent.connection:type_name -> daemon.Connection
20, // 7: daemon.ConnectionEvents.events:type_name -> daemon.ConnectionEvent
23, // 8: daemon.Connection.processInfo:type_name -> daemon.ProcessInfo
26, // 9: daemon.DeprecatedWarnings.warnings:type_name -> daemon.DeprecatedWarning
0, // 10: daemon.Log.Message.level:type_name -> daemon.LogLevel
29, // 11: daemon.StartedService.StopService:input_type -> google.protobuf.Empty
29, // 12: daemon.StartedService.ReloadService:input_type -> google.protobuf.Empty
29, // 13: daemon.StartedService.SubscribeServiceStatus:input_type -> google.protobuf.Empty
29, // 14: daemon.StartedService.SubscribeLog:input_type -> google.protobuf.Empty
29, // 15: daemon.StartedService.GetDefaultLogLevel:input_type -> google.protobuf.Empty
29, // 16: daemon.StartedService.ClearLogs:input_type -> google.protobuf.Empty
5, // 17: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest
29, // 18: daemon.StartedService.SubscribeGroups:input_type -> google.protobuf.Empty
29, // 19: daemon.StartedService.GetClashModeStatus:input_type -> google.protobuf.Empty
29, // 20: daemon.StartedService.SubscribeClashMode:input_type -> google.protobuf.Empty
15, // 21: daemon.StartedService.SetClashMode:input_type -> daemon.ClashMode
12, // 22: daemon.StartedService.URLTest:input_type -> daemon.URLTestRequest
13, // 23: daemon.StartedService.SelectOutbound:input_type -> daemon.SelectOutboundRequest
14, // 24: daemon.StartedService.SetGroupExpand:input_type -> daemon.SetGroupExpandRequest
29, // 25: daemon.StartedService.GetSystemProxyStatus:input_type -> google.protobuf.Empty
18, // 26: daemon.StartedService.SetSystemProxyEnabled:input_type -> daemon.SetSystemProxyEnabledRequest
19, // 27: daemon.StartedService.SubscribeConnections:input_type -> daemon.SubscribeConnectionsRequest
24, // 28: daemon.StartedService.CloseConnection:input_type -> daemon.CloseConnectionRequest
29, // 29: daemon.StartedService.CloseAllConnections:input_type -> google.protobuf.Empty
29, // 30: daemon.StartedService.GetDeprecatedWarnings:input_type -> google.protobuf.Empty
29, // 31: daemon.StartedService.GetStartedAt:input_type -> google.protobuf.Empty
29, // 32: daemon.StartedService.StopService:output_type -> google.protobuf.Empty
29, // 33: daemon.StartedService.ReloadService:output_type -> google.protobuf.Empty
3, // 34: daemon.StartedService.SubscribeServiceStatus:output_type -> daemon.ServiceStatus
6, // 35: daemon.StartedService.SubscribeLog:output_type -> daemon.Log
7, // 36: daemon.StartedService.GetDefaultLogLevel:output_type -> daemon.DefaultLogLevel
29, // 37: daemon.StartedService.ClearLogs:output_type -> google.protobuf.Empty
8, // 38: daemon.StartedService.SubscribeStatus:output_type -> daemon.Status
9, // 39: daemon.StartedService.SubscribeGroups:output_type -> daemon.Groups
16, // 40: daemon.StartedService.GetClashModeStatus:output_type -> daemon.ClashModeStatus
15, // 41: daemon.StartedService.SubscribeClashMode:output_type -> daemon.ClashMode
29, // 42: daemon.StartedService.SetClashMode:output_type -> google.protobuf.Empty
29, // 43: daemon.StartedService.URLTest:output_type -> google.protobuf.Empty
29, // 44: daemon.StartedService.SelectOutbound:output_type -> google.protobuf.Empty
29, // 45: daemon.StartedService.SetGroupExpand:output_type -> google.protobuf.Empty
17, // 46: daemon.StartedService.GetSystemProxyStatus:output_type -> daemon.SystemProxyStatus
29, // 47: daemon.StartedService.SetSystemProxyEnabled:output_type -> google.protobuf.Empty
21, // 48: daemon.StartedService.SubscribeConnections:output_type -> daemon.ConnectionEvents
29, // 49: daemon.StartedService.CloseConnection:output_type -> google.protobuf.Empty
29, // 50: daemon.StartedService.CloseAllConnections:output_type -> google.protobuf.Empty
25, // 51: daemon.StartedService.GetDeprecatedWarnings:output_type -> daemon.DeprecatedWarnings
27, // 52: daemon.StartedService.GetStartedAt:output_type -> daemon.StartedAt
32, // [32:53] is the sub-list for method output_type
11, // [11:32] is the sub-list for method input_type
11, // [11:11] is the sub-list for extension type_name
11, // [11:11] is the sub-list for extension extendee
0, // [0:11] is the sub-list for field type_name
11, // 3: daemon.Groups.group:type_name -> daemon.Group
12, // 4: daemon.Group.items:type_name -> daemon.GroupItem
3, // 5: daemon.DebugCrashRequest.type:type_name -> daemon.DebugCrashRequest.Type
1, // 6: daemon.ConnectionEvent.type:type_name -> daemon.ConnectionEventType
24, // 7: daemon.ConnectionEvent.connection:type_name -> daemon.Connection
22, // 8: daemon.ConnectionEvents.events:type_name -> daemon.ConnectionEvent
25, // 9: daemon.Connection.processInfo:type_name -> daemon.ProcessInfo
28, // 10: daemon.DeprecatedWarnings.warnings:type_name -> daemon.DeprecatedWarning
0, // 11: daemon.Log.Message.level:type_name -> daemon.LogLevel
31, // 12: daemon.StartedService.StopService:input_type -> google.protobuf.Empty
31, // 13: daemon.StartedService.ReloadService:input_type -> google.protobuf.Empty
31, // 14: daemon.StartedService.SubscribeServiceStatus:input_type -> google.protobuf.Empty
31, // 15: daemon.StartedService.SubscribeLog:input_type -> google.protobuf.Empty
31, // 16: daemon.StartedService.GetDefaultLogLevel:input_type -> google.protobuf.Empty
31, // 17: daemon.StartedService.ClearLogs:input_type -> google.protobuf.Empty
6, // 18: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest
31, // 19: daemon.StartedService.SubscribeGroups:input_type -> google.protobuf.Empty
31, // 20: daemon.StartedService.GetClashModeStatus:input_type -> google.protobuf.Empty
31, // 21: daemon.StartedService.SubscribeClashMode:input_type -> google.protobuf.Empty
16, // 22: daemon.StartedService.SetClashMode:input_type -> daemon.ClashMode
13, // 23: daemon.StartedService.URLTest:input_type -> daemon.URLTestRequest
14, // 24: daemon.StartedService.SelectOutbound:input_type -> daemon.SelectOutboundRequest
15, // 25: daemon.StartedService.SetGroupExpand:input_type -> daemon.SetGroupExpandRequest
31, // 26: daemon.StartedService.GetSystemProxyStatus:input_type -> google.protobuf.Empty
19, // 27: daemon.StartedService.SetSystemProxyEnabled:input_type -> daemon.SetSystemProxyEnabledRequest
20, // 28: daemon.StartedService.TriggerDebugCrash:input_type -> daemon.DebugCrashRequest
31, // 29: daemon.StartedService.TriggerOOMReport:input_type -> google.protobuf.Empty
21, // 30: daemon.StartedService.SubscribeConnections:input_type -> daemon.SubscribeConnectionsRequest
26, // 31: daemon.StartedService.CloseConnection:input_type -> daemon.CloseConnectionRequest
31, // 32: daemon.StartedService.CloseAllConnections:input_type -> google.protobuf.Empty
31, // 33: daemon.StartedService.GetDeprecatedWarnings:input_type -> google.protobuf.Empty
31, // 34: daemon.StartedService.GetStartedAt:input_type -> google.protobuf.Empty
31, // 35: daemon.StartedService.StopService:output_type -> google.protobuf.Empty
31, // 36: daemon.StartedService.ReloadService:output_type -> google.protobuf.Empty
4, // 37: daemon.StartedService.SubscribeServiceStatus:output_type -> daemon.ServiceStatus
7, // 38: daemon.StartedService.SubscribeLog:output_type -> daemon.Log
8, // 39: daemon.StartedService.GetDefaultLogLevel:output_type -> daemon.DefaultLogLevel
31, // 40: daemon.StartedService.ClearLogs:output_type -> google.protobuf.Empty
9, // 41: daemon.StartedService.SubscribeStatus:output_type -> daemon.Status
10, // 42: daemon.StartedService.SubscribeGroups:output_type -> daemon.Groups
17, // 43: daemon.StartedService.GetClashModeStatus:output_type -> daemon.ClashModeStatus
16, // 44: daemon.StartedService.SubscribeClashMode:output_type -> daemon.ClashMode
31, // 45: daemon.StartedService.SetClashMode:output_type -> google.protobuf.Empty
31, // 46: daemon.StartedService.URLTest:output_type -> google.protobuf.Empty
31, // 47: daemon.StartedService.SelectOutbound:output_type -> google.protobuf.Empty
31, // 48: daemon.StartedService.SetGroupExpand:output_type -> google.protobuf.Empty
18, // 49: daemon.StartedService.GetSystemProxyStatus:output_type -> daemon.SystemProxyStatus
31, // 50: daemon.StartedService.SetSystemProxyEnabled:output_type -> google.protobuf.Empty
31, // 51: daemon.StartedService.TriggerDebugCrash:output_type -> google.protobuf.Empty
31, // 52: daemon.StartedService.TriggerOOMReport:output_type -> google.protobuf.Empty
23, // 53: daemon.StartedService.SubscribeConnections:output_type -> daemon.ConnectionEvents
31, // 54: daemon.StartedService.CloseConnection:output_type -> google.protobuf.Empty
31, // 55: daemon.StartedService.CloseAllConnections:output_type -> google.protobuf.Empty
27, // 56: daemon.StartedService.GetDeprecatedWarnings:output_type -> daemon.DeprecatedWarnings
29, // 57: daemon.StartedService.GetStartedAt:output_type -> daemon.StartedAt
35, // [35:58] is the sub-list for method output_type
12, // [12:35] is the sub-list for method input_type
12, // [12:12] is the sub-list for extension type_name
12, // [12:12] is the sub-list for extension extendee
0, // [0:12] is the sub-list for field type_name
}
func init() { file_daemon_started_service_proto_init() }
@@ -2056,8 +2161,8 @@ func file_daemon_started_service_proto_init() {
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_started_service_proto_rawDesc), len(file_daemon_started_service_proto_rawDesc)),
NumEnums: 3,
NumMessages: 26,
NumEnums: 4,
NumMessages: 27,
NumExtensions: 0,
NumServices: 1,
},

View File

@@ -26,6 +26,8 @@ service StartedService {
rpc GetSystemProxyStatus(google.protobuf.Empty) returns(SystemProxyStatus) {}
rpc SetSystemProxyEnabled(SetSystemProxyEnabledRequest) returns(google.protobuf.Empty) {}
rpc TriggerDebugCrash(DebugCrashRequest) returns(google.protobuf.Empty) {}
rpc TriggerOOMReport(google.protobuf.Empty) returns(google.protobuf.Empty) {}
rpc SubscribeConnections(SubscribeConnectionsRequest) returns(stream ConnectionEvents) {}
rpc CloseConnection(CloseConnectionRequest) returns(google.protobuf.Empty) {}
@@ -141,6 +143,15 @@ message SetSystemProxyEnabledRequest {
bool enabled = 1;
}
message DebugCrashRequest {
enum Type {
GO = 0;
NATIVE = 1;
}
Type type = 1;
}
message SubscribeConnectionsRequest {
int64 interval = 1;
}

View File

@@ -31,6 +31,8 @@ const (
StartedService_SetGroupExpand_FullMethodName = "/daemon.StartedService/SetGroupExpand"
StartedService_GetSystemProxyStatus_FullMethodName = "/daemon.StartedService/GetSystemProxyStatus"
StartedService_SetSystemProxyEnabled_FullMethodName = "/daemon.StartedService/SetSystemProxyEnabled"
StartedService_TriggerDebugCrash_FullMethodName = "/daemon.StartedService/TriggerDebugCrash"
StartedService_TriggerOOMReport_FullMethodName = "/daemon.StartedService/TriggerOOMReport"
StartedService_SubscribeConnections_FullMethodName = "/daemon.StartedService/SubscribeConnections"
StartedService_CloseConnection_FullMethodName = "/daemon.StartedService/CloseConnection"
StartedService_CloseAllConnections_FullMethodName = "/daemon.StartedService/CloseAllConnections"
@@ -58,6 +60,8 @@ type StartedServiceClient interface {
SetGroupExpand(ctx context.Context, in *SetGroupExpandRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
GetSystemProxyStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SystemProxyStatus, error)
SetSystemProxyEnabled(ctx context.Context, in *SetSystemProxyEnabledRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
TriggerDebugCrash(ctx context.Context, in *DebugCrashRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
TriggerOOMReport(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ConnectionEvents], error)
CloseConnection(ctx context.Context, in *CloseConnectionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
CloseAllConnections(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
@@ -278,6 +282,26 @@ func (c *startedServiceClient) SetSystemProxyEnabled(ctx context.Context, in *Se
return out, nil
}
func (c *startedServiceClient) TriggerDebugCrash(ctx context.Context, in *DebugCrashRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, StartedService_TriggerDebugCrash_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *startedServiceClient) TriggerOOMReport(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, StartedService_TriggerOOMReport_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *startedServiceClient) SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ConnectionEvents], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[5], StartedService_SubscribeConnections_FullMethodName, cOpts...)
@@ -357,6 +381,8 @@ type StartedServiceServer interface {
SetGroupExpand(context.Context, *SetGroupExpandRequest) (*emptypb.Empty, error)
GetSystemProxyStatus(context.Context, *emptypb.Empty) (*SystemProxyStatus, error)
SetSystemProxyEnabled(context.Context, *SetSystemProxyEnabledRequest) (*emptypb.Empty, error)
TriggerDebugCrash(context.Context, *DebugCrashRequest) (*emptypb.Empty, error)
TriggerOOMReport(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[ConnectionEvents]) error
CloseConnection(context.Context, *CloseConnectionRequest) (*emptypb.Empty, error)
CloseAllConnections(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
@@ -436,6 +462,14 @@ func (UnimplementedStartedServiceServer) SetSystemProxyEnabled(context.Context,
return nil, status.Error(codes.Unimplemented, "method SetSystemProxyEnabled not implemented")
}
func (UnimplementedStartedServiceServer) TriggerDebugCrash(context.Context, *DebugCrashRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method TriggerDebugCrash not implemented")
}
func (UnimplementedStartedServiceServer) TriggerOOMReport(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method TriggerOOMReport not implemented")
}
func (UnimplementedStartedServiceServer) SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[ConnectionEvents]) error {
return status.Error(codes.Unimplemented, "method SubscribeConnections not implemented")
}
@@ -729,6 +763,42 @@ func _StartedService_SetSystemProxyEnabled_Handler(srv interface{}, ctx context.
return interceptor(ctx, in, info, handler)
}
func _StartedService_TriggerDebugCrash_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DebugCrashRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StartedServiceServer).TriggerDebugCrash(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: StartedService_TriggerDebugCrash_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StartedServiceServer).TriggerDebugCrash(ctx, req.(*DebugCrashRequest))
}
return interceptor(ctx, in, info, handler)
}
func _StartedService_TriggerOOMReport_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.(StartedServiceServer).TriggerOOMReport(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: StartedService_TriggerOOMReport_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StartedServiceServer).TriggerOOMReport(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _StartedService_SubscribeConnections_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(SubscribeConnectionsRequest)
if err := stream.RecvMsg(m); err != nil {
@@ -863,6 +933,14 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{
MethodName: "SetSystemProxyEnabled",
Handler: _StartedService_SetSystemProxyEnabled_Handler,
},
{
MethodName: "TriggerDebugCrash",
Handler: _StartedService_TriggerDebugCrash_Handler,
},
{
MethodName: "TriggerOOMReport",
Handler: _StartedService_TriggerOOMReport_Handler,
},
{
MethodName: "CloseConnection",
Handler: _StartedService_CloseConnection_Handler,

View File

@@ -540,6 +540,31 @@ func (c *CommandClient) SetSystemProxyEnabled(isEnabled bool) error {
return err
}
func (c *CommandClient) TriggerGoCrash() error {
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
return client.TriggerDebugCrash(context.Background(), &daemon.DebugCrashRequest{
Type: daemon.DebugCrashRequest_GO,
})
})
return err
}
func (c *CommandClient) TriggerNativeCrash() error {
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
return client.TriggerDebugCrash(context.Background(), &daemon.DebugCrashRequest{
Type: daemon.DebugCrashRequest_NATIVE,
})
})
return err
}
func (c *CommandClient) TriggerOOMReport() error {
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
return client.TriggerOOMReport(context.Background(), &emptypb.Empty{})
})
return err
}
func (c *CommandClient) GetDeprecatedNotes() (DeprecatedNoteIterator, error) {
return callWithResult(c, func(client daemon.StartedServiceClient) (DeprecatedNoteIterator, error) {
warnings, err := client.GetDeprecatedWarnings(context.Background(), &emptypb.Empty{})

View File

@@ -39,6 +39,7 @@ type CommandServerHandler interface {
ServiceReload() error
GetSystemProxyStatus() (*SystemProxyStatus, error)
SetSystemProxyEnabled(enabled bool) error
TriggerNativeCrash() error
WriteDebugMessage(message string)
}
@@ -60,7 +61,9 @@ func NewCommandServer(handler CommandServerHandler, platformInterface PlatformIn
Handler: (*platformHandler)(server),
Debug: sDebug,
LogMaxLines: sLogMaxLines,
OOMKiller: memoryLimitEnabled,
OOMKillerEnabled: sOOMKillerEnabled,
OOMKillerDisabled: sOOMKillerDisabled,
OOMMemoryLimit: uint64(sOOMMemoryLimit),
// WorkingDirectory: sWorkingPath,
// TempDirectory: sTempPath,
// UserID: sUserID,
@@ -170,11 +173,16 @@ type OverrideOptions struct {
}
func (s *CommandServer) StartOrReloadService(configContent string, options *OverrideOptions) error {
return s.StartedService.StartOrReloadService(configContent, &daemon.OverrideOptions{
saveConfigSnapshot(configContent)
err := s.StartedService.StartOrReloadService(configContent, &daemon.OverrideOptions{
AutoRedirect: options.AutoRedirect,
IncludePackage: iteratorToArray(options.IncludePackage),
ExcludePackage: iteratorToArray(options.ExcludePackage),
})
if err != nil {
return err
}
return nil
}
func (s *CommandServer) CloseService() error {
@@ -271,6 +279,10 @@ func (h *platformHandler) SetSystemProxyEnabled(enabled bool) error {
return (*CommandServer)(h).handler.SetSystemProxyEnabled(enabled)
}
func (h *platformHandler) TriggerNativeCrash() error {
return (*CommandServer)(h).handler.TriggerNativeCrash()
}
func (h *platformHandler) WriteDebugMessage(message string) {
(*CommandServer)(h).handler.WriteDebugMessage(message)
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/sagernet/sing-box/include"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/service/oomkiller"
tun "github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
@@ -22,6 +23,8 @@ import (
"github.com/sagernet/sing/service/filemanager"
)
var sOOMReporter oomkiller.OOMReporter
func baseContext(platformInterface PlatformInterface) context.Context {
dnsRegistry := include.DNSTransportRegistry()
if platformInterface != nil {
@@ -33,6 +36,9 @@ func baseContext(platformInterface PlatformInterface) context.Context {
}
ctx := context.Background()
ctx = filemanager.WithDefault(ctx, sWorkingPath, sTempPath, sUserID, sGroupID)
if sOOMReporter != nil {
ctx = service.ContextWith[oomkiller.OOMReporter](ctx, sOOMReporter)
}
return box.Context(ctx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), dnsRegistry, include.ServiceRegistry(), include.CertificateProviderRegistry())
}

View File

@@ -0,0 +1,9 @@
package libbox
import "time"
func TriggerGoPanic() {
time.AfterFunc(200*time.Millisecond, func() {
panic("debug go crash")
})
}

View File

@@ -0,0 +1,390 @@
//go:build darwin || linux || windows
package oomprofile
import (
"fmt"
"io"
"runtime"
"time"
)
const (
tagProfile_SampleType = 1
tagProfile_Sample = 2
tagProfile_Mapping = 3
tagProfile_Location = 4
tagProfile_Function = 5
tagProfile_StringTable = 6
tagProfile_TimeNanos = 9
tagProfile_PeriodType = 11
tagProfile_Period = 12
tagProfile_DefaultSampleType = 14
tagValueType_Type = 1
tagValueType_Unit = 2
tagSample_Location = 1
tagSample_Value = 2
tagSample_Label = 3
tagLabel_Key = 1
tagLabel_Str = 2
tagLabel_Num = 3
tagMapping_ID = 1
tagMapping_Start = 2
tagMapping_Limit = 3
tagMapping_Offset = 4
tagMapping_Filename = 5
tagMapping_BuildID = 6
tagMapping_HasFunctions = 7
tagMapping_HasFilenames = 8
tagMapping_HasLineNumbers = 9
tagMapping_HasInlineFrames = 10
tagLocation_ID = 1
tagLocation_MappingID = 2
tagLocation_Address = 3
tagLocation_Line = 4
tagLine_FunctionID = 1
tagLine_Line = 2
tagFunction_ID = 1
tagFunction_Name = 2
tagFunction_SystemName = 3
tagFunction_Filename = 4
tagFunction_StartLine = 5
)
type memMap struct {
start uintptr
end uintptr
offset uint64
file string
buildID string
funcs symbolizeFlag
fake bool
}
type symbolizeFlag uint8
const (
lookupTried symbolizeFlag = 1 << iota
lookupFailed
)
func newProfileBuilder(w io.Writer) *profileBuilder {
builder := &profileBuilder{
start: time.Now(),
w: w,
strings: []string{""},
stringMap: map[string]int{"": 0},
locs: map[uintptr]locInfo{},
funcs: map[string]int{},
}
builder.readMapping()
return builder
}
func (b *profileBuilder) stringIndex(s string) int64 {
id, ok := b.stringMap[s]
if !ok {
id = len(b.strings)
b.strings = append(b.strings, s)
b.stringMap[s] = id
}
return int64(id)
}
func (b *profileBuilder) flush() {
const dataFlush = 4096
if b.err != nil || b.pb.nest != 0 || len(b.pb.data) <= dataFlush {
return
}
_, b.err = b.w.Write(b.pb.data)
b.pb.data = b.pb.data[:0]
}
func (b *profileBuilder) pbValueType(tag int, typ string, unit string) {
start := b.pb.startMessage()
b.pb.int64(tagValueType_Type, b.stringIndex(typ))
b.pb.int64(tagValueType_Unit, b.stringIndex(unit))
b.pb.endMessage(tag, start)
}
func (b *profileBuilder) pbSample(values []int64, locs []uint64, labels func()) {
start := b.pb.startMessage()
b.pb.int64s(tagSample_Value, values)
b.pb.uint64s(tagSample_Location, locs)
if labels != nil {
labels()
}
b.pb.endMessage(tagProfile_Sample, start)
b.flush()
}
func (b *profileBuilder) pbLabel(tag int, key string, str string, num int64) {
start := b.pb.startMessage()
b.pb.int64Opt(tagLabel_Key, b.stringIndex(key))
b.pb.int64Opt(tagLabel_Str, b.stringIndex(str))
b.pb.int64Opt(tagLabel_Num, num)
b.pb.endMessage(tag, start)
}
func (b *profileBuilder) pbLine(tag int, funcID uint64, line int64) {
start := b.pb.startMessage()
b.pb.uint64Opt(tagLine_FunctionID, funcID)
b.pb.int64Opt(tagLine_Line, line)
b.pb.endMessage(tag, start)
}
func (b *profileBuilder) pbMapping(tag int, id uint64, base uint64, limit uint64, offset uint64, file string, buildID string, hasFuncs bool) {
start := b.pb.startMessage()
b.pb.uint64Opt(tagMapping_ID, id)
b.pb.uint64Opt(tagMapping_Start, base)
b.pb.uint64Opt(tagMapping_Limit, limit)
b.pb.uint64Opt(tagMapping_Offset, offset)
b.pb.int64Opt(tagMapping_Filename, b.stringIndex(file))
b.pb.int64Opt(tagMapping_BuildID, b.stringIndex(buildID))
if hasFuncs {
b.pb.bool(tagMapping_HasFunctions, true)
}
b.pb.endMessage(tag, start)
}
func (b *profileBuilder) build() error {
if b.err != nil {
return b.err
}
b.pb.int64Opt(tagProfile_TimeNanos, b.start.UnixNano())
for i, mapping := range b.mem {
hasFunctions := mapping.funcs == lookupTried
b.pbMapping(tagProfile_Mapping, uint64(i+1), uint64(mapping.start), uint64(mapping.end), mapping.offset, mapping.file, mapping.buildID, hasFunctions)
}
b.pb.strings(tagProfile_StringTable, b.strings)
if b.err != nil {
return b.err
}
_, err := b.w.Write(b.pb.data)
return err
}
func allFrames(addr uintptr) ([]runtime.Frame, symbolizeFlag) {
frames := runtime.CallersFrames([]uintptr{addr})
frame, more := frames.Next()
if frame.Function == "runtime.goexit" {
return nil, 0
}
result := lookupTried
if frame.PC == 0 || frame.Function == "" || frame.File == "" || frame.Line == 0 {
result |= lookupFailed
}
if frame.PC == 0 {
frame.PC = addr - 1
}
ret := []runtime.Frame{frame}
for frame.Function != "runtime.goexit" && more {
frame, more = frames.Next()
ret = append(ret, frame)
}
return ret, result
}
type locInfo struct {
id uint64
pcs []uintptr
firstPCFrames []runtime.Frame
firstPCSymbolizeResult symbolizeFlag
}
func (b *profileBuilder) appendLocsForStack(locs []uint64, stk []uintptr) []uint64 {
b.deck.reset()
origStk := stk
stk = runtimeExpandFinalInlineFrame(stk)
for len(stk) > 0 {
addr := stk[0]
if loc, ok := b.locs[addr]; ok {
if len(b.deck.pcs) > 0 {
if b.deck.tryAdd(addr, loc.firstPCFrames, loc.firstPCSymbolizeResult) {
stk = stk[1:]
continue
}
}
if id := b.emitLocation(); id > 0 {
locs = append(locs, id)
}
locs = append(locs, loc.id)
if len(loc.pcs) > len(stk) {
panic(fmt.Sprintf("stack too short to match cached location; stk = %#x, loc.pcs = %#x, original stk = %#x", stk, loc.pcs, origStk))
}
stk = stk[len(loc.pcs):]
continue
}
frames, symbolizeResult := allFrames(addr)
if len(frames) == 0 {
if id := b.emitLocation(); id > 0 {
locs = append(locs, id)
}
stk = stk[1:]
continue
}
if b.deck.tryAdd(addr, frames, symbolizeResult) {
stk = stk[1:]
continue
}
if id := b.emitLocation(); id > 0 {
locs = append(locs, id)
}
if loc, ok := b.locs[addr]; ok {
locs = append(locs, loc.id)
stk = stk[len(loc.pcs):]
} else {
b.deck.tryAdd(addr, frames, symbolizeResult)
stk = stk[1:]
}
}
if id := b.emitLocation(); id > 0 {
locs = append(locs, id)
}
return locs
}
type pcDeck struct {
pcs []uintptr
frames []runtime.Frame
symbolizeResult symbolizeFlag
firstPCFrames int
firstPCSymbolizeResult symbolizeFlag
}
func (d *pcDeck) reset() {
d.pcs = d.pcs[:0]
d.frames = d.frames[:0]
d.symbolizeResult = 0
d.firstPCFrames = 0
d.firstPCSymbolizeResult = 0
}
func (d *pcDeck) tryAdd(pc uintptr, frames []runtime.Frame, symbolizeResult symbolizeFlag) bool {
if existing := len(d.frames); existing > 0 {
newFrame := frames[0]
last := d.frames[existing-1]
if last.Func != nil {
return false
}
if last.Entry == 0 || newFrame.Entry == 0 {
return false
}
if last.Entry != newFrame.Entry {
return false
}
if runtimeFrameSymbolName(&last) == runtimeFrameSymbolName(&newFrame) {
return false
}
}
d.pcs = append(d.pcs, pc)
d.frames = append(d.frames, frames...)
d.symbolizeResult |= symbolizeResult
if len(d.pcs) == 1 {
d.firstPCFrames = len(d.frames)
d.firstPCSymbolizeResult = symbolizeResult
}
return true
}
func (b *profileBuilder) emitLocation() uint64 {
if len(b.deck.pcs) == 0 {
return 0
}
defer b.deck.reset()
addr := b.deck.pcs[0]
firstFrame := b.deck.frames[0]
type newFunc struct {
id uint64
name string
file string
startLine int64
}
newFuncs := make([]newFunc, 0, 8)
id := uint64(len(b.locs)) + 1
b.locs[addr] = locInfo{
id: id,
pcs: append([]uintptr{}, b.deck.pcs...),
firstPCFrames: append([]runtime.Frame{}, b.deck.frames[:b.deck.firstPCFrames]...),
firstPCSymbolizeResult: b.deck.firstPCSymbolizeResult,
}
start := b.pb.startMessage()
b.pb.uint64Opt(tagLocation_ID, id)
b.pb.uint64Opt(tagLocation_Address, uint64(firstFrame.PC))
for _, frame := range b.deck.frames {
funcName := runtimeFrameSymbolName(&frame)
funcID := uint64(b.funcs[funcName])
if funcID == 0 {
funcID = uint64(len(b.funcs)) + 1
b.funcs[funcName] = int(funcID)
newFuncs = append(newFuncs, newFunc{
id: funcID,
name: funcName,
file: frame.File,
startLine: int64(runtimeFrameStartLine(&frame)),
})
}
b.pbLine(tagLocation_Line, funcID, int64(frame.Line))
}
for i := range b.mem {
if (b.mem[i].start <= addr && addr < b.mem[i].end) || b.mem[i].fake {
b.pb.uint64Opt(tagLocation_MappingID, uint64(i+1))
mapping := b.mem[i]
mapping.funcs |= b.deck.symbolizeResult
b.mem[i] = mapping
break
}
}
b.pb.endMessage(tagProfile_Location, start)
for _, fn := range newFuncs {
start := b.pb.startMessage()
b.pb.uint64Opt(tagFunction_ID, fn.id)
b.pb.int64Opt(tagFunction_Name, b.stringIndex(fn.name))
b.pb.int64Opt(tagFunction_SystemName, b.stringIndex(fn.name))
b.pb.int64Opt(tagFunction_Filename, b.stringIndex(fn.file))
b.pb.int64Opt(tagFunction_StartLine, fn.startLine)
b.pb.endMessage(tagProfile_Function, start)
}
b.flush()
return id
}
func (b *profileBuilder) addMapping(lo uint64, hi uint64, offset uint64, file string, buildID string) {
b.addMappingEntry(lo, hi, offset, file, buildID, false)
}
func (b *profileBuilder) addMappingEntry(lo uint64, hi uint64, offset uint64, file string, buildID string, fake bool) {
b.mem = append(b.mem, memMap{
start: uintptr(lo),
end: uintptr(hi),
offset: offset,
file: file,
buildID: buildID,
fake: fake,
})
}

View File

@@ -0,0 +1,24 @@
//go:build darwin && amd64
package oomprofile
type machVMRegionBasicInfoData struct {
Protection int32
MaxProtection int32
Inheritance uint32
Shared uint32
Reserved uint32
Offset [8]byte
Behavior int32
UserWiredCount uint16
PadCgo1 [2]byte
}
const (
_VM_PROT_READ = 0x1
_VM_PROT_EXECUTE = 0x4
_MACH_SEND_INVALID_DEST = 0x10000003
_MAXPATHLEN = 0x400
)

View File

@@ -0,0 +1,24 @@
//go:build darwin && arm64
package oomprofile
type machVMRegionBasicInfoData struct {
Protection int32
MaxProtection int32
Inheritance uint32
Shared int32
Reserved int32
Offset [8]byte
Behavior int32
UserWiredCount uint16
PadCgo1 [2]byte
}
const (
_VM_PROT_READ = 0x1
_VM_PROT_EXECUTE = 0x4
_MACH_SEND_INVALID_DEST = 0x10000003
_MAXPATHLEN = 0x400
)

View File

@@ -0,0 +1,47 @@
//go:build darwin || linux || windows
package oomprofile
import (
"runtime"
_ "runtime/pprof"
"unsafe"
_ "unsafe"
)
//go:linkname runtimeMemProfileInternal runtime.pprof_memProfileInternal
func runtimeMemProfileInternal(p []memProfileRecord, inuseZero bool) (n int, ok bool)
//go:linkname runtimeBlockProfileInternal runtime.pprof_blockProfileInternal
func runtimeBlockProfileInternal(p []blockProfileRecord) (n int, ok bool)
//go:linkname runtimeMutexProfileInternal runtime.pprof_mutexProfileInternal
func runtimeMutexProfileInternal(p []blockProfileRecord) (n int, ok bool)
//go:linkname runtimeThreadCreateInternal runtime.pprof_threadCreateInternal
func runtimeThreadCreateInternal(p []stackRecord) (n int, ok bool)
//go:linkname runtimeGoroutineProfileWithLabels runtime.pprof_goroutineProfileWithLabels
func runtimeGoroutineProfileWithLabels(p []stackRecord, labels []unsafe.Pointer) (n int, ok bool)
//go:linkname runtimeCyclesPerSecond runtime/pprof.runtime_cyclesPerSecond
func runtimeCyclesPerSecond() int64
//go:linkname runtimeMakeProfStack runtime.pprof_makeProfStack
func runtimeMakeProfStack() []uintptr
//go:linkname runtimeFrameStartLine runtime/pprof.runtime_FrameStartLine
func runtimeFrameStartLine(f *runtime.Frame) int
//go:linkname runtimeFrameSymbolName runtime/pprof.runtime_FrameSymbolName
func runtimeFrameSymbolName(f *runtime.Frame) string
//go:linkname runtimeExpandFinalInlineFrame runtime/pprof.runtime_expandFinalInlineFrame
func runtimeExpandFinalInlineFrame(stk []uintptr) []uintptr
//go:linkname stdParseProcSelfMaps runtime/pprof.parseProcSelfMaps
func stdParseProcSelfMaps(data []byte, addMapping func(lo uint64, hi uint64, offset uint64, file string, buildID string))
//go:linkname stdELFBuildID runtime/pprof.elfBuildID
func stdELFBuildID(file string) (string, error)

View File

@@ -0,0 +1,57 @@
//go:build darwin
package oomprofile
import (
"encoding/binary"
"os"
"unsafe"
_ "unsafe"
)
func isExecutable(protection int32) bool {
return (protection&_VM_PROT_EXECUTE) != 0 && (protection&_VM_PROT_READ) != 0
}
func (b *profileBuilder) readMapping() {
if !machVMInfo(b.addMapping) {
b.addMappingEntry(0, 0, 0, "", "", true)
}
}
func machVMInfo(addMapping func(lo uint64, hi uint64, off uint64, file string, buildID string)) bool {
added := false
addr := uint64(0x1)
for {
var regionSize uint64
var info machVMRegionBasicInfoData
kr := machVMRegion(&addr, &regionSize, unsafe.Pointer(&info))
if kr != 0 {
if kr == _MACH_SEND_INVALID_DEST {
return true
}
return added
}
if isExecutable(info.Protection) {
addMapping(addr, addr+regionSize, binary.LittleEndian.Uint64(info.Offset[:]), regionFilename(addr), "")
added = true
}
addr += regionSize
}
}
func regionFilename(address uint64) string {
buf := make([]byte, _MAXPATHLEN)
n := procRegionFilename(os.Getpid(), address, unsafe.SliceData(buf), int64(cap(buf)))
if n == 0 {
return ""
}
return string(buf[:n])
}
//go:linkname machVMRegion runtime/pprof.mach_vm_region
func machVMRegion(address *uint64, regionSize *uint64, info unsafe.Pointer) int32
//go:linkname procRegionFilename runtime/pprof.proc_regionfilename
func procRegionFilename(pid int, address uint64, buf *byte, buflen int64) int32

View File

@@ -0,0 +1,13 @@
//go:build linux
package oomprofile
import "os"
func (b *profileBuilder) readMapping() {
data, _ := os.ReadFile("/proc/self/maps")
stdParseProcSelfMaps(data, b.addMapping)
if len(b.mem) == 0 {
b.addMappingEntry(0, 0, 0, "", "", true)
}
}

View File

@@ -0,0 +1,58 @@
//go:build windows
package oomprofile
import (
"errors"
"os"
"golang.org/x/sys/windows"
)
func (b *profileBuilder) readMapping() {
snapshot, err := createModuleSnapshot()
if err != nil {
b.addMappingEntry(0, 0, 0, "", "", true)
return
}
defer windows.CloseHandle(snapshot)
var module windows.ModuleEntry32
module.Size = uint32(windows.SizeofModuleEntry32)
err = windows.Module32First(snapshot, &module)
if err != nil {
b.addMappingEntry(0, 0, 0, "", "", true)
return
}
for err == nil {
exe := windows.UTF16ToString(module.ExePath[:])
b.addMappingEntry(
uint64(module.ModBaseAddr),
uint64(module.ModBaseAddr)+uint64(module.ModBaseSize),
0,
exe,
peBuildID(exe),
false,
)
err = windows.Module32Next(snapshot, &module)
}
}
func createModuleSnapshot() (windows.Handle, error) {
for {
snapshot, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPMODULE|windows.TH32CS_SNAPMODULE32, uint32(windows.GetCurrentProcessId()))
var errno windows.Errno
if err != nil && errors.As(err, &errno) && errno == windows.ERROR_BAD_LENGTH {
continue
}
return snapshot, err
}
}
func peBuildID(file string) string {
info, err := os.Stat(file)
if err != nil {
return file
}
return file + info.ModTime().String()
}

View File

@@ -0,0 +1,380 @@
//go:build darwin || linux || windows
package oomprofile
import (
"fmt"
"io"
"math"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"time"
"unsafe"
)
type stackRecord struct {
Stack []uintptr
}
type memProfileRecord struct {
AllocBytes, FreeBytes int64
AllocObjects, FreeObjects int64
Stack []uintptr
}
func (r *memProfileRecord) InUseBytes() int64 {
return r.AllocBytes - r.FreeBytes
}
func (r *memProfileRecord) InUseObjects() int64 {
return r.AllocObjects - r.FreeObjects
}
type blockProfileRecord struct {
Count int64
Cycles int64
Stack []uintptr
}
type label struct {
key string
value string
}
type labelSet struct {
list []label
}
type labelMap struct {
labelSet
}
func WriteFile(destPath string, name string) (string, error) {
writer, ok := profileWriters[name]
if !ok {
return "", fmt.Errorf("unsupported profile %q", name)
}
filePath := filepath.Join(destPath, name+".pb")
file, err := os.Create(filePath)
if err != nil {
return "", err
}
defer file.Close()
if err := writer(file); err != nil {
_ = os.Remove(filePath)
return "", err
}
if err := file.Close(); err != nil {
_ = os.Remove(filePath)
return "", err
}
return filePath, nil
}
var profileWriters = map[string]func(io.Writer) error{
"allocs": writeAlloc,
"block": writeBlock,
"goroutine": writeGoroutine,
"heap": writeHeap,
"mutex": writeMutex,
"threadcreate": writeThreadCreate,
}
func writeHeap(w io.Writer) error {
return writeHeapInternal(w, "")
}
func writeAlloc(w io.Writer) error {
return writeHeapInternal(w, "alloc_space")
}
func writeHeapInternal(w io.Writer, defaultSampleType string) error {
var profile []memProfileRecord
n, ok := runtimeMemProfileInternal(nil, true)
for {
profile = make([]memProfileRecord, n+50)
n, ok = runtimeMemProfileInternal(profile, true)
if ok {
profile = profile[:n]
break
}
}
return writeHeapProto(w, profile, int64(runtime.MemProfileRate), defaultSampleType)
}
func writeGoroutine(w io.Writer) error {
return writeRuntimeProfile(w, "goroutine", runtimeGoroutineProfileWithLabels)
}
func writeThreadCreate(w io.Writer) error {
return writeRuntimeProfile(w, "threadcreate", func(p []stackRecord, _ []unsafe.Pointer) (int, bool) {
return runtimeThreadCreateInternal(p)
})
}
func writeRuntimeProfile(w io.Writer, name string, fetch func([]stackRecord, []unsafe.Pointer) (int, bool)) error {
var profile []stackRecord
var labels []unsafe.Pointer
n, ok := fetch(nil, nil)
for {
profile = make([]stackRecord, n+10)
labels = make([]unsafe.Pointer, n+10)
n, ok = fetch(profile, labels)
if ok {
profile = profile[:n]
labels = labels[:n]
break
}
}
return writeCountProfile(w, name, &runtimeProfile{profile, labels})
}
func writeBlock(w io.Writer) error {
return writeCycleProfile(w, "contentions", "delay", runtimeBlockProfileInternal)
}
func writeMutex(w io.Writer) error {
return writeCycleProfile(w, "contentions", "delay", runtimeMutexProfileInternal)
}
func writeCycleProfile(w io.Writer, countName string, cycleName string, fetch func([]blockProfileRecord) (int, bool)) error {
var profile []blockProfileRecord
n, ok := fetch(nil)
for {
profile = make([]blockProfileRecord, n+50)
n, ok = fetch(profile)
if ok {
profile = profile[:n]
break
}
}
sort.Slice(profile, func(i, j int) bool {
return profile[i].Cycles > profile[j].Cycles
})
builder := newProfileBuilder(w)
builder.pbValueType(tagProfile_PeriodType, countName, "count")
builder.pb.int64Opt(tagProfile_Period, 1)
builder.pbValueType(tagProfile_SampleType, countName, "count")
builder.pbValueType(tagProfile_SampleType, cycleName, "nanoseconds")
cpuGHz := float64(runtimeCyclesPerSecond()) / 1e9
values := []int64{0, 0}
var locs []uint64
expandedStack := runtimeMakeProfStack()
for _, record := range profile {
values[0] = record.Count
if cpuGHz > 0 {
values[1] = int64(float64(record.Cycles) / cpuGHz)
} else {
values[1] = 0
}
n := expandInlinedFrames(expandedStack, record.Stack)
locs = builder.appendLocsForStack(locs[:0], expandedStack[:n])
builder.pbSample(values, locs, nil)
}
return builder.build()
}
type countProfile interface {
Len() int
Stack(i int) []uintptr
Label(i int) *labelMap
}
type runtimeProfile struct {
stk []stackRecord
labels []unsafe.Pointer
}
func (p *runtimeProfile) Len() int {
return len(p.stk)
}
func (p *runtimeProfile) Stack(i int) []uintptr {
return p.stk[i].Stack
}
func (p *runtimeProfile) Label(i int) *labelMap {
return (*labelMap)(p.labels[i])
}
func writeCountProfile(w io.Writer, name string, profile countProfile) error {
var buf strings.Builder
key := func(stk []uintptr, labels *labelMap) string {
buf.Reset()
buf.WriteByte('@')
for _, pc := range stk {
fmt.Fprintf(&buf, " %#x", pc)
}
if labels != nil {
buf.WriteString("\n# labels:")
for _, label := range labels.list {
fmt.Fprintf(&buf, " %q:%q", label.key, label.value)
}
}
return buf.String()
}
counts := make(map[string]int)
index := make(map[string]int)
var keys []string
for i := 0; i < profile.Len(); i++ {
k := key(profile.Stack(i), profile.Label(i))
if counts[k] == 0 {
index[k] = i
keys = append(keys, k)
}
counts[k]++
}
sort.Sort(&keysByCount{keys: keys, count: counts})
builder := newProfileBuilder(w)
builder.pbValueType(tagProfile_PeriodType, name, "count")
builder.pb.int64Opt(tagProfile_Period, 1)
builder.pbValueType(tagProfile_SampleType, name, "count")
values := []int64{0}
var locs []uint64
for _, k := range keys {
values[0] = int64(counts[k])
idx := index[k]
locs = builder.appendLocsForStack(locs[:0], profile.Stack(idx))
var labels func()
if profile.Label(idx) != nil {
labels = func() {
for _, label := range profile.Label(idx).list {
builder.pbLabel(tagSample_Label, label.key, label.value, 0)
}
}
}
builder.pbSample(values, locs, labels)
}
return builder.build()
}
type keysByCount struct {
keys []string
count map[string]int
}
func (x *keysByCount) Len() int {
return len(x.keys)
}
func (x *keysByCount) Swap(i int, j int) {
x.keys[i], x.keys[j] = x.keys[j], x.keys[i]
}
func (x *keysByCount) Less(i int, j int) bool {
ki, kj := x.keys[i], x.keys[j]
ci, cj := x.count[ki], x.count[kj]
if ci != cj {
return ci > cj
}
return ki < kj
}
func expandInlinedFrames(dst []uintptr, pcs []uintptr) int {
frames := runtime.CallersFrames(pcs)
var n int
for n < len(dst) {
frame, more := frames.Next()
dst[n] = frame.PC + 1
n++
if !more {
break
}
}
return n
}
func writeHeapProto(w io.Writer, profile []memProfileRecord, rate int64, defaultSampleType string) error {
builder := newProfileBuilder(w)
builder.pbValueType(tagProfile_PeriodType, "space", "bytes")
builder.pb.int64Opt(tagProfile_Period, rate)
builder.pbValueType(tagProfile_SampleType, "alloc_objects", "count")
builder.pbValueType(tagProfile_SampleType, "alloc_space", "bytes")
builder.pbValueType(tagProfile_SampleType, "inuse_objects", "count")
builder.pbValueType(tagProfile_SampleType, "inuse_space", "bytes")
if defaultSampleType != "" {
builder.pb.int64Opt(tagProfile_DefaultSampleType, builder.stringIndex(defaultSampleType))
}
values := []int64{0, 0, 0, 0}
var locs []uint64
for _, record := range profile {
hideRuntime := true
for tries := 0; tries < 2; tries++ {
stk := record.Stack
if hideRuntime {
for i, addr := range stk {
if f := runtime.FuncForPC(addr); f != nil && (strings.HasPrefix(f.Name(), "runtime.") || strings.HasPrefix(f.Name(), "internal/runtime/")) {
continue
}
stk = stk[i:]
break
}
}
locs = builder.appendLocsForStack(locs[:0], stk)
if len(locs) > 0 {
break
}
hideRuntime = false
}
values[0], values[1] = scaleHeapSample(record.AllocObjects, record.AllocBytes, rate)
values[2], values[3] = scaleHeapSample(record.InUseObjects(), record.InUseBytes(), rate)
var blockSize int64
if record.AllocObjects > 0 {
blockSize = record.AllocBytes / record.AllocObjects
}
builder.pbSample(values, locs, func() {
if blockSize != 0 {
builder.pbLabel(tagSample_Label, "bytes", "", blockSize)
}
})
}
return builder.build()
}
func scaleHeapSample(count int64, size int64, rate int64) (int64, int64) {
if count == 0 || size == 0 {
return 0, 0
}
if rate <= 1 {
return count, size
}
avgSize := float64(size) / float64(count)
scale := 1 / (1 - math.Exp(-avgSize/float64(rate)))
return int64(float64(count) * scale), int64(float64(size) * scale)
}
type profileBuilder struct {
start time.Time
w io.Writer
err error
pb protobuf
strings []string
stringMap map[string]int
locs map[uintptr]locInfo
funcs map[string]int
mem []memMap
deck pcDeck
}

View File

@@ -0,0 +1,120 @@
//go:build darwin || linux || windows
package oomprofile
type protobuf struct {
data []byte
tmp [16]byte
nest int
}
func (b *protobuf) varint(x uint64) {
for x >= 128 {
b.data = append(b.data, byte(x)|0x80)
x >>= 7
}
b.data = append(b.data, byte(x))
}
func (b *protobuf) length(tag int, length int) {
b.varint(uint64(tag)<<3 | 2)
b.varint(uint64(length))
}
func (b *protobuf) uint64(tag int, x uint64) {
b.varint(uint64(tag)<<3 | 0)
b.varint(x)
}
func (b *protobuf) uint64s(tag int, x []uint64) {
if len(x) > 2 {
n1 := len(b.data)
for _, u := range x {
b.varint(u)
}
n2 := len(b.data)
b.length(tag, n2-n1)
n3 := len(b.data)
copy(b.tmp[:], b.data[n2:n3])
copy(b.data[n1+(n3-n2):], b.data[n1:n2])
copy(b.data[n1:], b.tmp[:n3-n2])
return
}
for _, u := range x {
b.uint64(tag, u)
}
}
func (b *protobuf) uint64Opt(tag int, x uint64) {
if x == 0 {
return
}
b.uint64(tag, x)
}
func (b *protobuf) int64(tag int, x int64) {
b.uint64(tag, uint64(x))
}
func (b *protobuf) int64Opt(tag int, x int64) {
if x == 0 {
return
}
b.int64(tag, x)
}
func (b *protobuf) int64s(tag int, x []int64) {
if len(x) > 2 {
n1 := len(b.data)
for _, u := range x {
b.varint(uint64(u))
}
n2 := len(b.data)
b.length(tag, n2-n1)
n3 := len(b.data)
copy(b.tmp[:], b.data[n2:n3])
copy(b.data[n1+(n3-n2):], b.data[n1:n2])
copy(b.data[n1:], b.tmp[:n3-n2])
return
}
for _, u := range x {
b.int64(tag, u)
}
}
func (b *protobuf) bool(tag int, x bool) {
if x {
b.uint64(tag, 1)
} else {
b.uint64(tag, 0)
}
}
func (b *protobuf) string(tag int, x string) {
b.length(tag, len(x))
b.data = append(b.data, x...)
}
func (b *protobuf) strings(tag int, x []string) {
for _, s := range x {
b.string(tag, s)
}
}
type msgOffset int
func (b *protobuf) startMessage() msgOffset {
b.nest++
return msgOffset(len(b.data))
}
func (b *protobuf) endMessage(tag int, start msgOffset) {
n1 := int(start)
n2 := len(b.data)
b.length(tag, n2-n1)
n3 := len(b.data)
copy(b.tmp[:], b.data[n2:n3])
copy(b.data[n1+(n3-n2):], b.data[n1:n2])
copy(b.data[n1:], b.tmp[:n3-n2])
b.nest--
}

View File

@@ -1,24 +1,76 @@
//go:build darwin || linux
//go:build darwin || linux || windows
package libbox
import (
"archive/zip"
"io"
"io/fs"
"os"
"path/filepath"
"runtime"
"runtime/debug"
"time"
)
var crashOutputFile *os.File
func RedirectStderr(path string) error {
if stats, err := os.Stat(path); err == nil && stats.Size() > 0 {
_ = os.Rename(path, path+".old")
type crashReportMetadata struct {
reportMetadata
CrashedAt string `json:"crashedAt,omitempty"`
SignalName string `json:"signalName,omitempty"`
SignalCode string `json:"signalCode,omitempty"`
ExceptionName string `json:"exceptionName,omitempty"`
ExceptionReason string `json:"exceptionReason,omitempty"`
}
func archiveCrashReport(path string, crashReportsDir string) {
content, err := os.ReadFile(path)
if err != nil || len(content) == 0 {
return
}
info, _ := os.Stat(path)
crashTime := time.Now().UTC()
if info != nil {
crashTime = info.ModTime().UTC()
}
initReportDir(crashReportsDir)
destPath, err := nextAvailableReportPath(crashReportsDir, crashTime)
if err != nil {
return
}
initReportDir(destPath)
writeReportFile(destPath, "go.log", content)
metadata := crashReportMetadata{
reportMetadata: baseReportMetadata(),
CrashedAt: crashTime.Format(time.RFC3339),
}
writeReportMetadata(destPath, metadata)
os.Remove(path)
copyConfigSnapshot(destPath)
}
func configSnapshotPath() string {
return filepath.Join(sBasePath, "configuration.json")
}
func saveConfigSnapshot(configContent string) {
snapshotPath := configSnapshotPath()
os.WriteFile(snapshotPath, []byte(configContent), 0o666)
chownReport(snapshotPath)
}
func redirectStderr(path string) error {
crashReportsDir := filepath.Join(sWorkingPath, "crash_reports")
archiveCrashReport(path, crashReportsDir)
archiveCrashReport(path+".old", crashReportsDir)
outputFile, err := os.Create(path)
if err != nil {
return err
}
if runtime.GOOS != "android" {
if runtime.GOOS != "android" && runtime.GOOS != "windows" {
err = outputFile.Chown(sUserID, sGroupID)
if err != nil {
outputFile.Close()
@@ -26,12 +78,88 @@ func RedirectStderr(path string) error {
return err
}
}
err = debug.SetCrashOutput(outputFile, debug.CrashOptions{})
if err != nil {
outputFile.Close()
os.Remove(outputFile.Name())
return err
}
crashOutputFile = outputFile
_ = outputFile.Close()
return nil
}
func CreateZipArchive(sourcePath string, destinationPath string) error {
sourceInfo, err := os.Stat(sourcePath)
if err != nil {
return err
}
if !sourceInfo.IsDir() {
return os.ErrInvalid
}
destinationFile, err := os.Create(destinationPath)
if err != nil {
return err
}
defer func() {
_ = destinationFile.Close()
}()
zipWriter := zip.NewWriter(destinationFile)
rootName := filepath.Base(sourcePath)
err = filepath.WalkDir(sourcePath, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
relativePath, err := filepath.Rel(sourcePath, path)
if err != nil {
return err
}
if relativePath == "." {
return nil
}
archivePath := filepath.ToSlash(filepath.Join(rootName, relativePath))
if d.IsDir() {
_, err = zipWriter.Create(archivePath + "/")
return err
}
fileInfo, err := d.Info()
if err != nil {
return err
}
header, err := zip.FileInfoHeader(fileInfo)
if err != nil {
return err
}
header.Name = archivePath
header.Method = zip.Deflate
writer, err := zipWriter.CreateHeader(header)
if err != nil {
return err
}
sourceFile, err := os.Open(path)
if err != nil {
return err
}
_, err = io.Copy(writer, sourceFile)
closeErr := sourceFile.Close()
if err != nil {
return err
}
return closeErr
})
if err != nil {
_ = zipWriter.Close()
return err
}
return zipWriter.Close()
}

View File

@@ -1,26 +0,0 @@
package libbox
import (
"math"
runtimeDebug "runtime/debug"
C "github.com/sagernet/sing-box/constant"
)
var memoryLimitEnabled bool
func SetMemoryLimit(enabled bool) {
memoryLimitEnabled = enabled
const memoryLimitGo = 45 * 1024 * 1024
if enabled {
runtimeDebug.SetGCPercent(10)
if C.IsIos {
runtimeDebug.SetMemoryLimit(memoryLimitGo)
}
} else {
runtimeDebug.SetGCPercent(100)
if C.IsIos {
runtimeDebug.SetMemoryLimit(math.MaxInt64)
}
}
}

View File

@@ -0,0 +1,141 @@
//go:build darwin || linux || windows
package libbox
import (
"os"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/sagernet/sing-box/experimental/libbox/internal/oomprofile"
"github.com/sagernet/sing-box/service/oomkiller"
"github.com/sagernet/sing/common/byteformats"
"github.com/sagernet/sing/common/memory"
)
func init() {
sOOMReporter = &oomReporter{}
}
var oomReportProfiles = []string{
"allocs",
"block",
"goroutine",
"heap",
"mutex",
"threadcreate",
}
type oomReportMetadata struct {
reportMetadata
RecordedAt string `json:"recordedAt"`
MemoryUsage string `json:"memoryUsage"`
AvailableMemory string `json:"availableMemory,omitempty"`
// Heap
HeapAlloc string `json:"heapAlloc,omitempty"`
HeapObjects uint64 `json:"heapObjects,omitempty,string"`
HeapInuse string `json:"heapInuse,omitempty"`
HeapIdle string `json:"heapIdle,omitempty"`
HeapReleased string `json:"heapReleased,omitempty"`
HeapSys string `json:"heapSys,omitempty"`
// Stack
StackInuse string `json:"stackInuse,omitempty"`
StackSys string `json:"stackSys,omitempty"`
// Runtime metadata
MSpanInuse string `json:"mSpanInuse,omitempty"`
MSpanSys string `json:"mSpanSys,omitempty"`
MCacheSys string `json:"mCacheSys,omitempty"`
BuckHashSys string `json:"buckHashSys,omitempty"`
GCSys string `json:"gcSys,omitempty"`
OtherSys string `json:"otherSys,omitempty"`
Sys string `json:"sys,omitempty"`
// GC & runtime
TotalAlloc string `json:"totalAlloc,omitempty"`
NumGC uint32 `json:"numGC,omitempty,string"`
NumGoroutine int `json:"numGoroutine,omitempty,string"`
NextGC string `json:"nextGC,omitempty"`
LastGC string `json:"lastGC,omitempty"`
}
type oomReporter struct{}
var _ oomkiller.OOMReporter = (*oomReporter)(nil)
func (r *oomReporter) WriteReport(memoryUsage uint64) error {
now := time.Now().UTC()
reportsDir := filepath.Join(sWorkingPath, "oom_reports")
err := os.MkdirAll(reportsDir, 0o777)
if err != nil {
return err
}
chownReport(reportsDir)
destPath, err := nextAvailableReportPath(reportsDir, now)
if err != nil {
return err
}
err = os.MkdirAll(destPath, 0o777)
if err != nil {
return err
}
chownReport(destPath)
for _, name := range oomReportProfiles {
writeOOMProfile(destPath, name)
}
writeReportFile(destPath, "cmdline", []byte(strings.Join(os.Args, "\000")))
var memStats runtime.MemStats
runtime.ReadMemStats(&memStats)
metadata := oomReportMetadata{
reportMetadata: baseReportMetadata(),
RecordedAt: now.Format(time.RFC3339),
MemoryUsage: byteformats.FormatMemoryBytes(memoryUsage),
// Heap
HeapAlloc: byteformats.FormatMemoryBytes(memStats.HeapAlloc),
HeapObjects: memStats.HeapObjects,
HeapInuse: byteformats.FormatMemoryBytes(memStats.HeapInuse),
HeapIdle: byteformats.FormatMemoryBytes(memStats.HeapIdle),
HeapReleased: byteformats.FormatMemoryBytes(memStats.HeapReleased),
HeapSys: byteformats.FormatMemoryBytes(memStats.HeapSys),
// Stack
StackInuse: byteformats.FormatMemoryBytes(memStats.StackInuse),
StackSys: byteformats.FormatMemoryBytes(memStats.StackSys),
// Runtime metadata
MSpanInuse: byteformats.FormatMemoryBytes(memStats.MSpanInuse),
MSpanSys: byteformats.FormatMemoryBytes(memStats.MSpanSys),
MCacheSys: byteformats.FormatMemoryBytes(memStats.MCacheSys),
BuckHashSys: byteformats.FormatMemoryBytes(memStats.BuckHashSys),
GCSys: byteformats.FormatMemoryBytes(memStats.GCSys),
OtherSys: byteformats.FormatMemoryBytes(memStats.OtherSys),
Sys: byteformats.FormatMemoryBytes(memStats.Sys),
// GC & runtime
TotalAlloc: byteformats.FormatMemoryBytes(memStats.TotalAlloc),
NumGC: memStats.NumGC,
NumGoroutine: runtime.NumGoroutine(),
NextGC: byteformats.FormatMemoryBytes(memStats.NextGC),
}
if memStats.LastGC > 0 {
metadata.LastGC = time.Unix(0, int64(memStats.LastGC)).UTC().Format(time.RFC3339)
}
availableMemory := memory.Available()
if availableMemory > 0 {
metadata.AvailableMemory = byteformats.FormatMemoryBytes(availableMemory)
}
writeReportMetadata(destPath, metadata)
copyConfigSnapshot(destPath)
return nil
}
func writeOOMProfile(destPath string, name string) {
filePath, err := oomprofile.WriteFile(destPath, name)
if err != nil {
return
}
chownReport(filePath)
}

View File

@@ -0,0 +1,97 @@
//go:build darwin || linux || windows
package libbox
import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"runtime"
"strconv"
"time"
C "github.com/sagernet/sing-box/constant"
E "github.com/sagernet/sing/common/exceptions"
)
type reportMetadata struct {
Source string `json:"source,omitempty"`
BundleIdentifier string `json:"bundleIdentifier,omitempty"`
ProcessName string `json:"processName,omitempty"`
ProcessPath string `json:"processPath,omitempty"`
StartedAt string `json:"startedAt,omitempty"`
AppVersion string `json:"appVersion,omitempty"`
AppMarketingVersion string `json:"appMarketingVersion,omitempty"`
CoreVersion string `json:"coreVersion,omitempty"`
GoVersion string `json:"goVersion,omitempty"`
}
func baseReportMetadata() reportMetadata {
processPath, _ := os.Executable()
processName := filepath.Base(processPath)
if processName == "." {
processName = ""
}
return reportMetadata{
Source: sCrashReportSource,
ProcessName: processName,
ProcessPath: processPath,
CoreVersion: C.Version,
GoVersion: GoVersion(),
}
}
func writeReportFile(destPath string, name string, content []byte) {
filePath := filepath.Join(destPath, name)
os.WriteFile(filePath, content, 0o666)
chownReport(filePath)
}
func writeReportMetadata(destPath string, metadata any) {
data, err := json.Marshal(metadata)
if err != nil {
return
}
writeReportFile(destPath, "metadata.json", data)
}
func copyConfigSnapshot(destPath string) {
snapshotPath := configSnapshotPath()
content, err := os.ReadFile(snapshotPath)
if err != nil {
return
}
if len(bytes.TrimSpace(content)) == 0 {
return
}
writeReportFile(destPath, "configuration.json", content)
}
func initReportDir(path string) {
os.MkdirAll(path, 0o777)
chownReport(path)
}
func chownReport(path string) {
if runtime.GOOS != "android" && runtime.GOOS != "windows" {
os.Chown(path, sUserID, sGroupID)
}
}
func nextAvailableReportPath(reportsDir string, timestamp time.Time) (string, error) {
destName := timestamp.Format("2006-01-02T15-04-05")
destPath := filepath.Join(reportsDir, destName)
_, err := os.Stat(destPath)
if os.IsNotExist(err) {
return destPath, nil
}
for i := 1; i <= 1000; i++ {
suffixedPath := filepath.Join(reportsDir, destName+"-"+strconv.Itoa(i))
_, err = os.Stat(suffixedPath)
if os.IsNotExist(err) {
return suffixedPath, nil
}
}
return "", E.New("no available report path for ", destName)
}

View File

@@ -1,13 +1,17 @@
package libbox
import (
"math"
"os"
"path/filepath"
"runtime"
"runtime/debug"
"time"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/locale"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/service/oomkiller"
"github.com/sagernet/sing/common/byteformats"
)
@@ -22,6 +26,10 @@ var (
sCommandServerSecret string
sLogMaxLines int
sDebug bool
sCrashReportSource string
sOOMKillerEnabled bool
sOOMKillerDisabled bool
sOOMMemoryLimit int64
)
func init() {
@@ -38,9 +46,13 @@ type SetupOptions struct {
CommandServerSecret string
LogMaxLines int
Debug bool
CrashReportSource string
OomKillerEnabled bool
OomKillerDisabled bool
OomMemoryLimit int64
}
func Setup(options *SetupOptions) error {
func applySetupOptions(options *SetupOptions) {
sBasePath = options.BasePath
sWorkingPath = options.WorkingPath
sTempPath = options.TempPath
@@ -56,10 +68,33 @@ func Setup(options *SetupOptions) error {
sCommandServerSecret = options.CommandServerSecret
sLogMaxLines = options.LogMaxLines
sDebug = options.Debug
sCrashReportSource = options.CrashReportSource
ReloadSetupOptions(options)
}
func ReloadSetupOptions(options *SetupOptions) {
sOOMKillerEnabled = options.OomKillerEnabled
sOOMKillerDisabled = options.OomKillerDisabled
sOOMMemoryLimit = options.OomMemoryLimit
if sOOMKillerEnabled {
if sOOMMemoryLimit == 0 && C.IsIos {
sOOMMemoryLimit = oomkiller.DefaultAppleNetworkExtensionMemoryLimit
}
if sOOMMemoryLimit > 0 {
debug.SetMemoryLimit(sOOMMemoryLimit * 3 / 4)
} else {
debug.SetMemoryLimit(math.MaxInt64)
}
} else {
debug.SetMemoryLimit(math.MaxInt64)
}
}
func Setup(options *SetupOptions) error {
applySetupOptions(options)
os.MkdirAll(sWorkingPath, 0o777)
os.MkdirAll(sTempPath, 0o777)
return nil
return redirectStderr(filepath.Join(sWorkingPath, "CrashReport-"+sCrashReportSource+".log"))
}
func SetLocale(localeId string) {
@@ -70,6 +105,10 @@ func Version() string {
return C.Version
}
func GoVersion() string {
return runtime.Version() + ", " + runtime.GOOS + "/" + runtime.GOARCH
}
func FormatBytes(length int64) string {
return byteformats.FormatKBytes(uint64(length))
}

2
go.mod
View File

@@ -37,7 +37,7 @@ require (
github.com/sagernet/gomobile v0.1.12
github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4
github.com/sagernet/sing v0.8.4
github.com/sagernet/sing v0.8.5-0.20260404181712-947827ec3849
github.com/sagernet/sing-mux v0.3.4
github.com/sagernet/sing-quic v0.6.2-0.20260330152607-bf674c163212
github.com/sagernet/sing-shadowsocks v0.2.8

4
go.sum
View File

@@ -236,8 +236,8 @@ github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNen
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o=
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4=
github.com/sagernet/sing v0.8.4 h1:Fj+jlY3F8vhcRfz/G/P3Dwcs5wqnmyNPT7u1RVVmjFI=
github.com/sagernet/sing v0.8.4/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing v0.8.5-0.20260404181712-947827ec3849 h1:P8jaGN561IbHBxjlU8IGrFK65n1vDOrHo8FOMgHfn14=
github.com/sagernet/sing v0.8.5-0.20260404181712-947827ec3849/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s=
github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk=
github.com/sagernet/sing-quic v0.6.2-0.20260330152607-bf674c163212 h1:7mFOUqy+DyOj7qKGd1X54UMXbnbJiiMileK/tn17xYc=

View File

@@ -10,5 +10,6 @@ type OOMKillerServiceOptions struct {
SafetyMargin *byteformats.MemoryBytes `json:"safety_margin,omitempty"`
MinInterval badoption.Duration `json:"min_interval,omitempty"`
MaxInterval badoption.Duration `json:"max_interval,omitempty"`
ChecksBeforeLimit int `json:"checks_before_limit,omitempty"`
KillerDisabled bool `json:"-"`
MemoryLimitOverride uint64 `json:"-"`
}

View File

@@ -1,51 +0,0 @@
package oomkiller
import (
"time"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
)
func buildTimerConfig(options option.OOMKillerServiceOptions, memoryLimit uint64, useAvailable bool) (timerConfig, error) {
safetyMargin := uint64(defaultSafetyMargin)
if options.SafetyMargin != nil && options.SafetyMargin.Value() > 0 {
safetyMargin = options.SafetyMargin.Value()
}
minInterval := defaultMinInterval
if options.MinInterval != 0 {
minInterval = time.Duration(options.MinInterval.Build())
if minInterval <= 0 {
return timerConfig{}, E.New("min_interval must be greater than 0")
}
}
maxInterval := defaultMaxInterval
if options.MaxInterval != 0 {
maxInterval = time.Duration(options.MaxInterval.Build())
if maxInterval <= 0 {
return timerConfig{}, E.New("max_interval must be greater than 0")
}
}
if maxInterval < minInterval {
return timerConfig{}, E.New("max_interval must be greater than or equal to min_interval")
}
checksBeforeLimit := defaultChecksBeforeLimit
if options.ChecksBeforeLimit != 0 {
checksBeforeLimit = options.ChecksBeforeLimit
if checksBeforeLimit <= 0 {
return timerConfig{}, E.New("checks_before_limit must be greater than 0")
}
}
return timerConfig{
memoryLimit: memoryLimit,
safetyMargin: safetyMargin,
minInterval: minInterval,
maxInterval: maxInterval,
checksBeforeLimit: checksBeforeLimit,
useAvailable: useAvailable,
}, nil
}

View File

@@ -0,0 +1,46 @@
package oomkiller
import (
"context"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/memory"
"github.com/sagernet/sing/service"
)
const DefaultAppleNetworkExtensionMemoryLimit = 50 * 1024 * 1024
type policyMode uint8
const (
policyModeNone policyMode = iota
policyModeMemoryLimit
policyModeAvailable
policyModeNetworkExtension
)
func (m policyMode) hasTimerMode() bool {
return m != policyModeNone
}
func resolvePolicyMode(ctx context.Context, options option.OOMKillerServiceOptions) (uint64, policyMode) {
platformInterface := service.FromContext[adapter.PlatformInterface](ctx)
if C.IsIos && platformInterface != nil && platformInterface.UnderNetworkExtension() {
return DefaultAppleNetworkExtensionMemoryLimit, policyModeNetworkExtension
}
if options.MemoryLimitOverride > 0 {
return options.MemoryLimitOverride, policyModeMemoryLimit
}
if options.MemoryLimit != nil {
memoryLimit := options.MemoryLimit.Value()
if memoryLimit > 0 {
return memoryLimit, policyModeMemoryLimit
}
}
if memory.AvailableAvailable() {
return 0, policyModeAvailable
}
return 0, policyModeNone
}

View File

@@ -1,192 +1,83 @@
//go:build darwin && cgo
package oomkiller
/*
#include <dispatch/dispatch.h>
static dispatch_source_t memoryPressureSource;
extern void goMemoryPressureCallback(unsigned long status);
static void startMemoryPressureMonitor() {
memoryPressureSource = dispatch_source_create(
DISPATCH_SOURCE_TYPE_MEMORYPRESSURE,
0,
DISPATCH_MEMORYPRESSURE_WARN | DISPATCH_MEMORYPRESSURE_CRITICAL,
dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0)
);
dispatch_source_set_event_handler(memoryPressureSource, ^{
unsigned long status = dispatch_source_get_data(memoryPressureSource);
goMemoryPressureCallback(status);
});
dispatch_activate(memoryPressureSource);
}
static void stopMemoryPressureMonitor() {
if (memoryPressureSource) {
dispatch_source_cancel(memoryPressureSource);
memoryPressureSource = NULL;
}
}
*/
import "C"
import (
"context"
runtimeDebug "runtime/debug"
"sync"
"sync/atomic"
"time"
"github.com/sagernet/sing-box/adapter"
boxService "github.com/sagernet/sing-box/adapter/service"
boxConstant "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/memory"
"github.com/sagernet/sing/service"
)
type OOMReporter interface {
WriteReport(memoryUsage uint64) error
}
func RegisterService(registry *boxService.Registry) {
boxService.Register[option.OOMKillerServiceOptions](registry, boxConstant.TypeOOMKiller, NewService)
}
var (
globalAccess sync.Mutex
globalServices []*Service
)
type Service struct {
boxService.Adapter
ctx context.Context
logger log.ContextLogger
router adapter.Router
memoryLimit uint64
hasTimerMode bool
useAvailable bool
timerConfig timerConfig
adaptiveTimer *adaptiveTimer
lastReportTime atomic.Int64
}
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.OOMKillerServiceOptions) (adapter.Service, error) {
s := &Service{
Adapter: boxService.NewAdapter(boxConstant.TypeOOMKiller, tag),
logger: logger,
router: service.FromContext[adapter.Router](ctx),
}
if options.MemoryLimit != nil {
s.memoryLimit = options.MemoryLimit.Value()
if s.memoryLimit > 0 {
s.hasTimerMode = true
}
}
config, err := buildTimerConfig(options, s.memoryLimit, s.useAvailable)
memoryLimit, mode := resolvePolicyMode(ctx, options)
config, err := buildTimerConfig(options, memoryLimit, mode, options.KillerDisabled)
if err != nil {
return nil, err
}
s.timerConfig = config
return s, nil
return &Service{
Adapter: boxService.NewAdapter(boxConstant.TypeOOMKiller, tag),
ctx: ctx,
logger: logger,
router: service.FromContext[adapter.Router](ctx),
timerConfig: config,
}, nil
}
func (s *Service) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
func (s *Service) createTimer() {
s.adaptiveTimer = newAdaptiveTimer(s.logger, s.router, s.timerConfig, s.writeOOMReport)
}
if s.hasTimerMode {
s.adaptiveTimer = newAdaptiveTimer(s.logger, s.router, s.timerConfig)
if s.memoryLimit > 0 {
s.logger.Info("started memory monitor with limit: ", s.memoryLimit/(1024*1024), " MiB")
} else {
s.logger.Info("started memory monitor with available memory detection")
}
} else {
s.logger.Info("started memory pressure monitor")
func (s *Service) startTimer() {
s.createTimer()
s.adaptiveTimer.start()
}
globalAccess.Lock()
isFirst := len(globalServices) == 0
globalServices = append(globalServices, s)
globalAccess.Unlock()
if isFirst {
C.startMemoryPressureMonitor()
}
return nil
}
func (s *Service) Close() error {
func (s *Service) stopTimer() {
if s.adaptiveTimer != nil {
s.adaptiveTimer.stop()
}
globalAccess.Lock()
for i, svc := range globalServices {
if svc == s {
globalServices = append(globalServices[:i], globalServices[i+1:]...)
break
}
}
isLast := len(globalServices) == 0
globalAccess.Unlock()
if isLast {
C.stopMemoryPressureMonitor()
}
return nil
}
//export goMemoryPressureCallback
func goMemoryPressureCallback(status C.ulong) {
globalAccess.Lock()
services := make([]*Service, len(globalServices))
copy(services, globalServices)
globalAccess.Unlock()
if len(services) == 0 {
func (s *Service) writeOOMReport(memoryUsage uint64) {
now := time.Now().Unix()
lastReport := s.lastReportTime.Load()
if now-lastReport < 3600 {
return
}
criticalFlag := C.ulong(C.DISPATCH_MEMORYPRESSURE_CRITICAL)
warnFlag := C.ulong(C.DISPATCH_MEMORYPRESSURE_WARN)
isCritical := status&criticalFlag != 0
isWarning := status&warnFlag != 0
var level string
switch {
case isCritical:
level = "critical"
case isWarning:
level = "warning"
default:
level = "normal"
if !s.lastReportTime.CompareAndSwap(lastReport, now) {
return
}
var freeOSMemory bool
for _, s := range services {
usage := memory.Total()
if s.hasTimerMode {
if isCritical {
s.logger.Warn("memory pressure: ", level, ", usage: ", usage/(1024*1024), " MiB")
if s.adaptiveTimer != nil {
s.adaptiveTimer.startNow()
reporter := service.FromContext[OOMReporter](s.ctx)
if reporter == nil {
return
}
} else if isWarning {
s.logger.Warn("memory pressure: ", level, ", usage: ", usage/(1024*1024), " MiB")
err := reporter.WriteReport(memoryUsage)
if err != nil {
s.logger.Warn("failed to write OOM report: ", err)
} else {
s.logger.Debug("memory pressure: ", level, ", usage: ", usage/(1024*1024), " MiB")
if s.adaptiveTimer != nil {
s.adaptiveTimer.stop()
}
}
} else {
if isCritical {
s.logger.Error("memory pressure: ", level, ", usage: ", usage/(1024*1024), " MiB, resetting network")
s.router.ResetNetwork()
freeOSMemory = true
} else if isWarning {
s.logger.Warn("memory pressure: ", level, ", usage: ", usage/(1024*1024), " MiB")
} else {
s.logger.Debug("memory pressure: ", level, ", usage: ", usage/(1024*1024), " MiB")
}
}
}
if freeOSMemory {
runtimeDebug.FreeOSMemory()
s.logger.Info("OOM report saved")
}
}

View File

@@ -0,0 +1,103 @@
//go:build darwin && cgo
package oomkiller
/*
#include <dispatch/dispatch.h>
static dispatch_source_t memoryPressureSource;
extern void goMemoryPressureCallback(unsigned long status);
static void startMemoryPressureMonitor() {
memoryPressureSource = dispatch_source_create(
DISPATCH_SOURCE_TYPE_MEMORYPRESSURE,
0,
DISPATCH_MEMORYPRESSURE_CRITICAL,
dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0)
);
dispatch_source_set_event_handler(memoryPressureSource, ^{
unsigned long status = dispatch_source_get_data(memoryPressureSource);
goMemoryPressureCallback(status);
});
dispatch_activate(memoryPressureSource);
}
static void stopMemoryPressureMonitor() {
if (memoryPressureSource) {
dispatch_source_cancel(memoryPressureSource);
memoryPressureSource = NULL;
}
}
*/
import "C"
import (
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common/byteformats"
E "github.com/sagernet/sing/common/exceptions"
)
var (
globalAccess sync.Mutex
globalServices []*Service
)
func (s *Service) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
if s.timerConfig.policyMode == policyModeNetworkExtension {
s.createTimer()
globalAccess.Lock()
isFirst := len(globalServices) == 0
globalServices = append(globalServices, s)
globalAccess.Unlock()
if isFirst {
C.startMemoryPressureMonitor()
}
return nil
}
if !s.timerConfig.policyMode.hasTimerMode() {
return E.New("memory pressure monitoring is not available on this platform without memory_limit")
}
s.startTimer()
return nil
}
func (s *Service) Close() error {
s.stopTimer()
if s.timerConfig.policyMode == policyModeNetworkExtension {
globalAccess.Lock()
for i, svc := range globalServices {
if svc == s {
globalServices = append(globalServices[:i], globalServices[i+1:]...)
break
}
}
isLast := len(globalServices) == 0
globalAccess.Unlock()
if isLast {
C.stopMemoryPressureMonitor()
}
}
return nil
}
//export goMemoryPressureCallback
func goMemoryPressureCallback(status C.ulong) {
globalAccess.Lock()
services := make([]*Service, len(globalServices))
copy(services, globalServices)
globalAccess.Unlock()
if len(services) == 0 {
return
}
sample := readMemorySample(policyModeNetworkExtension)
for _, s := range services {
s.logger.Warn("memory pressure: critical, usage: ", byteformats.FormatMemoryBytes(sample.usage))
s.adaptiveTimer.notifyPressure()
}
}

View File

@@ -3,79 +3,22 @@
package oomkiller
import (
"context"
"github.com/sagernet/sing-box/adapter"
boxService "github.com/sagernet/sing-box/adapter/service"
boxConstant "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/memory"
"github.com/sagernet/sing/service"
)
func RegisterService(registry *boxService.Registry) {
boxService.Register[option.OOMKillerServiceOptions](registry, boxConstant.TypeOOMKiller, NewService)
}
type Service struct {
boxService.Adapter
logger log.ContextLogger
router adapter.Router
adaptiveTimer *adaptiveTimer
timerConfig timerConfig
hasTimerMode bool
useAvailable bool
memoryLimit uint64
}
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.OOMKillerServiceOptions) (adapter.Service, error) {
s := &Service{
Adapter: boxService.NewAdapter(boxConstant.TypeOOMKiller, tag),
logger: logger,
router: service.FromContext[adapter.Router](ctx),
}
if options.MemoryLimit != nil {
s.memoryLimit = options.MemoryLimit.Value()
}
if s.memoryLimit > 0 {
s.hasTimerMode = true
} else if memory.AvailableSupported() {
s.useAvailable = true
s.hasTimerMode = true
}
config, err := buildTimerConfig(options, s.memoryLimit, s.useAvailable)
if err != nil {
return nil, err
}
s.timerConfig = config
return s, nil
}
func (s *Service) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
if !s.hasTimerMode {
if !s.timerConfig.policyMode.hasTimerMode() {
return E.New("memory pressure monitoring is not available on this platform without memory_limit")
}
s.adaptiveTimer = newAdaptiveTimer(s.logger, s.router, s.timerConfig)
s.adaptiveTimer.start(0)
if s.useAvailable {
s.logger.Info("started memory monitor with available memory detection")
} else {
s.logger.Info("started memory monitor with limit: ", s.memoryLimit/(1024*1024), " MiB")
}
s.startTimer()
return nil
}
func (s *Service) Close() error {
if s.adaptiveTimer != nil {
s.adaptiveTimer.stop()
}
s.stopTimer()
return nil
}

View File

@@ -1,158 +0,0 @@
package oomkiller
import (
runtimeDebug "runtime/debug"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common/memory"
)
const (
defaultChecksBeforeLimit = 4
defaultMinInterval = 500 * time.Millisecond
defaultMaxInterval = 10 * time.Second
defaultSafetyMargin = 5 * 1024 * 1024
)
type adaptiveTimer struct {
logger log.ContextLogger
router adapter.Router
memoryLimit uint64
safetyMargin uint64
minInterval time.Duration
maxInterval time.Duration
checksBeforeLimit int
useAvailable bool
access sync.Mutex
timer *time.Timer
previousUsage uint64
lastInterval time.Duration
}
type timerConfig struct {
memoryLimit uint64
safetyMargin uint64
minInterval time.Duration
maxInterval time.Duration
checksBeforeLimit int
useAvailable bool
}
func newAdaptiveTimer(logger log.ContextLogger, router adapter.Router, config timerConfig) *adaptiveTimer {
return &adaptiveTimer{
logger: logger,
router: router,
memoryLimit: config.memoryLimit,
safetyMargin: config.safetyMargin,
minInterval: config.minInterval,
maxInterval: config.maxInterval,
checksBeforeLimit: config.checksBeforeLimit,
useAvailable: config.useAvailable,
}
}
func (t *adaptiveTimer) start(_ uint64) {
t.access.Lock()
defer t.access.Unlock()
t.startLocked()
}
func (t *adaptiveTimer) startNow() {
t.access.Lock()
t.startLocked()
t.access.Unlock()
t.poll()
}
func (t *adaptiveTimer) startLocked() {
if t.timer != nil {
return
}
t.previousUsage = memory.Total()
t.lastInterval = t.minInterval
t.timer = time.AfterFunc(t.minInterval, t.poll)
}
func (t *adaptiveTimer) stop() {
t.access.Lock()
defer t.access.Unlock()
t.stopLocked()
}
func (t *adaptiveTimer) stopLocked() {
if t.timer != nil {
t.timer.Stop()
t.timer = nil
}
}
func (t *adaptiveTimer) running() bool {
t.access.Lock()
defer t.access.Unlock()
return t.timer != nil
}
func (t *adaptiveTimer) poll() {
t.access.Lock()
defer t.access.Unlock()
if t.timer == nil {
return
}
usage := memory.Total()
delta := int64(usage) - int64(t.previousUsage)
t.previousUsage = usage
var remaining uint64
var triggered bool
if t.memoryLimit > 0 {
if usage >= t.memoryLimit {
remaining = 0
triggered = true
} else {
remaining = t.memoryLimit - usage
}
} else if t.useAvailable {
available := memory.Available()
if available <= t.safetyMargin {
remaining = 0
triggered = true
} else {
remaining = available - t.safetyMargin
}
} else {
remaining = 0
}
if triggered {
t.logger.Error("memory threshold reached, usage: ", usage/(1024*1024), " MiB, resetting network")
t.router.ResetNetwork()
runtimeDebug.FreeOSMemory()
}
var interval time.Duration
if triggered {
interval = t.maxInterval
} else if delta <= 0 {
interval = t.maxInterval
} else if t.checksBeforeLimit <= 0 {
interval = t.maxInterval
} else {
timeToLimit := time.Duration(float64(remaining) / float64(delta) * float64(t.lastInterval))
interval = timeToLimit / time.Duration(t.checksBeforeLimit)
if interval < t.minInterval {
interval = t.minInterval
}
if interval > t.maxInterval {
interval = t.maxInterval
}
}
t.lastInterval = interval
t.timer.Reset(interval)
}

325
service/oomkiller/timer.go Normal file
View File

@@ -0,0 +1,325 @@
package oomkiller
import (
runtimeDebug "runtime/debug"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/byteformats"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/memory"
)
const (
defaultMinInterval = 100 * time.Millisecond
defaultArmedInterval = time.Second
defaultMaxInterval = 10 * time.Second
defaultSafetyMargin = 5 * 1024 * 1024
defaultAvailableTriggerMarginMin = 32 * 1024 * 1024
defaultAvailableTriggerMarginMax = 128 * 1024 * 1024
)
type pressureState uint8
const (
pressureStateNormal pressureState = iota
pressureStateArmed
pressureStateTriggered
)
type memorySample struct {
usage uint64
available uint64
availableKnown bool
}
type pressureThresholds struct {
trigger uint64
armed uint64
resume uint64
}
type timerConfig struct {
memoryLimit uint64
safetyMargin uint64
hasSafetyMargin bool
minInterval time.Duration
armedInterval time.Duration
maxInterval time.Duration
policyMode policyMode
killerDisabled bool
}
func buildTimerConfig(options option.OOMKillerServiceOptions, memoryLimit uint64, policyMode policyMode, killerDisabled bool) (timerConfig, error) {
minInterval := defaultMinInterval
if options.MinInterval != 0 {
minInterval = time.Duration(options.MinInterval.Build())
if minInterval <= 0 {
return timerConfig{}, E.New("min_interval must be greater than 0")
}
}
maxInterval := defaultMaxInterval
if options.MaxInterval != 0 {
maxInterval = time.Duration(options.MaxInterval.Build())
if maxInterval <= 0 {
return timerConfig{}, E.New("max_interval must be greater than 0")
}
}
if maxInterval < minInterval {
return timerConfig{}, E.New("max_interval must be greater than or equal to min_interval")
}
var (
safetyMargin uint64
hasSafetyMargin bool
)
if options.SafetyMargin != nil && options.SafetyMargin.Value() > 0 {
safetyMargin = options.SafetyMargin.Value()
hasSafetyMargin = true
} else if memoryLimit > 0 {
safetyMargin = defaultSafetyMargin
hasSafetyMargin = true
}
return timerConfig{
memoryLimit: memoryLimit,
safetyMargin: safetyMargin,
hasSafetyMargin: hasSafetyMargin,
minInterval: minInterval,
armedInterval: max(min(defaultArmedInterval, maxInterval), minInterval),
maxInterval: maxInterval,
policyMode: policyMode,
killerDisabled: killerDisabled,
}, nil
}
type adaptiveTimer struct {
timerConfig
logger log.ContextLogger
router adapter.Router
onTriggered func(uint64)
limitThresholds pressureThresholds
access sync.Mutex
timer *time.Timer
state pressureState
forceMinInterval bool
pendingPressureBaseline bool
pressureBaseline memorySample
pressureBaselineTime time.Time
}
func newAdaptiveTimer(logger log.ContextLogger, router adapter.Router, config timerConfig, onTriggered func(uint64)) *adaptiveTimer {
t := &adaptiveTimer{
timerConfig: config,
logger: logger,
router: router,
onTriggered: onTriggered,
}
if config.policyMode == policyModeMemoryLimit || config.policyMode == policyModeNetworkExtension {
t.limitThresholds = computeLimitThresholds(config.memoryLimit, config.safetyMargin)
}
return t
}
func (t *adaptiveTimer) start() {
t.access.Lock()
defer t.access.Unlock()
t.startLocked()
}
func (t *adaptiveTimer) notifyPressure() {
t.access.Lock()
t.startLocked()
t.forceMinInterval = true
t.pendingPressureBaseline = true
t.access.Unlock()
t.poll()
}
func (t *adaptiveTimer) startLocked() {
if t.timer != nil {
return
}
t.state = pressureStateNormal
t.forceMinInterval = false
t.timer = time.AfterFunc(t.minInterval, t.poll)
}
func (t *adaptiveTimer) stop() {
t.access.Lock()
defer t.access.Unlock()
if t.timer != nil {
t.timer.Stop()
t.timer = nil
}
}
func (t *adaptiveTimer) poll() {
var triggered bool
var rateTriggered bool
sample := readMemorySample(t.policyMode)
t.access.Lock()
if t.timer == nil {
t.access.Unlock()
return
}
if t.pendingPressureBaseline {
t.pressureBaseline = sample
t.pressureBaselineTime = time.Now()
t.pendingPressureBaseline = false
}
previousState := t.state
t.state = t.nextState(sample)
if t.state == pressureStateNormal {
t.forceMinInterval = false
t.pressureBaselineTime = time.Time{}
}
t.timer.Reset(t.intervalForState())
triggered = previousState != pressureStateTriggered && t.state == pressureStateTriggered
if !triggered && !t.pressureBaselineTime.IsZero() && t.memoryLimit > 0 &&
sample.usage > t.pressureBaseline.usage && sample.usage < t.memoryLimit {
elapsed := time.Since(t.pressureBaselineTime)
if elapsed >= t.minInterval/2 {
growth := sample.usage - t.pressureBaseline.usage
ratePerSecond := float64(growth) / elapsed.Seconds()
headroom := t.memoryLimit - sample.usage
timeToLimit := time.Duration(float64(headroom)/ratePerSecond) * time.Second
if timeToLimit < t.minInterval {
triggered = true
rateTriggered = true
t.state = pressureStateTriggered
}
}
}
t.access.Unlock()
if !triggered {
return
}
if rateTriggered {
if t.killerDisabled {
t.logger.Warn("memory growth rate critical (report only), usage: ", byteformats.FormatMemoryBytes(sample.usage), t.logDetails(sample))
} else {
t.logger.Error("memory growth rate critical, usage: ", byteformats.FormatMemoryBytes(sample.usage), t.logDetails(sample), ", resetting network")
t.router.ResetNetwork()
}
} else {
if t.killerDisabled {
t.logger.Warn("memory threshold reached (report only), usage: ", byteformats.FormatMemoryBytes(sample.usage), t.logDetails(sample))
} else {
t.logger.Error("memory threshold reached, usage: ", byteformats.FormatMemoryBytes(sample.usage), t.logDetails(sample), ", resetting network")
t.router.ResetNetwork()
}
}
t.onTriggered(sample.usage)
runtimeDebug.FreeOSMemory()
}
func (t *adaptiveTimer) nextState(sample memorySample) pressureState {
switch t.policyMode {
case policyModeMemoryLimit, policyModeNetworkExtension:
return nextPressureState(t.state,
sample.usage >= t.limitThresholds.trigger,
sample.usage >= t.limitThresholds.armed,
sample.usage >= t.limitThresholds.resume,
)
case policyModeAvailable:
if !sample.availableKnown {
return pressureStateNormal
}
thresholds := t.availableThresholds(sample)
return nextPressureState(t.state,
sample.available <= thresholds.trigger,
sample.available <= thresholds.armed,
sample.available <= thresholds.resume,
)
default:
return pressureStateNormal
}
}
func computeLimitThresholds(memoryLimit uint64, safetyMargin uint64) pressureThresholds {
triggerMargin := min(safetyMargin, memoryLimit)
armedMargin := min(triggerMargin*2, memoryLimit)
resumeMargin := min(triggerMargin*4, memoryLimit)
return pressureThresholds{
trigger: memoryLimit - triggerMargin,
armed: memoryLimit - armedMargin,
resume: memoryLimit - resumeMargin,
}
}
func (t *adaptiveTimer) availableThresholds(sample memorySample) pressureThresholds {
var triggerMargin uint64
if t.hasSafetyMargin {
triggerMargin = t.safetyMargin
} else if sample.usage == 0 {
triggerMargin = defaultAvailableTriggerMarginMin
} else {
triggerMargin = max(defaultAvailableTriggerMarginMin, min(sample.usage/4, defaultAvailableTriggerMarginMax))
}
return pressureThresholds{
trigger: triggerMargin,
armed: triggerMargin * 2,
resume: triggerMargin * 4,
}
}
func (t *adaptiveTimer) intervalForState() time.Duration {
if t.state == pressureStateNormal {
return t.maxInterval
}
if t.forceMinInterval || t.state == pressureStateTriggered {
return t.minInterval
}
return t.armedInterval
}
func (t *adaptiveTimer) logDetails(sample memorySample) string {
switch t.policyMode {
case policyModeMemoryLimit, policyModeNetworkExtension:
headroom := uint64(0)
if sample.usage < t.memoryLimit {
headroom = t.memoryLimit - sample.usage
}
return ", limit: " + byteformats.FormatMemoryBytes(t.memoryLimit) + ", headroom: " + byteformats.FormatMemoryBytes(headroom)
case policyModeAvailable:
if sample.availableKnown {
return ", available: " + byteformats.FormatMemoryBytes(sample.available)
}
}
return ""
}
func nextPressureState(current pressureState, shouldTrigger, shouldArm, shouldStayTriggered bool) pressureState {
if current == pressureStateTriggered {
if shouldStayTriggered {
return pressureStateTriggered
}
return pressureStateNormal
}
if shouldTrigger {
return pressureStateTriggered
}
if shouldArm {
return pressureStateArmed
}
return pressureStateNormal
}
func readMemorySample(mode policyMode) memorySample {
sample := memorySample{
usage: memory.Total(),
}
if mode == policyModeAvailable {
sample.availableKnown = true
sample.available = memory.Available()
}
return sample
}