diff --git a/go.mod b/go.mod index 190f3259..031b04d0 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.24.4 require ( github.com/AdguardTeam/dnsproxy v0.75.6 - github.com/AdguardTeam/golibs v0.32.11 + github.com/AdguardTeam/golibs v0.32.14 github.com/AdguardTeam/urlfilter v0.20.0 github.com/NYTimes/gziphandler v1.1.1 github.com/ameshkov/dnscrypt/v2 v2.4.0 @@ -33,7 +33,7 @@ require ( github.com/ti-mo/netfilter v0.5.3 go.etcd.io/bbolt v1.4.0 golang.org/x/crypto v0.39.0 - golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b + golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 golang.org/x/net v0.41.0 golang.org/x/sys v0.33.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 diff --git a/go.sum b/go.sum index 7b9586f1..9f69f5f3 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,8 @@ cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFs cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= github.com/AdguardTeam/dnsproxy v0.75.6 h1:Xz5dciFQeMCtaqBHAyUYGI5PO1Lu7HwIi5ejhSVGsLY= github.com/AdguardTeam/dnsproxy v0.75.6/go.mod h1:WHwb2fHfyujIZLrBymBQtqDUTbzDlNebWqykd1mCNuM= -github.com/AdguardTeam/golibs v0.32.11 h1:75EquS8SWvzsM3JFJY0359ZBw66jDjAegteHzh9nSw8= -github.com/AdguardTeam/golibs v0.32.11/go.mod h1:LXr0gqqZuVpt+L+bP3Nnr0/CecLmm3rxkdgyyW5JXXM= +github.com/AdguardTeam/golibs v0.32.14 h1:b8ExsMuAA7Ewf6bjIbjAtIewWkbEyjDM4v/CU2NxT0Q= +github.com/AdguardTeam/golibs v0.32.14/go.mod h1:DUtN+CepuWjA4Z7fxsr14s/qUTkcYmi9TUuxhkBsxOA= github.com/AdguardTeam/urlfilter v0.20.0 h1:X32qiuVCVd8WDYCEsbdZKfXMzwdVqrdulamtUi4rmzs= github.com/AdguardTeam/urlfilter v0.20.0/go.mod h1:gjrywLTxfJh6JOkwi9SU+frhP7kVVEZ5exFGkR99qpk= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= @@ -205,8 +205,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= -golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b h1:QoALfVG9rhQ/M7vYDScfPdWjGL9dlsVVM5VGh7aKoAA= -golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= +golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 h1:bsqhLWFR6G6xiQcb+JoGqdKdRU6WzPWmK8E0jxTjzo4= +golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/exp/typeparams v0.0.0-20250606033433-dcc06ee1d476 h1:tceuojoK22x7b3gj32PCOPkkFeVtewHm1imqrGyFp1I= golang.org/x/exp/typeparams v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:LKZHyeOpPuZcMgxeHjJp4p5yvxrCX1xDvH10zYHhjjQ= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= diff --git a/internal/next/cmd/cmd.go b/internal/next/cmd/cmd.go index 3bab1396..db6c5200 100644 --- a/internal/next/cmd/cmd.go +++ b/internal/next/cmd/cmd.go @@ -61,38 +61,28 @@ func Main(embeddedFrontend fs.FS) { FileName: opts.confFile, } - confMgr, err := configmgr.New(startCtx, confMgrConf) + svc, err := newServiceMgr(ctx, &serviceMgrConfig{ + confMgrConf: confMgrConf, + logger: baseLogger.With(slogutil.KeyPrefix, "svc"), + pidFilePath: opts.pidFile, + }) errors.Check(err) - web := confMgr.Web() - err = web.Start(startCtx) - errors.Check(err) + errors.Check(svc.Start(startCtx)) - dns := confMgr.DNS() - err = dns.Start(startCtx) - errors.Check(err) + sigHdlr := service.NewSignalHandler(&service.SignalHandlerConfig{ + Logger: baseLogger.With(slogutil.KeyPrefix, service.SignalHandlerPrefix), + }) - sigHdlr := newSignalHandler( - baseLogger.With(slogutil.KeyPrefix, service.SignalHandlerPrefix), - confMgrConf, - opts.pidFile, - web, - dns, - ) + sigHdlr.AddService(svc) + sigHdlr.AddRefresher(svc) - os.Exit(sigHdlr.handle(ctx)) + os.Exit(sigHdlr.Handle(ctx)) } // Default timeouts. // // TODO(a.garipov): Make configurable. const ( - defaultTimeoutStart = 1 * time.Minute - defaultTimeoutShutdown = 5 * time.Second + defaultTimeoutStart = 1 * time.Minute ) - -// newConfigMgr returns a new configuration manager using defaultTimeout as the -// context timeout. -func newConfigMgr(ctx context.Context, c *configmgr.Config) (m *configmgr.Manager, err error) { - return configmgr.New(ctx, c) -} diff --git a/internal/next/cmd/service.go b/internal/next/cmd/service.go new file mode 100644 index 00000000..2aec9d73 --- /dev/null +++ b/internal/next/cmd/service.go @@ -0,0 +1,187 @@ +package cmd + +import ( + "context" + "fmt" + "log/slog" + "os" + "strconv" + "sync" + + "github.com/AdguardTeam/AdGuardHome/internal/next/configmgr" + "github.com/AdguardTeam/golibs/errors" + "github.com/AdguardTeam/golibs/logutil/slogutil" + "github.com/AdguardTeam/golibs/service" + "github.com/google/renameio/v2/maybe" +) + +// serviceMgr manages AdGuard Home services. +type serviceMgr struct { + // confMgrMu protects confMgr. + confMgrMu *sync.RWMutex + + confMgr *configmgr.Manager + confMgrConf *configmgr.Config + logger *slog.Logger + pidFilePath string +} + +// serviceMgrConfig contains service manager configuration parameters. +type serviceMgrConfig struct { + // confMgrConf is the configuration manager config, it must not be nil. + confMgrConf *configmgr.Config + + // logger is the logger used to log services activity, it must not be nil. + logger *slog.Logger + + // pidFilePath is the path to the file where to store the PID, if any. + pidFilePath string +} + +// newServiceMgr creates a new *serviceMgr. +func newServiceMgr(ctx context.Context, conf *serviceMgrConfig) (s *serviceMgr, err error) { + confMgr, err := configmgr.New(ctx, conf.confMgrConf) + if err != nil { + return nil, fmt.Errorf("creating config manager: %w", err) + } + + return &serviceMgr{ + confMgr: confMgr, + confMgrMu: &sync.RWMutex{}, + confMgrConf: conf.confMgrConf, + logger: conf.logger, + pidFilePath: conf.pidFilePath, + }, nil +} + +// type check +var _ service.Interface = (*serviceMgr)(nil) + +// Start implements the [service.Interface] interface for *serviceMgr. +func (s *serviceMgr) Start(ctx context.Context) (err error) { + s.writePID(ctx) + + s.confMgrMu.RLock() + defer s.confMgrMu.RUnlock() + + var errs []error + + err = s.confMgr.Web().Start(ctx) + if err != nil { + errs = append(errs, fmt.Errorf("starting web: %w", err)) + } + + err = s.confMgr.DNS().Start(ctx) + if err != nil { + errs = append(errs, fmt.Errorf("starting dnssvc: %w", err)) + } + + return errors.Join(errs...) +} + +// writePID writes the PID to the file. Any errors are reported to log. +func (s *serviceMgr) writePID(ctx context.Context) { + if s.pidFilePath == "" { + return + } + + pid := os.Getpid() + data := strconv.AppendInt(nil, int64(pid), 10) + data = append(data, '\n') + + err := maybe.WriteFile(s.pidFilePath, data, 0o644) + if err != nil { + s.logger.ErrorContext(ctx, "writing pidfile", slogutil.KeyError, err) + + return + } + + s.logger.DebugContext(ctx, "wrote pid", "file", s.pidFilePath, "pid", pid) +} + +// Shutdown implements the [service.Interface] interface for *serviceMgr. +func (s *serviceMgr) Shutdown(ctx context.Context) (err error) { + s.confMgrMu.RLock() + defer s.confMgrMu.RUnlock() + + var errs []error + + err = s.confMgr.Web().Shutdown(ctx) + if err != nil { + errs = append(errs, fmt.Errorf("shutting down web: %w", err)) + } + + err = s.confMgr.DNS().Shutdown(ctx) + if err != nil { + errs = append(errs, fmt.Errorf("shutting down dnssvc: %w", err)) + } + + s.removePID(ctx) + + return errors.Join(errs...) +} + +// removePID removes the PID file. Any errors are reported to log. +func (s *serviceMgr) removePID(ctx context.Context) { + if s.pidFilePath == "" { + return + } + + err := os.Remove(s.pidFilePath) + if err != nil { + s.logger.ErrorContext(ctx, "removing pidfile", slogutil.KeyError, err) + + return + } + + s.logger.DebugContext(ctx, "removed pidfile", "file", s.pidFilePath) +} + +// type check +var _ service.Refresher = (*serviceMgr)(nil) + +// Refresh implements the [service.Refresher] interface for *serviceMgr. +func (s *serviceMgr) Refresh(ctx context.Context) (err error) { + s.logger.InfoContext(ctx, "reconfiguring started") + + err = s.Shutdown(ctx) + if err != nil { + return fmt.Errorf("shutdown failed: %w", err) + } + + // TODO(a.garipov): This is a very rough way to do it. Some services can + // be reconfigured without the full shutdown, and the error handling is + // currently not the best. + + ctx, cancel := context.WithTimeout(ctx, defaultTimeoutStart) + defer cancel() + + err = s.updConfMgr(ctx) + if err != nil { + return fmt.Errorf("updating configuration manager: %w", err) + } + + err = s.Start(ctx) + if err != nil { + return fmt.Errorf("restarting services: %w", err) + } + + s.logger.InfoContext(ctx, "reconfiguring finished") + + return nil +} + +// updConfMgr updates the configuration manager. +func (s *serviceMgr) updConfMgr(ctx context.Context) (err error) { + confMgr, err := configmgr.New(ctx, s.confMgrConf) + if err != nil { + return fmt.Errorf("creating config manager: %w", err) + } + + s.confMgrMu.Lock() + defer s.confMgrMu.Unlock() + + s.confMgr = confMgr + + return nil +} diff --git a/internal/next/cmd/signal.go b/internal/next/cmd/signal.go deleted file mode 100644 index cdff1b44..00000000 --- a/internal/next/cmd/signal.go +++ /dev/null @@ -1,203 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - "log/slog" - "os" - "strconv" - "time" - - "github.com/AdguardTeam/AdGuardHome/internal/next/configmgr" - "github.com/AdguardTeam/golibs/errors" - "github.com/AdguardTeam/golibs/logutil/slogutil" - "github.com/AdguardTeam/golibs/osutil" - "github.com/AdguardTeam/golibs/service" - "github.com/google/renameio/v2/maybe" -) - -// signalHandler processes incoming signals and shuts services down. -type signalHandler struct { - // logger is used for logging the operation of the signal handler. - logger *slog.Logger - - // confMgrConf contains the configuration parameters for the configuration - // manager. - confMgrConf *configmgr.Config - - // signal is the channel to which OS signals are sent. - signal chan os.Signal - - // pidFile is the path to the file where to store the PID, if any. - pidFile string - - // services are the services that are shut down before application exiting. - services []service.Interface - - // shutdownTimeout is the timeout for the shutdown operation. - shutdownTimeout time.Duration -} - -// handle processes OS signals. It blocks until a termination or a -// reconfiguration signal is received, after which it either shuts down all -// services or reconfigures them. ctx is used for logging and serves as the -// base for the shutdown timeout. status is [osutil.ExitCodeSuccess] on success -// and [osutil.ExitCodeFailure] on error. -// -// TODO(a.garipov): Add reconfiguration logic to golibs. -func (h *signalHandler) handle(ctx context.Context) (status osutil.ExitCode) { - defer slogutil.RecoverAndLog(ctx, h.logger) - - h.writePID(ctx) - - for sig := range h.signal { - h.logger.InfoContext(ctx, "received", "signal", sig) - - if osutil.IsReconfigureSignal(sig) { - err := h.reconfigure(ctx) - if err != nil { - h.logger.ErrorContext(ctx, "reconfiguration error", slogutil.KeyError, err) - - return osutil.ExitCodeFailure - } - } else if osutil.IsShutdownSignal(sig) { - status = h.shutdown(ctx) - - h.removePID(ctx) - - return status - } - } - - // Shouldn't happen, since h.signal is currently never closed. - panic("unexpected close of h.signal") -} - -// writePID writes the PID to the file, if needed. Any errors are reported to -// log. -func (h *signalHandler) writePID(ctx context.Context) { - if h.pidFile == "" { - return - } - - pid := os.Getpid() - data := strconv.AppendInt(nil, int64(pid), 10) - data = append(data, '\n') - - err := maybe.WriteFile(h.pidFile, data, 0o644) - if err != nil { - h.logger.ErrorContext(ctx, "writing pidfile", slogutil.KeyError, err) - - return - } - - h.logger.DebugContext(ctx, "wrote pid", "file", h.pidFile, "pid", pid) -} - -// reconfigure rereads the configuration file and updates and restarts services. -func (h *signalHandler) reconfigure(ctx context.Context) (err error) { - h.logger.InfoContext(ctx, "reconfiguring started") - - status := h.shutdown(ctx) - if status != osutil.ExitCodeSuccess { - return errors.Error("shutdown failed") - } - - // TODO(a.garipov): This is a very rough way to do it. Some services can - // be reconfigured without the full shutdown, and the error handling is - // currently not the best. - - var errs []error - - ctx, cancel := context.WithTimeout(ctx, defaultTimeoutStart) - defer cancel() - - confMgr, err := newConfigMgr(ctx, h.confMgrConf) - if err != nil { - errs = append(errs, fmt.Errorf("configuration manager: %w", err)) - } - - web := confMgr.Web() - err = web.Start(ctx) - if err != nil { - errs = append(errs, fmt.Errorf("starting web: %w", err)) - } - - dns := confMgr.DNS() - err = dns.Start(ctx) - if err != nil { - errs = append(errs, fmt.Errorf("starting dns: %w", err)) - } - - if len(errs) > 0 { - return errors.Join(errs...) - } - - h.services = []service.Interface{ - dns, - web, - } - - h.logger.InfoContext(ctx, "reconfiguring finished") - - return nil -} - -// shutdown gracefully shuts down all services. -func (h *signalHandler) shutdown(ctx context.Context) (status int) { - ctx, cancel := context.WithTimeout(ctx, h.shutdownTimeout) - defer cancel() - - status = osutil.ExitCodeSuccess - - h.logger.InfoContext(ctx, "shutting down") - for i, svc := range h.services { - err := svc.Shutdown(ctx) - if err != nil { - h.logger.ErrorContext(ctx, "shutting down service", "idx", i, slogutil.KeyError, err) - status = osutil.ExitCodeFailure - } - } - - return status -} - -// newSignalHandler returns a new signalHandler that shuts down svcs. logger -// and confMgrConf must not be nil. -func newSignalHandler( - logger *slog.Logger, - confMgrConf *configmgr.Config, - pidFile string, - svcs ...service.Interface, -) (h *signalHandler) { - h = &signalHandler{ - logger: logger, - confMgrConf: confMgrConf, - signal: make(chan os.Signal, 1), - pidFile: pidFile, - services: svcs, - shutdownTimeout: defaultTimeoutShutdown, - } - - notifier := osutil.DefaultSignalNotifier{} - osutil.NotifyShutdownSignal(notifier, h.signal) - osutil.NotifyReconfigureSignal(notifier, h.signal) - - return h -} - -// removePID removes the PID file, if any. -func (h *signalHandler) removePID(ctx context.Context) { - if h.pidFile == "" { - return - } - - err := os.Remove(h.pidFile) - if err != nil { - h.logger.ErrorContext(ctx, "removing pidfile", slogutil.KeyError, err) - - return - } - - h.logger.DebugContext(ctx, "removed pidfile", "file", h.pidFile) -} diff --git a/internal/next/websvc/websvc.go b/internal/next/websvc/websvc.go index 189d231e..3f709cd1 100644 --- a/internal/next/websvc/websvc.go +++ b/internal/next/websvc/websvc.go @@ -25,6 +25,8 @@ import ( ) // ConfigManager is the configuration manager interface. +// +// TODO(a.garipov): Add docs. type ConfigManager interface { DNS() (svc agh.ServiceWithConfig[*dnssvc.Config]) Web() (svc agh.ServiceWithConfig[*Config])