AdGuardHome/internal/next/cmd/signal.go
Eugene Burkov 3895cfb4f0 Pull request 2312: 7400 Windows permcheck
Updates #7400.

Squashed commit of the following:

commit f50d7c200de545dc6c8ef70b39208f522033fb90
Merge: 47040a14c 37b16bcf7
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Tue Dec 3 18:09:23 2024 +0300

    Merge branch 'master' into 7400-chown-permcheck

commit 47040a14cd
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Tue Dec 3 14:26:43 2024 +0300

    permcheck: fix nil entries

commit e1d21c576d
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Dec 2 15:37:58 2024 +0300

    permcheck: fix nil owner

commit b1fc67c4d1
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Nov 29 18:07:15 2024 +0300

    permcheck: imp doc

commit 0b6a71326e
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Nov 29 17:16:24 2024 +0300

    permcheck: imp code

commit 7dfbeda179
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Nov 29 14:28:17 2024 +0300

    permcheck: imp code

commit 3a5b6aced9
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Nov 28 19:21:03 2024 +0300

    all: imp code, docs

commit c076c93669
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Nov 28 15:14:06 2024 +0300

    permcheck: imp code, docs

commit 09e4ae1ba1
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Nov 27 19:19:11 2024 +0300

    all: implement windows permcheck

commit b75ed7d4d3
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Nov 25 18:01:47 2024 +0300

    all: revert permissions
2024-12-03 18:26:00 +03:00

203 lines
5.1 KiB
Go

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)
}