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  }