github.com/sagernet/sing@v0.4.0-beta.19.0.20240518125136-f67a0988a636/common/ntp/service.go (about) 1 package ntp 2 3 import ( 4 "context" 5 "os" 6 "time" 7 8 "github.com/sagernet/sing/common" 9 E "github.com/sagernet/sing/common/exceptions" 10 "github.com/sagernet/sing/common/logger" 11 M "github.com/sagernet/sing/common/metadata" 12 N "github.com/sagernet/sing/common/network" 13 "github.com/sagernet/sing/service" 14 "github.com/sagernet/sing/service/pause" 15 ) 16 17 const TimeLayout = "2006-01-02 15:04:05 -0700" 18 19 type TimeService interface { 20 TimeFunc() func() time.Time 21 } 22 23 type Options struct { 24 Context context.Context 25 Dialer N.Dialer 26 Logger logger.Logger 27 Server M.Socksaddr 28 Interval time.Duration 29 WriteToSystem bool 30 } 31 32 var _ TimeService = (*Service)(nil) 33 34 type Service struct { 35 ctx context.Context 36 cancel common.ContextCancelCauseFunc 37 dialer N.Dialer 38 logger logger.Logger 39 server M.Socksaddr 40 writeToSystem bool 41 ticker *time.Ticker 42 clockOffset time.Duration 43 pause pause.Manager 44 } 45 46 func NewService(options Options) *Service { 47 ctx := options.Context 48 if ctx == nil { 49 ctx = context.Background() 50 } 51 ctx, cancel := common.ContextWithCancelCause(ctx) 52 destination := options.Server 53 if !destination.IsValid() { 54 destination = M.Socksaddr{ 55 Fqdn: "time.apple.com", 56 } 57 } 58 if options.Logger == nil { 59 options.Logger = logger.NOP() 60 } 61 if destination.Port == 0 { 62 destination.Port = 123 63 } 64 var interval time.Duration 65 if options.Interval > 0 { 66 interval = options.Interval 67 } else { 68 interval = 30 * time.Minute 69 } 70 var dialer N.Dialer 71 if options.Dialer != nil { 72 dialer = options.Dialer 73 } else { 74 dialer = N.SystemDialer 75 } 76 return &Service{ 77 ctx: ctx, 78 cancel: cancel, 79 dialer: dialer, 80 logger: options.Logger, 81 writeToSystem: options.WriteToSystem, 82 server: destination, 83 ticker: time.NewTicker(interval), 84 pause: service.FromContext[pause.Manager](ctx), 85 } 86 } 87 88 func (s *Service) Start() error { 89 err := s.update() 90 if err != nil { 91 return E.Cause(err, "initialize time") 92 } 93 s.logger.Info("updated time: ", s.TimeFunc()().Local().Format(TimeLayout)) 94 go s.loopUpdate() 95 return nil 96 } 97 98 func (s *Service) Close() error { 99 s.ticker.Stop() 100 s.cancel(os.ErrClosed) 101 return nil 102 } 103 104 func (s *Service) TimeFunc() func() time.Time { 105 return func() time.Time { 106 return time.Now().Add(s.clockOffset) 107 } 108 } 109 110 func (s *Service) loopUpdate() { 111 for { 112 select { 113 case <-s.ctx.Done(): 114 return 115 case <-s.ticker.C: 116 } 117 if s.pause != nil { 118 s.pause.WaitActive() 119 select { 120 case <-s.ctx.Done(): 121 return 122 default: 123 } 124 } 125 err := s.update() 126 if err == nil { 127 s.logger.Debug("updated time: ", s.TimeFunc()().Local().Format(TimeLayout)) 128 } else { 129 s.logger.Warn("update time: ", err) 130 } 131 } 132 } 133 134 func (s *Service) update() error { 135 response, err := Exchange(s.ctx, s.dialer, s.server) 136 if err != nil { 137 return err 138 } 139 s.clockOffset = response.ClockOffset 140 if s.writeToSystem { 141 writeErr := SetSystemTime(s.TimeFunc()()) 142 if writeErr != nil { 143 s.logger.Warn("write time to system: ", writeErr) 144 } 145 } 146 return nil 147 }