package provider import ( "context" "os" "path/filepath" "time" "github.com/sagernet/fswatch" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/provider" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/parser" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/filemanager" ) func RegisterProviderLocal(registry *provider.Registry) { provider.Register[option.ProviderLocalOptions](registry, C.ProviderTypeLocal, NewProviderLocal) } func RegisterProviderInline(registry *provider.Registry) { provider.Register[option.ProviderInlineOptions](registry, C.ProviderTypeInline, NewProviderInline) } var _ adapter.Provider = (*ProviderLocal)(nil) type ProviderLocal struct { provider.Adapter ctx context.Context logger log.ContextLogger provider adapter.ProviderManager path string lastOutOpts []option.Outbound lastUpdated time.Time watcher *fswatch.Watcher } func NewProviderInline(ctx context.Context, router adapter.Router, logFactory log.Factory, tag string, options option.ProviderInlineOptions) (adapter.Provider, error) { var ( outbound = service.FromContext[adapter.OutboundManager](ctx) logger = logFactory.NewLogger(F.ToString("provider/inline", "[", tag, "]")) ) provider := &ProviderLocal{ Adapter: provider.NewAdapter(ctx, router, outbound, logFactory, logger, tag, C.ProviderTypeInline, options.HealthCheck), ctx: ctx, logger: logger, } provider.UpdateOutbounds(nil, options.Outbounds) return provider, nil } func NewProviderLocal(ctx context.Context, router adapter.Router, logFactory log.Factory, tag string, options option.ProviderLocalOptions) (adapter.Provider, error) { if options.Path == "" { return nil, E.New("provider path is required") } var ( outbound = service.FromContext[adapter.OutboundManager](ctx) logger = logFactory.NewLogger(F.ToString("provider/local", "[", tag, "]")) ) provider := &ProviderLocal{ Adapter: provider.NewAdapter(ctx, router, outbound, logFactory, logger, tag, C.ProviderTypeLocal, options.HealthCheck), ctx: ctx, logger: logger, provider: service.FromContext[adapter.ProviderManager](ctx), } filePath := filemanager.BasePath(ctx, options.Path) provider.path, _ = filepath.Abs(filePath) watcher, err := fswatch.NewWatcher(fswatch.Options{ Path: []string{filePath}, Callback: func(path string) { uErr := provider.reloadFile(path) if uErr != nil { logger.Error(E.Cause(uErr, "reload provider ", tag)) } provider.UpdateGroups() }, }) if err != nil { return nil, err } provider.watcher = watcher return provider, nil } func (s *ProviderLocal) Start() error { err := s.reloadFile(s.path) if err != nil { return err } s.UpdateGroups() if s.watcher != nil { err := s.watcher.Start() if err != nil { s.logger.Error(E.Cause(err, "watch provider file")) } } return s.Adapter.Start() } func (s *ProviderLocal) UpdatedAt() time.Time { return s.lastUpdated } func (s *ProviderLocal) reloadFile(path string) error { if fileInfo, err := os.Stat(path); err == nil { s.lastUpdated = fileInfo.ModTime() } content, err := os.ReadFile(path) if err != nil { return err } outboundOpts, err := parser.ParseSubscription(s.ctx, string(content)) if err != nil { return err } s.UpdateOutbounds(s.lastOutOpts, outboundOpts) s.lastOutOpts = outboundOpts return nil } func (s *ProviderLocal) Close() error { return common.Close(&s.Adapter, common.PtrOrNil(s.watcher)) }