all: sync with master
This commit is contained in:
@@ -12,11 +12,15 @@ import (
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/configmgr"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
"github.com/AdguardTeam/golibs/service"
|
||||
)
|
||||
|
||||
// Main is the entry point of AdGuard Home.
|
||||
func Main(embeddedFrontend fs.FS) {
|
||||
ctx := context.Background()
|
||||
|
||||
start := time.Now()
|
||||
|
||||
cmdName := os.Args[0]
|
||||
@@ -26,70 +30,69 @@ func Main(embeddedFrontend fs.FS) {
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
|
||||
err = setLog(opts)
|
||||
check(err)
|
||||
baseLogger := newBaseLogger(opts)
|
||||
|
||||
log.Info("starting adguard home, version %s, pid %d", version.Version(), os.Getpid())
|
||||
baseLogger.InfoContext(
|
||||
ctx,
|
||||
"starting adguard home",
|
||||
"version", version.Version(),
|
||||
"pid", os.Getpid(),
|
||||
)
|
||||
|
||||
if opts.workDir != "" {
|
||||
log.Info("changing working directory to %q", opts.workDir)
|
||||
baseLogger.InfoContext(ctx, "changing working directory", "dir", opts.workDir)
|
||||
|
||||
err = os.Chdir(opts.workDir)
|
||||
check(err)
|
||||
errors.Check(err)
|
||||
}
|
||||
|
||||
frontend, err := frontendFromOpts(opts, embeddedFrontend)
|
||||
check(err)
|
||||
frontend, err := frontendFromOpts(ctx, baseLogger, opts, embeddedFrontend)
|
||||
errors.Check(err)
|
||||
|
||||
startCtx, startCancel := context.WithTimeout(ctx, defaultTimeoutStart)
|
||||
defer startCancel()
|
||||
|
||||
confMgrConf := &configmgr.Config{
|
||||
Frontend: frontend,
|
||||
WebAddr: opts.webAddr,
|
||||
Start: start,
|
||||
FileName: opts.confFile,
|
||||
BaseLogger: baseLogger,
|
||||
Logger: baseLogger.With(slogutil.KeyPrefix, "configmgr"),
|
||||
Frontend: frontend,
|
||||
WebAddr: opts.webAddr,
|
||||
Start: start,
|
||||
FileName: opts.confFile,
|
||||
}
|
||||
|
||||
confMgr, err := newConfigMgr(confMgrConf)
|
||||
check(err)
|
||||
confMgr, err := configmgr.New(startCtx, confMgrConf)
|
||||
errors.Check(err)
|
||||
|
||||
web := confMgr.Web()
|
||||
err = web.Start()
|
||||
check(err)
|
||||
err = web.Start(startCtx)
|
||||
errors.Check(err)
|
||||
|
||||
dns := confMgr.DNS()
|
||||
err = dns.Start()
|
||||
check(err)
|
||||
err = dns.Start(startCtx)
|
||||
errors.Check(err)
|
||||
|
||||
sigHdlr := newSignalHandler(
|
||||
baseLogger.With(slogutil.KeyPrefix, service.SignalHandlerPrefix),
|
||||
confMgrConf,
|
||||
opts.pidFile,
|
||||
web,
|
||||
dns,
|
||||
)
|
||||
|
||||
sigHdlr.handle()
|
||||
os.Exit(sigHdlr.handle(ctx))
|
||||
}
|
||||
|
||||
// defaultTimeout is the timeout used for some operations where another timeout
|
||||
// hasn't been defined yet.
|
||||
const defaultTimeout = 5 * time.Second
|
||||
|
||||
// ctxWithDefaultTimeout is a helper function that returns a context with
|
||||
// timeout set to defaultTimeout.
|
||||
func ctxWithDefaultTimeout() (ctx context.Context, cancel context.CancelFunc) {
|
||||
return context.WithTimeout(context.Background(), defaultTimeout)
|
||||
}
|
||||
// Default timeouts.
|
||||
//
|
||||
// TODO(a.garipov): Make configurable.
|
||||
const (
|
||||
defaultTimeoutStart = 1 * time.Minute
|
||||
defaultTimeoutShutdown = 5 * time.Second
|
||||
)
|
||||
|
||||
// newConfigMgr returns a new configuration manager using defaultTimeout as the
|
||||
// context timeout.
|
||||
func newConfigMgr(c *configmgr.Config) (m *configmgr.Manager, err error) {
|
||||
ctx, cancel := ctxWithDefaultTimeout()
|
||||
defer cancel()
|
||||
|
||||
func newConfigMgr(ctx context.Context, c *configmgr.Config) (m *configmgr.Manager, err error) {
|
||||
return configmgr.New(ctx, c)
|
||||
}
|
||||
|
||||
// check is a simple error-checking helper. It must only be used within Main.
|
||||
func check(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
)
|
||||
|
||||
// syslogServiceName is the name of the AdGuard Home service used for writing
|
||||
// logs to the system log.
|
||||
const syslogServiceName = "AdGuardHome"
|
||||
|
||||
// setLog sets up the text logging.
|
||||
//
|
||||
// TODO(a.garipov): Add parameters from configuration file.
|
||||
func setLog(opts *options) (err error) {
|
||||
// newBaseLogger constructs a base logger based on the command-line options.
|
||||
// opts must not be nil.
|
||||
func newBaseLogger(opts *options) (baseLogger *slog.Logger) {
|
||||
var output io.Writer
|
||||
switch opts.confFile {
|
||||
case "stdout":
|
||||
log.SetOutput(os.Stdout)
|
||||
output = os.Stdout
|
||||
case "stderr":
|
||||
log.SetOutput(os.Stderr)
|
||||
output = os.Stderr
|
||||
case "syslog":
|
||||
err = aghos.ConfigureSyslog(syslogServiceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("initializing syslog: %w", err)
|
||||
}
|
||||
// TODO(a.garipov): Add a syslog handler to golibs.
|
||||
default:
|
||||
// TODO(a.garipov): Use the path.
|
||||
// TODO(a.garipov): Use the path.
|
||||
}
|
||||
|
||||
lvl := slog.LevelInfo
|
||||
if opts.verbose {
|
||||
log.SetLevel(log.DEBUG)
|
||||
log.Debug("verbose logging enabled")
|
||||
lvl = slog.LevelDebug
|
||||
}
|
||||
|
||||
return nil
|
||||
return slogutil.New(&slogutil.Config{
|
||||
Output: output,
|
||||
// TODO(a.garipov): Get from config?
|
||||
Format: slogutil.FormatText,
|
||||
Level: lvl,
|
||||
// TODO(a.garipov): Get from config.
|
||||
AddTimestamp: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"net/netip"
|
||||
"os"
|
||||
"slices"
|
||||
@@ -14,7 +16,7 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/configmigrate"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/configmgr"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/osutil"
|
||||
)
|
||||
|
||||
// options contains all command-line options for the AdGuardHome(.exe) binary.
|
||||
@@ -87,6 +89,12 @@ type options struct {
|
||||
// TODO(a.garipov): Use.
|
||||
performUpdate bool
|
||||
|
||||
// noPermCheck, if true, instructs AdGuard Home to skip checking and
|
||||
// migrating the permissions of its security-sensitive files.
|
||||
//
|
||||
// TODO(e.burkov): Use.
|
||||
noPermCheck bool
|
||||
|
||||
// verbose, if true, instructs AdGuard Home to enable verbose logging.
|
||||
verbose bool
|
||||
|
||||
@@ -108,7 +116,8 @@ const (
|
||||
disableUpdateIdx
|
||||
glinetModeIdx
|
||||
helpIdx
|
||||
localFrontend
|
||||
localFrontendIdx
|
||||
noPermCheckIdx
|
||||
performUpdateIdx
|
||||
verboseIdx
|
||||
versionIdx
|
||||
@@ -212,7 +221,7 @@ var commandLineOptions = []*commandLineOption{
|
||||
valueType: "",
|
||||
},
|
||||
|
||||
localFrontend: {
|
||||
localFrontendIdx: {
|
||||
defaultValue: false,
|
||||
description: "Use local frontend directories.",
|
||||
long: "local-frontend",
|
||||
@@ -220,6 +229,14 @@ var commandLineOptions = []*commandLineOption{
|
||||
valueType: "",
|
||||
},
|
||||
|
||||
noPermCheckIdx: {
|
||||
defaultValue: false,
|
||||
description: "Skip checking the permissions of security-sensitive files.",
|
||||
long: "no-permcheck",
|
||||
short: "",
|
||||
valueType: "",
|
||||
},
|
||||
|
||||
performUpdateIdx: {
|
||||
defaultValue: false,
|
||||
description: "Update the current binary and restart the service in case it's installed.",
|
||||
@@ -262,7 +279,8 @@ func parseOptions(cmdName string, args []string) (opts *options, err error) {
|
||||
disableUpdateIdx: &opts.disableUpdate,
|
||||
glinetModeIdx: &opts.glinetMode,
|
||||
helpIdx: &opts.help,
|
||||
localFrontend: &opts.localFrontend,
|
||||
localFrontendIdx: &opts.localFrontend,
|
||||
noPermCheckIdx: &opts.noPermCheck,
|
||||
performUpdateIdx: &opts.performUpdate,
|
||||
verboseIdx: &opts.verbose,
|
||||
versionIdx: &opts.version,
|
||||
@@ -372,13 +390,13 @@ func processOptions(
|
||||
) (exitCode int, needExit bool) {
|
||||
if parseErr != nil {
|
||||
// Assume that usage has already been printed.
|
||||
return statusArgumentError, true
|
||||
return osutil.ExitCodeArgumentError, true
|
||||
}
|
||||
|
||||
if opts.help {
|
||||
usage(cmdName, os.Stdout)
|
||||
|
||||
return statusSuccess, true
|
||||
return osutil.ExitCodeSuccess, true
|
||||
}
|
||||
|
||||
if opts.version {
|
||||
@@ -388,7 +406,7 @@ func processOptions(
|
||||
fmt.Printf("AdGuard Home %s\n", version.Version())
|
||||
}
|
||||
|
||||
return statusSuccess, true
|
||||
return osutil.ExitCodeSuccess, true
|
||||
}
|
||||
|
||||
if opts.checkConfig {
|
||||
@@ -396,21 +414,26 @@ func processOptions(
|
||||
if err != nil {
|
||||
_, _ = io.WriteString(os.Stdout, err.Error()+"\n")
|
||||
|
||||
return statusError, true
|
||||
return osutil.ExitCodeFailure, true
|
||||
}
|
||||
|
||||
return statusSuccess, true
|
||||
return osutil.ExitCodeSuccess, true
|
||||
}
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// frontendFromOpts returns the frontend to use based on the options.
|
||||
func frontendFromOpts(opts *options, embeddedFrontend fs.FS) (frontend fs.FS, err error) {
|
||||
func frontendFromOpts(
|
||||
ctx context.Context,
|
||||
logger *slog.Logger,
|
||||
opts *options,
|
||||
embeddedFrontend fs.FS,
|
||||
) (frontend fs.FS, err error) {
|
||||
const frontendSubdir = "build/static"
|
||||
|
||||
if opts.localFrontend {
|
||||
log.Info("warning: using local frontend files")
|
||||
logger.WarnContext(ctx, "using local frontend files")
|
||||
|
||||
return os.DirFS(frontendSubdir), nil
|
||||
}
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/configmgr"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"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
|
||||
@@ -24,145 +32,172 @@ type signalHandler struct {
|
||||
pidFile string
|
||||
|
||||
// services are the services that are shut down before application exiting.
|
||||
services []agh.Service
|
||||
services []service.Interface
|
||||
|
||||
// shutdownTimeout is the timeout for the shutdown operation.
|
||||
shutdownTimeout time.Duration
|
||||
}
|
||||
|
||||
// handle processes OS signals.
|
||||
func (h *signalHandler) handle() {
|
||||
defer log.OnPanic("signalHandler.handle")
|
||||
// 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()
|
||||
h.writePID(ctx)
|
||||
|
||||
for sig := range h.signal {
|
||||
log.Info("sighdlr: received signal %q", sig)
|
||||
h.logger.InfoContext(ctx, "received", "signal", sig)
|
||||
|
||||
if aghos.IsReconfigureSignal(sig) {
|
||||
h.reconfigure()
|
||||
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()
|
||||
h.removePID()
|
||||
status = h.shutdown(ctx)
|
||||
|
||||
log.Info("sighdlr: exiting with status %d", status)
|
||||
h.removePID(ctx)
|
||||
|
||||
os.Exit(status)
|
||||
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() {
|
||||
log.Info("sighdlr: reconfiguring adguard home")
|
||||
func (h *signalHandler) reconfigure(ctx context.Context) (err error) {
|
||||
h.logger.InfoContext(ctx, "reconfiguring started")
|
||||
|
||||
status := h.shutdown()
|
||||
if status != statusSuccess {
|
||||
log.Info("sighdlr: reconfiguring: exiting with status %d", status)
|
||||
|
||||
os.Exit(status)
|
||||
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
|
||||
// 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 := newConfigMgr(h.confMgrConf)
|
||||
check(err)
|
||||
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()
|
||||
check(err)
|
||||
err = web.Start(ctx)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("starting web: %w", err))
|
||||
}
|
||||
|
||||
dns := confMgr.DNS()
|
||||
err = dns.Start()
|
||||
check(err)
|
||||
err = dns.Start(ctx)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("starting dns: %w", err))
|
||||
}
|
||||
|
||||
h.services = []agh.Service{
|
||||
if len(errs) > 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
h.services = []service.Interface{
|
||||
dns,
|
||||
web,
|
||||
}
|
||||
|
||||
log.Info("sighdlr: successfully reconfigured adguard home")
|
||||
h.logger.InfoContext(ctx, "reconfiguring finished")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Exit status constants.
|
||||
const (
|
||||
statusSuccess = 0
|
||||
statusError = 1
|
||||
statusArgumentError = 2
|
||||
)
|
||||
|
||||
// shutdown gracefully shuts down all services.
|
||||
func (h *signalHandler) shutdown() (status int) {
|
||||
ctx, cancel := ctxWithDefaultTimeout()
|
||||
func (h *signalHandler) shutdown(ctx context.Context) (status int) {
|
||||
ctx, cancel := context.WithTimeout(ctx, h.shutdownTimeout)
|
||||
defer cancel()
|
||||
|
||||
status = statusSuccess
|
||||
status = osutil.ExitCodeSuccess
|
||||
|
||||
log.Info("sighdlr: shutting down services")
|
||||
for i, service := range h.services {
|
||||
err := service.Shutdown(ctx)
|
||||
h.logger.InfoContext(ctx, "shutting down")
|
||||
for i, svc := range h.services {
|
||||
err := svc.Shutdown(ctx)
|
||||
if err != nil {
|
||||
log.Error("sighdlr: shutting down service at index %d: %s", i, err)
|
||||
status = statusError
|
||||
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.
|
||||
// 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 ...agh.Service,
|
||||
svcs ...service.Interface,
|
||||
) (h *signalHandler) {
|
||||
h = &signalHandler{
|
||||
confMgrConf: confMgrConf,
|
||||
signal: make(chan os.Signal, 1),
|
||||
pidFile: pidFile,
|
||||
services: svcs,
|
||||
logger: logger,
|
||||
confMgrConf: confMgrConf,
|
||||
signal: make(chan os.Signal, 1),
|
||||
pidFile: pidFile,
|
||||
services: svcs,
|
||||
shutdownTimeout: defaultTimeoutShutdown,
|
||||
}
|
||||
|
||||
notifier := osutil.DefaultSignalNotifier{}
|
||||
osutil.NotifyShutdownSignal(notifier, h.signal)
|
||||
aghos.NotifyReconfigureSignal(h.signal)
|
||||
osutil.NotifyReconfigureSignal(notifier, h.signal)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// writePID writes the PID to the file, if needed. Any errors are reported to
|
||||
// log.
|
||||
func (h *signalHandler) writePID() {
|
||||
if h.pidFile == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Use 8, since most PIDs will fit.
|
||||
data := make([]byte, 0, 8)
|
||||
data = strconv.AppendInt(data, int64(os.Getpid()), 10)
|
||||
data = append(data, '\n')
|
||||
|
||||
err := aghos.WriteFile(h.pidFile, data, 0o644)
|
||||
if err != nil {
|
||||
log.Error("sighdlr: writing pidfile: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("sighdlr: wrote pid to %q", h.pidFile)
|
||||
}
|
||||
|
||||
// removePID removes the PID file, if any.
|
||||
func (h *signalHandler) removePID() {
|
||||
func (h *signalHandler) removePID(ctx context.Context) {
|
||||
if h.pidFile == "" {
|
||||
return
|
||||
}
|
||||
|
||||
err := os.Remove(h.pidFile)
|
||||
if err != nil {
|
||||
log.Error("sighdlr: removing pidfile: %s", err)
|
||||
h.logger.ErrorContext(ctx, "removing pidfile", slogutil.KeyError, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("sighdlr: removed pid at %q", h.pidFile)
|
||||
h.logger.DebugContext(ctx, "removed pidfile", "file", h.pidFile)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user