diff --git a/Makefile b/Makefile index 76c1f4fb..25910d3f 100644 --- a/Makefile +++ b/Makefile @@ -89,8 +89,8 @@ lib: lib_install: go get -v -d - go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.0.0-20230413023804-244d7ff07035 - go install -v github.com/sagernet/gomobile/cmd/gobind@v0.0.0-20230413023804-244d7ff07035 + go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.0.0-20230701084532-493ee2e45182 + go install -v github.com/sagernet/gomobile/cmd/gobind@v0.0.0-20230701084532-493ee2e45182 clean: rm -rf bin dist sing-box diff --git a/adapter/experimental.go b/adapter/experimental.go index e9a5f903..f25d70a4 100644 --- a/adapter/experimental.go +++ b/adapter/experimental.go @@ -31,6 +31,7 @@ type Tracker interface { } type OutboundGroup interface { + Outbound Now() string All() []string } diff --git a/adapter/prestart.go b/adapter/prestart.go index c1b5f581..6a39aec3 100644 --- a/adapter/prestart.go +++ b/adapter/prestart.go @@ -4,12 +4,6 @@ type PreStarter interface { PreStart() error } -func PreStart(starter any) error { - if preService, ok := starter.(PreStarter); ok { - err := preService.PreStart() - if err != nil { - return err - } - } - return nil +type PostStarter interface { + PostStart() error } diff --git a/box.go b/box.go index dc589071..3ceb7a55 100644 --- a/box.go +++ b/box.go @@ -211,10 +211,12 @@ func (s *Box) Start() error { func (s *Box) preStart() error { for serviceName, service := range s.preServices { - s.logger.Trace("pre-start ", serviceName) - err := adapter.PreStart(service) - if err != nil { - return E.Cause(err, "pre-starting ", serviceName) + if preService, isPreService := service.(adapter.PreStarter); isPreService { + s.logger.Trace("pre-start ", serviceName) + err := preService.PreStart() + if err != nil { + return E.Cause(err, "pre-starting ", serviceName) + } } } err := s.startOutbounds() @@ -249,13 +251,26 @@ func (s *Box) start() error { return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]") } } + return nil +} + +func (s *Box) postStart() error { for serviceName, service := range s.postServices { s.logger.Trace("starting ", service) - err = service.Start() + err := service.Start() if err != nil { return E.Cause(err, "start ", serviceName) } } + for serviceName, service := range s.outbounds { + if lateService, isLateService := service.(adapter.PostStarter); isLateService { + s.logger.Trace("post-starting ", service) + err := lateService.PostStart() + if err != nil { + return E.Cause(err, "post-start ", serviceName) + } + } + } return nil } diff --git a/cmd/internal/build_libbox/main.go b/cmd/internal/build_libbox/main.go index 44911797..f65262ba 100644 --- a/cmd/internal/build_libbox/main.go +++ b/cmd/internal/build_libbox/main.go @@ -133,7 +133,7 @@ func buildiOS() { log.Fatal(err) } - copyPath := filepath.Join("..", "sing-box-for-ios") + copyPath := filepath.Join("..", "sing-box-for-apple") if rw.FileExists(copyPath) { targetDir := filepath.Join(copyPath, "Libbox.xcframework") targetDir, _ = filepath.Abs(targetDir) diff --git a/common/urltest/urltest.go b/common/urltest/urltest.go index 436c2a5e..8dd85f51 100644 --- a/common/urltest/urltest.go +++ b/common/urltest/urltest.go @@ -10,6 +10,7 @@ import ( M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/x/list" ) type History struct { @@ -20,6 +21,7 @@ type History struct { type HistoryStorage struct { access sync.RWMutex delayHistory map[string]*History + callbacks list.List[func()] } func NewHistoryStorage() *HistoryStorage { @@ -28,6 +30,18 @@ func NewHistoryStorage() *HistoryStorage { } } +func (s *HistoryStorage) AddListener(listener func()) *list.Element[func()] { + s.access.Lock() + defer s.access.Unlock() + return s.callbacks.PushBack(listener) +} + +func (s *HistoryStorage) RemoveListener(element *list.Element[func()]) { + s.access.Lock() + defer s.access.Unlock() + s.callbacks.Remove(element) +} + func (s *HistoryStorage) LoadURLTestHistory(tag string) *History { if s == nil { return nil @@ -39,14 +53,24 @@ func (s *HistoryStorage) LoadURLTestHistory(tag string) *History { func (s *HistoryStorage) DeleteURLTestHistory(tag string) { s.access.Lock() - defer s.access.Unlock() delete(s.delayHistory, tag) + s.access.Unlock() + s.notifyUpdated() } func (s *HistoryStorage) StoreURLTestHistory(tag string, history *History) { s.access.Lock() - defer s.access.Unlock() s.delayHistory[tag] = history + s.access.Unlock() + s.notifyUpdated() +} + +func (s *HistoryStorage) notifyUpdated() { + s.access.RLock() + defer s.access.RUnlock() + for element := s.callbacks.Front(); element != nil; element = element.Next() { + element.Value() + } } func URLTest(ctx context.Context, link string, detour N.Dialer) (t uint16, err error) { diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go index a365d0c9..3a315c93 100644 --- a/experimental/clashapi/server.go +++ b/experimental/clashapi/server.go @@ -23,6 +23,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/filemanager" "github.com/sagernet/websocket" @@ -68,13 +69,16 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ Handler: chiRouter, }, trafficManager: trafficManager, - urlTestHistory: urltest.NewHistoryStorage(), mode: strings.ToLower(options.DefaultMode), storeSelected: options.StoreSelected, storeFakeIP: options.StoreFakeIP, externalUIDownloadURL: options.ExternalUIDownloadURL, externalUIDownloadDetour: options.ExternalUIDownloadDetour, } + server.urlTestHistory = service.PtrFromContext[urltest.HistoryStorage](ctx) + if server.urlTestHistory == nil { + server.urlTestHistory = urltest.NewHistoryStorage() + } if server.mode == "" { server.mode = "rule" } diff --git a/experimental/libbox/command.go b/experimental/libbox/command.go index 5f344d10..dff012a2 100644 --- a/experimental/libbox/command.go +++ b/experimental/libbox/command.go @@ -3,7 +3,9 @@ package libbox const ( CommandLog int32 = iota CommandStatus - CommandServiceStop CommandServiceReload CommandCloseConnections + CommandGroup + CommandSelectOutbound + CommandURLTest ) diff --git a/experimental/libbox/command_client.go b/experimental/libbox/command_client.go index f48df1af..5c491777 100644 --- a/experimental/libbox/command_client.go +++ b/experimental/libbox/command_client.go @@ -26,6 +26,13 @@ type CommandClientHandler interface { Disconnected(message string) WriteLog(message string) WriteStatus(message *StatusMessage) + WriteGroups(message OutboundGroupIterator) +} + +func NewStandaloneCommandClient(sharedDirectory string) *CommandClient { + return &CommandClient{ + sharedDirectory: sharedDirectory, + } } func NewCommandClient(sharedDirectory string, handler CommandClientHandler, options *CommandClientOptions) *CommandClient { @@ -36,16 +43,16 @@ func NewCommandClient(sharedDirectory string, handler CommandClientHandler, opti } } -func clientConnect(sharedDirectory string) (net.Conn, error) { +func (c *CommandClient) directConnect() (net.Conn, error) { return net.DialUnix("unix", nil, &net.UnixAddr{ - Name: filepath.Join(sharedDirectory, "command.sock"), + Name: filepath.Join(c.sharedDirectory, "command.sock"), Net: "unix", }) } func (c *CommandClient) Connect() error { common.Close(c.conn) - conn, err := clientConnect(c.sharedDirectory) + conn, err := c.directConnect() if err != nil { return err } @@ -65,6 +72,13 @@ func (c *CommandClient) Connect() error { } c.handler.Connected() go c.handleStatusConn(conn) + case CommandGroup: + err = binary.Write(conn, binary.BigEndian, c.options.StatusInterval) + if err != nil { + return E.Cause(err, "write interval") + } + c.handler.Connected() + go c.handleGroupConn(conn) } return nil } diff --git a/experimental/libbox/command_conntrack.go b/experimental/libbox/command_conntrack.go index c7f24199..dbc7738a 100644 --- a/experimental/libbox/command_conntrack.go +++ b/experimental/libbox/command_conntrack.go @@ -9,8 +9,8 @@ import ( "github.com/sagernet/sing-box/common/dialer/conntrack" ) -func ClientCloseConnections(sharedDirectory string) error { - conn, err := clientConnect(sharedDirectory) +func (c *CommandClient) CloseConnections() error { + conn, err := c.directConnect() if err != nil { return err } diff --git a/experimental/libbox/command_group.go b/experimental/libbox/command_group.go new file mode 100644 index 00000000..6ff8770e --- /dev/null +++ b/experimental/libbox/command_group.go @@ -0,0 +1,228 @@ +package libbox + +import ( + "encoding/binary" + "io" + "net" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/urltest" + "github.com/sagernet/sing-box/outbound" + "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/service" +) + +type OutboundGroup struct { + Tag string + Type string + Selectable bool + Selected string + items []*OutboundGroupItem +} + +func (g *OutboundGroup) GetItems() OutboundGroupItemIterator { + return newIterator(g.items) +} + +type OutboundGroupIterator interface { + Next() *OutboundGroup + HasNext() bool +} + +type OutboundGroupItem struct { + Tag string + Type string + URLTestTime int64 + URLTestDelay int32 +} + +type OutboundGroupItemIterator interface { + Next() *OutboundGroupItem + HasNext() bool +} + +func (c *CommandClient) handleGroupConn(conn net.Conn) { + defer conn.Close() + + for { + groups, err := readGroups(conn) + if err != nil { + c.handler.Disconnected(err.Error()) + return + } + c.handler.WriteGroups(groups) + } +} + +func (s *CommandServer) handleGroupConn(conn net.Conn) error { + defer conn.Close() + ctx := connKeepAlive(conn) + for { + service := s.service + if service != nil { + err := writeGroups(conn, service) + if err != nil { + return err + } + } else { + err := binary.Write(conn, binary.BigEndian, uint16(0)) + if err != nil { + return err + } + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-s.urlTestUpdate: + } + } +} + +func readGroups(reader io.Reader) (OutboundGroupIterator, error) { + var groupLength uint16 + err := binary.Read(reader, binary.BigEndian, &groupLength) + if err != nil { + return nil, err + } + + groups := make([]*OutboundGroup, 0, groupLength) + for i := 0; i < int(groupLength); i++ { + var group OutboundGroup + group.Tag, err = rw.ReadVString(reader) + if err != nil { + return nil, err + } + + group.Type, err = rw.ReadVString(reader) + if err != nil { + return nil, err + } + + err = binary.Read(reader, binary.BigEndian, &group.Selectable) + if err != nil { + return nil, err + } + + group.Selected, err = rw.ReadVString(reader) + if err != nil { + return nil, err + } + + var itemLength uint16 + err = binary.Read(reader, binary.BigEndian, &itemLength) + if err != nil { + return nil, err + } + + group.items = make([]*OutboundGroupItem, itemLength) + for j := 0; j < int(itemLength); j++ { + var item OutboundGroupItem + item.Tag, err = rw.ReadVString(reader) + if err != nil { + return nil, err + } + + item.Type, err = rw.ReadVString(reader) + if err != nil { + return nil, err + } + + err = binary.Read(reader, binary.BigEndian, &item.URLTestTime) + if err != nil { + return nil, err + } + + err = binary.Read(reader, binary.BigEndian, &item.URLTestDelay) + if err != nil { + return nil, err + } + + group.items[j] = &item + } + groups = append(groups, &group) + } + return newIterator(groups), nil +} + +func writeGroups(writer io.Writer, boxService *BoxService) error { + historyStorage := service.PtrFromContext[urltest.HistoryStorage](boxService.ctx) + + outbounds := boxService.instance.Router().Outbounds() + var iGroups []adapter.OutboundGroup + for _, it := range outbounds { + if group, isGroup := it.(adapter.OutboundGroup); isGroup { + iGroups = append(iGroups, group) + } + } + var groups []OutboundGroup + for _, iGroup := range iGroups { + var group OutboundGroup + group.Tag = iGroup.Tag() + group.Type = iGroup.Type() + _, group.Selectable = iGroup.(*outbound.Selector) + group.Selected = iGroup.Now() + + for _, itemTag := range iGroup.All() { + itemOutbound, isLoaded := boxService.instance.Router().Outbound(itemTag) + if !isLoaded { + continue + } + + var item OutboundGroupItem + item.Tag = itemTag + item.Type = itemOutbound.Type() + if history := historyStorage.LoadURLTestHistory(adapter.OutboundTag(itemOutbound)); history != nil { + item.URLTestTime = history.Time.Unix() + item.URLTestDelay = int32(history.Delay) + } + group.items = append(group.items, &item) + } + groups = append(groups, group) + } + + err := binary.Write(writer, binary.BigEndian, uint16(len(groups))) + if err != nil { + return err + } + for _, group := range groups { + err = rw.WriteVString(writer, group.Tag) + if err != nil { + return err + } + err = rw.WriteVString(writer, group.Type) + if err != nil { + return err + } + err = binary.Write(writer, binary.BigEndian, group.Selectable) + if err != nil { + return err + } + err = rw.WriteVString(writer, group.Selected) + if err != nil { + return err + } + err = binary.Write(writer, binary.BigEndian, uint16(len(group.items))) + if err != nil { + return err + } + for _, item := range group.items { + err = rw.WriteVString(writer, item.Tag) + if err != nil { + return err + } + err = rw.WriteVString(writer, item.Type) + if err != nil { + return err + } + err = binary.Write(writer, binary.BigEndian, item.URLTestTime) + if err != nil { + return err + } + err = binary.Write(writer, binary.BigEndian, item.URLTestDelay) + if err != nil { + return err + } + } + } + return nil +} diff --git a/experimental/libbox/command_log.go b/experimental/libbox/command_log.go index b70e9884..7cb1696b 100644 --- a/experimental/libbox/command_log.go +++ b/experimental/libbox/command_log.go @@ -11,7 +11,7 @@ func (s *CommandServer) WriteMessage(message string) { s.subscriber.Emit(message) s.access.Lock() s.savedLines.PushBack(message) - if s.savedLines.Len() > 100 { + if s.savedLines.Len() > s.maxLines { s.savedLines.Remove(s.savedLines.Front()) } s.access.Unlock() diff --git a/experimental/libbox/command_reload.go b/experimental/libbox/command_reload.go index 19ac0264..4c8774a7 100644 --- a/experimental/libbox/command_reload.go +++ b/experimental/libbox/command_reload.go @@ -8,8 +8,8 @@ import ( "github.com/sagernet/sing/common/rw" ) -func ClientServiceReload(sharedDirectory string) error { - conn, err := clientConnect(sharedDirectory) +func (c *CommandClient) ServiceReload() error { + conn, err := c.directConnect() if err != nil { return err } diff --git a/experimental/libbox/command_select.go b/experimental/libbox/command_select.go new file mode 100644 index 00000000..a434aa0e --- /dev/null +++ b/experimental/libbox/command_select.go @@ -0,0 +1,59 @@ +package libbox + +import ( + "encoding/binary" + "net" + + "github.com/sagernet/sing-box/outbound" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/rw" +) + +func (c *CommandClient) SelectOutbound(groupTag string, outboundTag string) error { + conn, err := c.directConnect() + if err != nil { + return err + } + defer conn.Close() + err = binary.Write(conn, binary.BigEndian, uint8(CommandSelectOutbound)) + if err != nil { + return err + } + err = rw.WriteVString(conn, groupTag) + if err != nil { + return err + } + err = rw.WriteVString(conn, outboundTag) + if err != nil { + return err + } + return readError(conn) +} + +func (s *CommandServer) handleSelectOutbound(conn net.Conn) error { + defer conn.Close() + groupTag, err := rw.ReadVString(conn) + if err != nil { + return err + } + outboundTag, err := rw.ReadVString(conn) + if err != nil { + return err + } + service := s.service + if service == nil { + return writeError(conn, E.New("service not ready")) + } + outboundGroup, isLoaded := service.instance.Router().Outbound(groupTag) + if !isLoaded { + return writeError(conn, E.New("selector not found: ", groupTag)) + } + selector, isSelector := outboundGroup.(*outbound.Selector) + if !isSelector { + return writeError(conn, E.New("outbound is not a selector: ", groupTag)) + } + if !selector.SelectOutbound(outboundTag) { + return writeError(conn, E.New("outbound not found in selector: ", outboundTag)) + } + return writeError(conn, nil) +} diff --git a/experimental/libbox/command_server.go b/experimental/libbox/command_server.go index d5391cbd..dcedb7e5 100644 --- a/experimental/libbox/command_server.go +++ b/experimental/libbox/command_server.go @@ -7,12 +7,14 @@ import ( "path/filepath" "sync" + "github.com/sagernet/sing-box/common/urltest" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/debug" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/observable" "github.com/sagernet/sing/common/x/list" + "github.com/sagernet/sing/service" ) type CommandServer struct { @@ -22,26 +24,51 @@ type CommandServer struct { access sync.Mutex savedLines *list.List[string] + maxLines int subscriber *observable.Subscriber[string] observer *observable.Observer[string] + service *BoxService + + urlTestListener *list.Element[func()] + urlTestUpdate chan struct{} } type CommandServerHandler interface { - ServiceStop() error ServiceReload() error } -func NewCommandServer(sharedDirectory string, handler CommandServerHandler) *CommandServer { +func NewCommandServer(sharedDirectory string, handler CommandServerHandler, maxLines int32) *CommandServer { server := &CommandServer{ - sockPath: filepath.Join(sharedDirectory, "command.sock"), - handler: handler, - savedLines: new(list.List[string]), - subscriber: observable.NewSubscriber[string](128), + sockPath: filepath.Join(sharedDirectory, "command.sock"), + handler: handler, + savedLines: new(list.List[string]), + maxLines: int(maxLines), + subscriber: observable.NewSubscriber[string](128), + urlTestUpdate: make(chan struct{}, 1), } server.observer = observable.NewObserver[string](server.subscriber, 64) return server } +func (s *CommandServer) SetService(newService *BoxService) { + if s.service != nil && s.listener != nil { + service.PtrFromContext[urltest.HistoryStorage](s.service.ctx).RemoveListener(s.urlTestListener) + s.urlTestListener = nil + } + s.service = newService + if newService != nil { + s.urlTestListener = service.PtrFromContext[urltest.HistoryStorage](newService.ctx).AddListener(s.notifyURLTestUpdate) + } + s.notifyURLTestUpdate() +} + +func (s *CommandServer) notifyURLTestUpdate() { + select { + case s.urlTestUpdate <- struct{}{}: + default: + } +} + func (s *CommandServer) Start() error { os.Remove(s.sockPath) listener, err := net.ListenUnix("unix", &net.UnixAddr{ @@ -92,12 +119,16 @@ func (s *CommandServer) handleConnection(conn net.Conn) error { return s.handleLogConn(conn) case CommandStatus: return s.handleStatusConn(conn) - case CommandServiceStop: - return s.handleServiceStop(conn) case CommandServiceReload: return s.handleServiceReload(conn) case CommandCloseConnections: return s.handleCloseConnections(conn) + case CommandGroup: + return s.handleGroupConn(conn) + case CommandSelectOutbound: + return s.handleSelectOutbound(conn) + case CommandURLTest: + return s.handleURLTest(conn) default: return E.New("unknown command: ", command) } diff --git a/experimental/libbox/command_shared.go b/experimental/libbox/command_shared.go new file mode 100644 index 00000000..ecad78dd --- /dev/null +++ b/experimental/libbox/command_shared.go @@ -0,0 +1,39 @@ +package libbox + +import ( + "encoding/binary" + "io" + + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/rw" +) + +func readError(reader io.Reader) error { + var hasError bool + err := binary.Read(reader, binary.BigEndian, &hasError) + if err != nil { + return err + } + if hasError { + errorMessage, err := rw.ReadVString(reader) + if err != nil { + return err + } + return E.New(errorMessage) + } + return nil +} + +func writeError(writer io.Writer, wErr error) error { + err := binary.Write(writer, binary.BigEndian, wErr != nil) + if err != nil { + return err + } + if wErr != nil { + err = rw.WriteVString(writer, wErr.Error()) + if err != nil { + return err + } + } + return nil +} diff --git a/experimental/libbox/command_stop.go b/experimental/libbox/command_stop.go deleted file mode 100644 index 8609b99a..00000000 --- a/experimental/libbox/command_stop.go +++ /dev/null @@ -1,48 +0,0 @@ -package libbox - -import ( - "encoding/binary" - "net" - "runtime/debug" - - E "github.com/sagernet/sing/common/exceptions" - "github.com/sagernet/sing/common/rw" -) - -func ClientServiceStop(sharedDirectory string) error { - conn, err := clientConnect(sharedDirectory) - if err != nil { - return err - } - defer conn.Close() - err = binary.Write(conn, binary.BigEndian, uint8(CommandServiceStop)) - if err != nil { - return err - } - var hasError bool - err = binary.Read(conn, binary.BigEndian, &hasError) - if err != nil { - return err - } - if hasError { - errorMessage, err := rw.ReadVString(conn) - if err != nil { - return err - } - return E.New(errorMessage) - } - return nil -} - -func (s *CommandServer) handleServiceStop(conn net.Conn) error { - rErr := s.handler.ServiceStop() - err := binary.Write(conn, binary.BigEndian, rErr != nil) - if err != nil { - return err - } - if rErr != nil { - return rw.WriteVString(conn, rErr.Error()) - } - debug.FreeOSMemory() - return nil -} diff --git a/experimental/libbox/command_urltest.go b/experimental/libbox/command_urltest.go new file mode 100644 index 00000000..88e86a8f --- /dev/null +++ b/experimental/libbox/command_urltest.go @@ -0,0 +1,95 @@ +package libbox + +import ( + "encoding/binary" + "net" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/urltest" + "github.com/sagernet/sing-box/outbound" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/batch" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/rw" +) + +func (c *CommandClient) URLTest(groupTag string) error { + conn, err := c.directConnect() + if err != nil { + return err + } + defer conn.Close() + err = binary.Write(conn, binary.BigEndian, uint8(CommandURLTest)) + if err != nil { + return err + } + err = rw.WriteVString(conn, groupTag) + if err != nil { + return err + } + return readError(conn) +} + +func (s *CommandServer) handleURLTest(conn net.Conn) error { + defer conn.Close() + groupTag, err := rw.ReadVString(conn) + if err != nil { + return err + } + service := s.service + if service == nil { + return nil + } + abstractOutboundGroup, isLoaded := service.instance.Router().Outbound(groupTag) + if !isLoaded { + return writeError(conn, E.New("outbound group not found: ", groupTag)) + } + outboundGroup, isOutboundGroup := abstractOutboundGroup.(adapter.OutboundGroup) + if !isOutboundGroup { + return writeError(conn, E.New("outbound is not a group: ", groupTag)) + } + urlTest, isURLTest := abstractOutboundGroup.(*outbound.URLTest) + if isURLTest { + go urlTest.CheckOutbounds() + } else { + var historyStorage *urltest.HistoryStorage + if clashServer := service.instance.Router().ClashServer(); clashServer != nil { + historyStorage = clashServer.HistoryStorage() + } else { + return writeError(conn, E.New("Clash API is required for URLTest on non-URLTest group")) + } + + outbounds := common.Filter(common.Map(outboundGroup.All(), func(it string) adapter.Outbound { + itOutbound, _ := service.instance.Router().Outbound(it) + return itOutbound + }), func(it adapter.Outbound) bool { + if it == nil { + return false + } + _, isGroup := it.(adapter.OutboundGroup) + if isGroup { + return false + } + return true + }) + b, _ := batch.New(service.ctx, batch.WithConcurrencyNum[any](10)) + for _, detour := range outbounds { + outboundToTest := detour + outboundTag := outboundToTest.Tag() + b.Go(outboundTag, func() (any, error) { + t, err := urltest.URLTest(service.ctx, "", outboundToTest) + if err != nil { + historyStorage.DeleteURLTestHistory(outboundTag) + } else { + historyStorage.StoreURLTestHistory(outboundTag, &urltest.History{ + Time: time.Now(), + Delay: t, + }) + } + return nil, nil + }) + } + } + return writeError(conn, nil) +} diff --git a/experimental/libbox/service.go b/experimental/libbox/service.go index 2032e541..1ed2d68e 100644 --- a/experimental/libbox/service.go +++ b/experimental/libbox/service.go @@ -8,6 +8,7 @@ import ( "github.com/sagernet/sing-box" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/process" + "github.com/sagernet/sing-box/common/urltest" "github.com/sagernet/sing-box/experimental/libbox/internal/procfs" "github.com/sagernet/sing-box/experimental/libbox/platform" "github.com/sagernet/sing-box/option" @@ -16,6 +17,7 @@ import ( "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/filemanager" ) @@ -32,6 +34,7 @@ func NewService(configContent string, platformInterface PlatformInterface) (*Box } ctx, cancel := context.WithCancel(context.Background()) ctx = filemanager.WithDefault(ctx, sBasePath, sTempPath, sUserID, sGroupID) + ctx = service.ContextWithPtr(ctx, urltest.NewHistoryStorage()) instance, err := box.New(box.Options{ Context: ctx, Options: options, diff --git a/go.mod b/go.mod index e8cd4984..e1dc57c9 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/oschwald/maxminddb-golang v1.11.0 github.com/pires/go-proxyproto v0.7.0 github.com/sagernet/cloudflare-tls v0.0.0-20221031050923-d70792f4c3a0 - github.com/sagernet/gomobile v0.0.0-20230413023804-244d7ff07035 + github.com/sagernet/gomobile v0.0.0-20230701084532-493ee2e45182 github.com/sagernet/gvisor v0.0.0-20230627031050-1ab0276e0dd2 github.com/sagernet/quic-go v0.0.0-20230615020047-10f05c797c02 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 diff --git a/go.sum b/go.sum index dceb5254..d1ae9a28 100644 --- a/go.sum +++ b/go.sum @@ -104,8 +104,8 @@ github.com/sagernet/cloudflare-tls v0.0.0-20221031050923-d70792f4c3a0 h1:KyhtFFt github.com/sagernet/cloudflare-tls v0.0.0-20221031050923-d70792f4c3a0/go.mod h1:D4SFEOkJK+4W1v86ZhX0jPM0rAL498fyQAChqMtes/I= github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 h1:5+m7c6AkmAylhauulqN/c5dnh8/KssrE9c93TQrXldA= github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61/go.mod h1:QUQ4RRHD6hGGHdFMEtR8T2P6GS6R3D/CXKdaYHKKXms= -github.com/sagernet/gomobile v0.0.0-20230413023804-244d7ff07035 h1:KttYh6bBhIw8Y6/Ljn7CGwC3CKZn788rzMJmeAKjY+8= -github.com/sagernet/gomobile v0.0.0-20230413023804-244d7ff07035/go.mod h1:5YE39YkJkCcMsfq1jMKkjsrM2GfBoF9JVWnvU89hmvU= +github.com/sagernet/gomobile v0.0.0-20230701084532-493ee2e45182 h1:sD5g92IO15RAX2DvA4Cq3Uc7tcgqNWVi8K3VTCI6sEo= +github.com/sagernet/gomobile v0.0.0-20230701084532-493ee2e45182/go.mod h1:5YE39YkJkCcMsfq1jMKkjsrM2GfBoF9JVWnvU89hmvU= github.com/sagernet/gvisor v0.0.0-20230627031050-1ab0276e0dd2 h1:dnkKrzapqtAwjTSWt6hdPrARORfoYvuUczynvRLrueo= github.com/sagernet/gvisor v0.0.0-20230627031050-1ab0276e0dd2/go.mod h1:1JUiV7nGuf++YFm9eWZ8q2lrwHmhcUGzptMl/vL1+LA= github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE= @@ -149,6 +149,7 @@ github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRM github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= diff --git a/log/default.go b/log/default.go index 1784b67f..e3554647 100644 --- a/log/default.go +++ b/log/default.go @@ -24,8 +24,9 @@ func NewFactory(formatter Formatter, writer io.Writer, platformWriter io.Writer) return &simpleFactory{ formatter: formatter, platformFormatter: Formatter{ - BaseTime: formatter.BaseTime, - DisableColors: C.IsDarwin || C.IsIos, + BaseTime: formatter.BaseTime, + DisableColors: C.IsDarwin || C.IsIos, + DisableLineBreak: true, }, writer: writer, platformWriter: platformWriter, diff --git a/log/format.go b/log/format.go index 38495feb..584af0ad 100644 --- a/log/format.go +++ b/log/format.go @@ -17,6 +17,7 @@ type Formatter struct { DisableTimestamp bool FullTimestamp bool TimestampFormat string + DisableLineBreak bool } func (f Formatter) Format(ctx context.Context, level Level, tag string, message string, timestamp time.Time) string { @@ -76,8 +77,14 @@ func (f Formatter) Format(ctx context.Context, level Level, tag string, message default: message = levelString + "[" + xd(int(timestamp.Sub(f.BaseTime)/time.Second), 4) + "] " + message } - if message[len(message)-1] != '\n' { - message += "\n" + if f.DisableLineBreak { + if message[len(message)-1] != '\n' { + message = message[:len(message)-1] + } + } else { + if message[len(message)-1] != '\n' { + message += "\n" + } } return message } diff --git a/log/observable.go b/log/observable.go index 7d873e0e..83d9cf9c 100644 --- a/log/observable.go +++ b/log/observable.go @@ -28,8 +28,9 @@ func NewObservableFactory(formatter Formatter, writer io.Writer, platformWriter factory := &observableFactory{ formatter: formatter, platformFormatter: Formatter{ - BaseTime: formatter.BaseTime, - DisableColors: C.IsDarwin || C.IsIos, + BaseTime: formatter.BaseTime, + DisableColors: C.IsDarwin || C.IsIos, + DisableLineBreak: true, }, writer: writer, platformWriter: platformWriter, diff --git a/outbound/urltest.go b/outbound/urltest.go index a1996652..fce7bd05 100644 --- a/outbound/urltest.go +++ b/outbound/urltest.go @@ -18,6 +18,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" ) var ( @@ -74,7 +75,11 @@ func (s *URLTest) Start() error { outbounds = append(outbounds, detour) } s.group = NewURLTestGroup(s.ctx, s.router, s.logger, outbounds, s.link, s.interval, s.tolerance) - go s.group.CheckOutbounds(false) + return nil +} + +func (s *URLTest) PostStart() error { + go s.CheckOutbounds() return nil } @@ -96,6 +101,10 @@ func (s *URLTest) URLTest(ctx context.Context, link string) (map[string]uint16, return s.group.URLTest(ctx, link) } +func (s *URLTest) CheckOutbounds() { + s.group.CheckOutbounds(true) +} + func (s *URLTest) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { s.group.Start() outbound := s.group.Select(network) @@ -157,7 +166,8 @@ func NewURLTestGroup(ctx context.Context, router adapter.Router, logger log.Logg tolerance = 50 } var history *urltest.HistoryStorage - if clashServer := router.ClashServer(); clashServer != nil { + if history = service.PtrFromContext[urltest.HistoryStorage](ctx); history != nil { + } else if clashServer := router.ClashServer(); clashServer != nil { history = clashServer.HistoryStorage() } else { history = urltest.NewHistoryStorage()