Pull request: hup-reload

Merge in DNS/adguard-home from hup-reload to master

Squashed commit of the following:

commit 5cd4ab85bdc7544a4eded2a61f5a5571175daa44
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Oct 7 19:58:17 2022 +0300

    next: imp signal hdlr

commit 8fd18e749fec46982d26fc408e661bd802586c37
Merge: a8780455 f1dd3334
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Oct 7 19:46:48 2022 +0300

    Merge branch 'master' into hup-reload

commit a87804550e15d7fe3d9ded2e5a736c395f96febd
Merge: 349dbe54 960a7a75
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Oct 7 15:49:23 2022 +0300

    Merge branch 'master' into hup-reload

commit 349dbe54fe27eeaf56776c73c3cc5649018d4c60
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Oct 7 15:43:52 2022 +0300

    next: imp docs, names

commit 7287a86d283489127453009267911003cea5227e
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Oct 7 13:39:44 2022 +0300

    WIP all: impl dynamic reconfiguration
This commit is contained in:
Ainar Garipov 2022-10-10 14:05:24 +03:00
parent f1dd33346a
commit f5602d9c46
18 changed files with 417 additions and 70 deletions

View File

@ -15,11 +15,11 @@ import (
// errFSOpen. // errFSOpen.
type errFS struct{} type errFS struct{}
// errFSOpen is returned from errGlobFS.Open. // errFSOpen is returned from errFS.Open.
const errFSOpen errors.Error = "test open error" const errFSOpen errors.Error = "test open error"
// Open implements the fs.FS interface for *errGlobFS. fsys is always nil and // Open implements the fs.FS interface for *errFS. fsys is always nil and err
// err is always errFSOpen. // is always errFSOpen.
func (efs *errFS) Open(name string) (fsys fs.File, err error) { func (efs *errFS) Open(name string) (fsys fs.File, err error) {
return nil, errFSOpen return nil, errFSOpen
} }

View File

@ -175,11 +175,21 @@ func RootDirFS() (fsys fs.FS) {
return os.DirFS("") return os.DirFS("")
} }
// NotifyReconfigureSignal notifies c on receiving reconfigure signals.
func NotifyReconfigureSignal(c chan<- os.Signal) {
notifyReconfigureSignal(c)
}
// NotifyShutdownSignal notifies c on receiving shutdown signals. // NotifyShutdownSignal notifies c on receiving shutdown signals.
func NotifyShutdownSignal(c chan<- os.Signal) { func NotifyShutdownSignal(c chan<- os.Signal) {
notifyShutdownSignal(c) notifyShutdownSignal(c)
} }
// IsReconfigureSignal returns true if sig is a reconfigure signal.
func IsReconfigureSignal(sig os.Signal) (ok bool) {
return isReconfigureSignal(sig)
}
// IsShutdownSignal returns true if sig is a shutdown signal. // IsShutdownSignal returns true if sig is a shutdown signal.
func IsShutdownSignal(sig os.Signal) (ok bool) { func IsShutdownSignal(sig os.Signal) (ok bool) {
return isShutdownSignal(sig) return isShutdownSignal(sig)

View File

@ -9,10 +9,18 @@ import (
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
func notifyReconfigureSignal(c chan<- os.Signal) {
signal.Notify(c, unix.SIGHUP)
}
func notifyShutdownSignal(c chan<- os.Signal) { func notifyShutdownSignal(c chan<- os.Signal) {
signal.Notify(c, unix.SIGINT, unix.SIGQUIT, unix.SIGTERM) signal.Notify(c, unix.SIGINT, unix.SIGQUIT, unix.SIGTERM)
} }
func isReconfigureSignal(sig os.Signal) (ok bool) {
return sig == unix.SIGHUP
}
func isShutdownSignal(sig os.Signal) (ok bool) { func isShutdownSignal(sig os.Signal) (ok bool) {
switch sig { switch sig {
case case

View File

@ -39,12 +39,20 @@ func isOpenWrt() (ok bool) {
return false return false
} }
func notifyReconfigureSignal(c chan<- os.Signal) {
signal.Notify(c, windows.SIGHUP)
}
func notifyShutdownSignal(c chan<- os.Signal) { func notifyShutdownSignal(c chan<- os.Signal) {
// syscall.SIGTERM is processed automatically. See go doc os/signal, // syscall.SIGTERM is processed automatically. See go doc os/signal,
// section Windows. // section Windows.
signal.Notify(c, os.Interrupt) signal.Notify(c, os.Interrupt)
} }
func isReconfigureSignal(sig os.Signal) (ok bool) {
return sig == windows.SIGHUP
}
func isShutdownSignal(sig os.Signal) (ok bool) { func isShutdownSignal(sig os.Signal) (ok bool) {
switch sig { switch sig {
case case

View File

@ -6,6 +6,7 @@ import (
"net" "net"
"github.com/AdguardTeam/AdGuardHome/internal/aghos" "github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
"github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/dnsproxy/upstream"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
@ -88,7 +89,7 @@ func (l *Listener) Close() (err error) {
return l.OnClose() return l.OnClose()
} }
// Module AdGuardHome // Module adguard-home
// Package aghos // Package aghos
@ -117,29 +118,31 @@ func (w *FSWatcher) Close() (err error) {
return w.OnClose() return w.OnClose()
} }
// Package websvc // Package agh
// ServiceWithConfig is a mock [websvc.ServiceWithConfig] implementation for // type check
// tests. var _ agh.ServiceWithConfig[struct{}] = (*ServiceWithConfig[struct{}])(nil)
// ServiceWithConfig is a mock [agh.ServiceWithConfig] implementation for tests.
type ServiceWithConfig[ConfigType any] struct { type ServiceWithConfig[ConfigType any] struct {
OnStart func() (err error) OnStart func() (err error)
OnShutdown func(ctx context.Context) (err error) OnShutdown func(ctx context.Context) (err error)
OnConfig func() (c ConfigType) OnConfig func() (c ConfigType)
} }
// Start implements the [websvc.ServiceWithConfig] interface for // Start implements the [agh.ServiceWithConfig] interface for
// *ServiceWithConfig. // *ServiceWithConfig.
func (s *ServiceWithConfig[_]) Start() (err error) { func (s *ServiceWithConfig[_]) Start() (err error) {
return s.OnStart() return s.OnStart()
} }
// Shutdown implements the [websvc.ServiceWithConfig] interface for // Shutdown implements the [agh.ServiceWithConfig] interface for
// *ServiceWithConfig. // *ServiceWithConfig.
func (s *ServiceWithConfig[_]) Shutdown(ctx context.Context) (err error) { func (s *ServiceWithConfig[_]) Shutdown(ctx context.Context) (err error) {
return s.OnShutdown(ctx) return s.OnShutdown(ctx)
} }
// Config implements the [websvc.ServiceWithConfig] interface for // Config implements the [agh.ServiceWithConfig] interface for
// *ServiceWithConfig. // *ServiceWithConfig.
func (s *ServiceWithConfig[ConfigType]) Config() (c ConfigType) { func (s *ServiceWithConfig[ConfigType]) Config() (c ConfigType) {
return s.OnConfig() return s.OnConfig()

View File

@ -1,9 +1,3 @@
package aghtest_test package aghtest_test
import ( // Put interface checks that cause import cycles here.
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
)
// type check
var _ websvc.ServiceWithConfig[struct{}] = (*aghtest.ServiceWithConfig[struct{}])(nil)

View File

@ -1,6 +1,4 @@
// Package agh contains common entities and interfaces of AdGuard Home. // Package agh contains common entities and interfaces of AdGuard Home.
//
// TODO(a.garipov): Move to the upper-level internal/.
package agh package agh
import "context" import "context"
@ -23,11 +21,43 @@ type Service interface {
// type check // type check
var _ Service = EmptyService{} var _ Service = EmptyService{}
// EmptyService is a Service that does nothing. // EmptyService is a [Service] that does nothing.
//
// TODO(a.garipov): Remove if unnecessary.
type EmptyService struct{} type EmptyService struct{}
// Start implements the Service interface for EmptyService. // Start implements the [Service] interface for EmptyService.
func (EmptyService) Start() (err error) { return nil } func (EmptyService) Start() (err error) { return nil }
// Shutdown implements the Service interface for EmptyService. // Shutdown implements the [Service] interface for EmptyService.
func (EmptyService) Shutdown(_ context.Context) (err error) { return nil } func (EmptyService) Shutdown(_ context.Context) (err error) { return nil }
// ServiceWithConfig is an extension of the [Service] interface for services
// that can return their configuration.
//
// TODO(a.garipov): Consider removing this generic interface if we figure out
// how to make it testable in a better way.
type ServiceWithConfig[ConfigType any] interface {
Service
Config() (c ConfigType)
}
// type check
var _ ServiceWithConfig[struct{}] = (*EmptyServiceWithConfig[struct{}])(nil)
// EmptyServiceWithConfig is a ServiceWithConfig that does nothing. Its Config
// method returns Conf.
//
// TODO(a.garipov): Remove if unnecessary.
type EmptyServiceWithConfig[ConfigType any] struct {
EmptyService
Conf ConfigType
}
// Config implements the [ServiceWithConfig] interface for
// *EmptyServiceWithConfig.
func (s *EmptyServiceWithConfig[ConfigType]) Config() (conf ConfigType) {
return s.Conf
}

View File

@ -8,10 +8,11 @@ import (
"context" "context"
"io/fs" "io/fs"
"math/rand" "math/rand"
"net/netip" "os"
"time" "time"
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc" "github.com/AdguardTeam/AdGuardHome/internal/next/configmgr"
"github.com/AdguardTeam/AdGuardHome/internal/version"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
) )
@ -24,26 +25,32 @@ func Main(clientBuildFS fs.FS) {
// TODO(a.garipov): Set up logging. // TODO(a.garipov): Set up logging.
log.Info("starting adguard home, version %s, pid %d", version.Version(), os.Getpid())
// Web Service // Web Service
// TODO(a.garipov): Use in the Web service. // TODO(a.garipov): Use in the Web service.
_ = clientBuildFS _ = clientBuildFS
// TODO(a.garipov): Make configurable. // TODO(a.garipov): Set up configuration file name.
web := websvc.New(&websvc.Config{ const confFile = "AdGuardHome.1.yaml"
// TODO(a.garipov): Use an actual implementation.
ConfigManager: nil,
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:3001")},
Start: start,
Timeout: 60 * time.Second,
ForceHTTPS: false,
})
err := web.Start() confMgr, err := configmgr.New(confFile, start)
fatalOnError(err)
web := confMgr.Web()
err = web.Start()
fatalOnError(err)
dns := confMgr.DNS()
err = dns.Start()
fatalOnError(err) fatalOnError(err)
sigHdlr := newSignalHandler( sigHdlr := newSignalHandler(
confFile,
start,
web, web,
dns,
) )
go sigHdlr.handle() go sigHdlr.handle()

View File

@ -2,18 +2,26 @@ package cmd
import ( import (
"os" "os"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghos" "github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/AdGuardHome/internal/next/agh" "github.com/AdguardTeam/AdGuardHome/internal/next/agh"
"github.com/AdguardTeam/AdGuardHome/internal/next/configmgr"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
) )
// signalHandler processes incoming signals and shuts services down. // signalHandler processes incoming signals and shuts services down.
type signalHandler struct { type signalHandler struct {
// signal is the channel to which OS signals are sent.
signal chan os.Signal signal chan os.Signal
// services are the services that are shut down before application // confFile is the path to the configuration file.
// exiting. confFile string
// start is the time at which AdGuard Home has been started.
start time.Time
// services are the services that are shut down before application exiting.
services []agh.Service services []agh.Service
} }
@ -24,12 +32,51 @@ func (h *signalHandler) handle() {
for sig := range h.signal { for sig := range h.signal {
log.Info("sighdlr: received signal %q", sig) log.Info("sighdlr: received signal %q", sig)
if aghos.IsShutdownSignal(sig) { if aghos.IsReconfigureSignal(sig) {
h.shutdown() h.reconfigure()
} else if aghos.IsShutdownSignal(sig) {
status := h.shutdown()
log.Info("sighdlr: exiting with status %d", status)
os.Exit(status)
} }
} }
} }
// reconfigure rereads the configuration file and updates and restarts services.
func (h *signalHandler) reconfigure() {
log.Info("sighdlr: reconfiguring adguard home")
status := h.shutdown()
if status != statusSuccess {
log.Info("sighdlr: reconfiruging: exiting with status %d", status)
os.Exit(status)
}
// 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.
confMgr, err := configmgr.New(h.confFile, h.start)
fatalOnError(err)
web := confMgr.Web()
err = web.Start()
fatalOnError(err)
dns := confMgr.DNS()
err = dns.Start()
fatalOnError(err)
h.services = []agh.Service{
dns,
web,
}
log.Info("sighdlr: successfully reconfigured adguard home")
}
// Exit status constants. // Exit status constants.
const ( const (
statusSuccess = 0 statusSuccess = 0
@ -37,11 +84,11 @@ const (
) )
// shutdown gracefully shuts down all services. // shutdown gracefully shuts down all services.
func (h *signalHandler) shutdown() { func (h *signalHandler) shutdown() (status int) {
ctx, cancel := ctxWithDefaultTimeout() ctx, cancel := ctxWithDefaultTimeout()
defer cancel() defer cancel()
status := statusSuccess status = statusSuccess
log.Info("sighdlr: shutting down services") log.Info("sighdlr: shutting down services")
for i, service := range h.services { for i, service := range h.services {
@ -52,19 +99,20 @@ func (h *signalHandler) shutdown() {
} }
} }
log.Info("sighdlr: shutting down adguard home") return status
os.Exit(status)
} }
// newSignalHandler returns a new signalHandler that shuts down svcs. // newSignalHandler returns a new signalHandler that shuts down svcs.
func newSignalHandler(svcs ...agh.Service) (h *signalHandler) { func newSignalHandler(confFile string, start time.Time, svcs ...agh.Service) (h *signalHandler) {
h = &signalHandler{ h = &signalHandler{
signal: make(chan os.Signal, 1), signal: make(chan os.Signal, 1),
confFile: confFile,
start: start,
services: svcs, services: svcs,
} }
aghos.NotifyShutdownSignal(h.signal) aghos.NotifyShutdownSignal(h.signal)
aghos.NotifyReconfigureSignal(h.signal)
return h return h
} }

View File

@ -0,0 +1,40 @@
package configmgr
import (
"net/netip"
"github.com/AdguardTeam/golibs/timeutil"
)
// Configuration Structures
// config is the top-level on-disk configuration structure.
type config struct {
DNS *dnsConfig `yaml:"dns"`
HTTP *httpConfig `yaml:"http"`
// TODO(a.garipov): Use.
SchemaVersion int `yaml:"schema_version"`
// TODO(a.garipov): Use.
DebugPprof bool `yaml:"debug_pprof"`
Verbose bool `yaml:"verbose"`
}
// dnsConfig is the on-disk DNS configuration.
//
// TODO(a.garipov): Validate.
type dnsConfig struct {
Addresses []netip.AddrPort `yaml:"addresses"`
BootstrapDNS []string `yaml:"bootstrap_dns"`
UpstreamDNS []string `yaml:"upstream_dns"`
UpstreamTimeout timeutil.Duration `yaml:"upstream_timeout"`
}
// httpConfig is the on-disk web API configuration.
//
// TODO(a.garipov): Validate.
type httpConfig struct {
Addresses []netip.AddrPort `yaml:"addresses"`
SecureAddresses []netip.AddrPort `yaml:"secure_addresses"`
Timeout timeutil.Duration `yaml:"timeout"`
ForceHTTPS bool `yaml:"force_https"`
}

View File

@ -0,0 +1,205 @@
// Package configmgr defines the AdGuard Home on-disk configuration entities and
// configuration manager.
package configmgr
import (
"context"
"fmt"
"os"
"sync"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"gopkg.in/yaml.v3"
)
// Configuration Manager
// Manager handles full and partial changes in the configuration, persisting
// them to disk if necessary.
type Manager struct {
// updMu makes sure that at most one reconfiguration is performed at a time.
// updMu protects all fields below.
updMu *sync.RWMutex
// dns is the DNS service.
dns *dnssvc.Service
// Web is the Web API service.
web *websvc.Service
// current is the current configuration.
current *config
// fileName is the name of the configuration file.
fileName string
}
// New creates a new *Manager that persists changes to the file pointed to by
// fileName. It reads the configuration file and populates the service fields.
// start is the startup time of AdGuard Home.
func New(fileName string, start time.Time) (m *Manager, err error) {
defer func() { err = errors.Annotate(err, "reading config") }()
conf := &config{}
f, err := os.Open(fileName)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
defer func() { err = errors.WithDeferred(err, f.Close()) }()
err = yaml.NewDecoder(f).Decode(conf)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
// TODO(a.garipov): Move into a separate function and add other logging
// settings.
if conf.Verbose {
log.SetLevel(log.DEBUG)
}
// TODO(a.garipov): Validate the configuration structure. Return an error
// if it's incorrect.
m = &Manager{
updMu: &sync.RWMutex{},
current: conf,
fileName: fileName,
}
// TODO(a.garipov): Get the context with the timeout from the arguments?
const assemblyTimeout = 5 * time.Second
ctx, cancel := context.WithTimeout(context.Background(), assemblyTimeout)
defer cancel()
err = m.assemble(ctx, conf, start)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
return m, nil
}
// assemble creates all services and puts them into the corresponding fields.
// The fields of conf must not be modified after calling assemble.
func (m *Manager) assemble(ctx context.Context, conf *config, start time.Time) (err error) {
dnsConf := &dnssvc.Config{
Addresses: conf.DNS.Addresses,
BootstrapServers: conf.DNS.BootstrapDNS,
UpstreamServers: conf.DNS.UpstreamDNS,
UpstreamTimeout: conf.DNS.UpstreamTimeout.Duration,
}
err = m.updateDNS(ctx, dnsConf)
if err != nil {
return fmt.Errorf("assembling dnssvc: %w", err)
}
webSvcConf := &websvc.Config{
ConfigManager: m,
// TODO(a.garipov): Fill from config file.
TLS: nil,
Start: start,
Addresses: conf.HTTP.Addresses,
SecureAddresses: conf.HTTP.SecureAddresses,
Timeout: conf.HTTP.Timeout.Duration,
ForceHTTPS: conf.HTTP.ForceHTTPS,
}
err = m.updateWeb(ctx, webSvcConf)
if err != nil {
return fmt.Errorf("assembling websvc: %w", err)
}
return nil
}
// DNS returns the current DNS service. It is safe for concurrent use.
func (m *Manager) DNS() (dns agh.ServiceWithConfig[*dnssvc.Config]) {
m.updMu.RLock()
defer m.updMu.RUnlock()
return m.dns
}
// UpdateDNS implements the [websvc.ConfigManager] interface for *Manager. The
// fields of c must not be modified after calling UpdateDNS.
func (m *Manager) UpdateDNS(ctx context.Context, c *dnssvc.Config) (err error) {
m.updMu.Lock()
defer m.updMu.Unlock()
// TODO(a.garipov): Update and write the configuration file. Return an
// error if something went wrong.
err = m.updateDNS(ctx, c)
if err != nil {
return fmt.Errorf("reassembling dnssvc: %w", err)
}
return nil
}
// updateDNS recreates the DNS service. m.updMu is expected to be locked.
func (m *Manager) updateDNS(ctx context.Context, c *dnssvc.Config) (err error) {
if prev := m.dns; prev != nil {
err = prev.Shutdown(ctx)
if err != nil {
return fmt.Errorf("shutting down dns svc: %w", err)
}
}
svc, err := dnssvc.New(c)
if err != nil {
return fmt.Errorf("creating dns svc: %w", err)
}
m.dns = svc
return nil
}
// Web returns the current web service. It is safe for concurrent use.
func (m *Manager) Web() (web agh.ServiceWithConfig[*websvc.Config]) {
m.updMu.RLock()
defer m.updMu.RUnlock()
return m.web
}
// UpdateWeb implements the [websvc.ConfigManager] interface for *Manager. The
// fields of c must not be modified after calling UpdateWeb.
func (m *Manager) UpdateWeb(ctx context.Context, c *websvc.Config) (err error) {
m.updMu.Lock()
defer m.updMu.Unlock()
// TODO(a.garipov): Update and write the configuration file. Return an
// error if something went wrong.
err = m.updateWeb(ctx, c)
if err != nil {
return fmt.Errorf("reassembling websvc: %w", err)
}
return nil
}
// updateWeb recreates the web service. m.upd is expected to be locked.
func (m *Manager) updateWeb(ctx context.Context, c *websvc.Config) (err error) {
if prev := m.web; prev != nil {
err = prev.Shutdown(ctx)
if err != nil {
return fmt.Errorf("shutting down web svc: %w", err)
}
}
m.web = websvc.New(c)
return nil
}

View File

@ -11,6 +11,7 @@ import (
"time" "time"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest" "github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc" "github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc" "github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -28,7 +29,7 @@ func TestService_HandlePatchSettingsDNS(t *testing.T) {
// TODO(a.garipov): Use [atomic.Bool] in Go 1.19. // TODO(a.garipov): Use [atomic.Bool] in Go 1.19.
var numStarted uint64 var numStarted uint64
confMgr := newConfigManager() confMgr := newConfigManager()
confMgr.onDNS = func() (s websvc.ServiceWithConfig[*dnssvc.Config]) { confMgr.onDNS = func() (s agh.ServiceWithConfig[*dnssvc.Config]) {
return &aghtest.ServiceWithConfig[*dnssvc.Config]{ return &aghtest.ServiceWithConfig[*dnssvc.Config]{
OnStart: func() (err error) { OnStart: func() (err error) {
atomic.AddUint64(&numStarted, 1) atomic.AddUint64(&numStarted, 1)

View File

@ -8,6 +8,7 @@ import (
"net/netip" "net/netip"
"time" "time"
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
) )
@ -89,7 +90,7 @@ func (svc *Service) handlePatchSettingsHTTP(w http.ResponseWriter, r *http.Reque
// TODO(a.garipov): Consider better ways to do this. // TODO(a.garipov): Consider better ways to do this.
const maxUpdDur = 10 * time.Second const maxUpdDur = 10 * time.Second
updStart := time.Now() updStart := time.Now()
var newSvc ServiceWithConfig[*Config] var newSvc agh.ServiceWithConfig[*Config]
for newSvc = svc.confMgr.Web(); newSvc == svc; { for newSvc = svc.confMgr.Web(); newSvc == svc; {
if time.Since(updStart) >= maxUpdDur { if time.Since(updStart) >= maxUpdDur {
log.Error("websvc: failed to update svc after %s", maxUpdDur) log.Error("websvc: failed to update svc after %s", maxUpdDur)

View File

@ -10,6 +10,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc" "github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -24,7 +25,7 @@ func TestService_HandlePatchSettingsHTTP(t *testing.T) {
} }
confMgr := newConfigManager() confMgr := newConfigManager()
confMgr.onWeb = func() (s websvc.ServiceWithConfig[*websvc.Config]) { confMgr.onWeb = func() (s agh.ServiceWithConfig[*websvc.Config]) {
return websvc.New(&websvc.Config{ return websvc.New(&websvc.Config{
TLS: &tls.Config{ TLS: &tls.Config{
Certificates: []tls.Certificate{{}}, Certificates: []tls.Certificate{{}},

View File

@ -9,6 +9,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc" "github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc" "github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -33,7 +34,7 @@ func TestService_HandleGetSettingsAll(t *testing.T) {
} }
confMgr := newConfigManager() confMgr := newConfigManager()
confMgr.onDNS = func() (s websvc.ServiceWithConfig[*dnssvc.Config]) { confMgr.onDNS = func() (s agh.ServiceWithConfig[*dnssvc.Config]) {
c, err := dnssvc.New(&dnssvc.Config{ c, err := dnssvc.New(&dnssvc.Config{
Addresses: wantDNS.Addresses, Addresses: wantDNS.Addresses,
UpstreamServers: wantDNS.UpstreamServers, UpstreamServers: wantDNS.UpstreamServers,
@ -45,7 +46,7 @@ func TestService_HandleGetSettingsAll(t *testing.T) {
return c return c
} }
confMgr.onWeb = func() (s websvc.ServiceWithConfig[*websvc.Config]) { confMgr.onWeb = func() (s agh.ServiceWithConfig[*websvc.Config]) {
return websvc.New(&websvc.Config{ return websvc.New(&websvc.Config{
TLS: &tls.Config{ TLS: &tls.Config{
Certificates: []tls.Certificate{{}}, Certificates: []tls.Certificate{{}},

View File

@ -24,21 +24,10 @@ import (
httptreemux "github.com/dimfeld/httptreemux/v5" httptreemux "github.com/dimfeld/httptreemux/v5"
) )
// ServiceWithConfig is an extension of the [agh.Service] interface for services
// that can return their configuration.
//
// TODO(a.garipov): Consider removing this generic interface if we figure out
// how to make it testable in a better way.
type ServiceWithConfig[ConfigType any] interface {
agh.Service
Config() (c ConfigType)
}
// ConfigManager is the configuration manager interface. // ConfigManager is the configuration manager interface.
type ConfigManager interface { type ConfigManager interface {
DNS() (svc ServiceWithConfig[*dnssvc.Config]) DNS() (svc agh.ServiceWithConfig[*dnssvc.Config])
Web() (svc ServiceWithConfig[*Config]) Web() (svc agh.ServiceWithConfig[*Config])
UpdateDNS(ctx context.Context, c *dnssvc.Config) (err error) UpdateDNS(ctx context.Context, c *dnssvc.Config) (err error)
UpdateWeb(ctx context.Context, c *Config) (err error) UpdateWeb(ctx context.Context, c *Config) (err error)

View File

@ -12,6 +12,7 @@ import (
"time" "time"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest" "github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc" "github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc" "github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
"github.com/AdguardTeam/golibs/testutil" "github.com/AdguardTeam/golibs/testutil"
@ -34,20 +35,20 @@ var _ websvc.ConfigManager = (*configManager)(nil)
// configManager is a [websvc.ConfigManager] for tests. // configManager is a [websvc.ConfigManager] for tests.
type configManager struct { type configManager struct {
onDNS func() (svc websvc.ServiceWithConfig[*dnssvc.Config]) onDNS func() (svc agh.ServiceWithConfig[*dnssvc.Config])
onWeb func() (svc websvc.ServiceWithConfig[*websvc.Config]) onWeb func() (svc agh.ServiceWithConfig[*websvc.Config])
onUpdateDNS func(ctx context.Context, c *dnssvc.Config) (err error) onUpdateDNS func(ctx context.Context, c *dnssvc.Config) (err error)
onUpdateWeb func(ctx context.Context, c *websvc.Config) (err error) onUpdateWeb func(ctx context.Context, c *websvc.Config) (err error)
} }
// DNS implements the [websvc.ConfigManager] interface for *configManager. // DNS implements the [websvc.ConfigManager] interface for *configManager.
func (m *configManager) DNS() (svc websvc.ServiceWithConfig[*dnssvc.Config]) { func (m *configManager) DNS() (svc agh.ServiceWithConfig[*dnssvc.Config]) {
return m.onDNS() return m.onDNS()
} }
// Web implements the [websvc.ConfigManager] interface for *configManager. // Web implements the [websvc.ConfigManager] interface for *configManager.
func (m *configManager) Web() (svc websvc.ServiceWithConfig[*websvc.Config]) { func (m *configManager) Web() (svc agh.ServiceWithConfig[*websvc.Config]) {
return m.onWeb() return m.onWeb()
} }
@ -64,8 +65,8 @@ func (m *configManager) UpdateWeb(ctx context.Context, c *websvc.Config) (err er
// newConfigManager returns a *configManager all methods of which panic. // newConfigManager returns a *configManager all methods of which panic.
func newConfigManager() (m *configManager) { func newConfigManager() (m *configManager) {
return &configManager{ return &configManager{
onDNS: func() (svc websvc.ServiceWithConfig[*dnssvc.Config]) { panic("not implemented") }, onDNS: func() (svc agh.ServiceWithConfig[*dnssvc.Config]) { panic("not implemented") },
onWeb: func() (svc websvc.ServiceWithConfig[*websvc.Config]) { panic("not implemented") }, onWeb: func() (svc agh.ServiceWithConfig[*websvc.Config]) { panic("not implemented") },
onUpdateDNS: func(_ context.Context, _ *dnssvc.Config) (err error) { onUpdateDNS: func(_ context.Context, _ *dnssvc.Config) (err error) {
panic("not implemented") panic("not implemented")
}, },

View File

@ -124,11 +124,11 @@ GO111MODULE='on'
export CGO_ENABLED GO111MODULE export CGO_ENABLED GO111MODULE
# Build the new binary if requested. # Build the new binary if requested.
if [ "${V1API:-0}" -eq '0' ] if [ "${NEXTAPI:-0}" -eq '0' ]
then then
tags_flags='--tags=' tags_flags='--tags='
else else
tags_flags='--tags=v1' tags_flags='--tags=next'
fi fi
readonly tags_flags readonly tags_flags