Pull request 2497: AGDNS-3306-rm-global-conf-filepath

Squashed commit of the following:

commit 6d50f74a25c8dcf2627bbb88674cca0ed7b2bbe0
Merge: aff0466ba 5c9fef62f
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Oct 23 18:46:46 2025 +0300

    Merge branch 'master' into AGDNS-3306-rm-global-conf-filepath

commit aff0466ba7
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Oct 21 10:17:59 2025 +0300

    home: fix tests

commit 8e2e1d871f
Merge: fe67680f7 feef4cd2a
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Oct 17 09:19:13 2025 +0300

    Merge branch 'master' into AGDNS-3306-rm-global-conf-filepath

commit fe67680f7b
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Oct 17 09:13:37 2025 +0300

    home: add todo

commit 6dc5635a7e
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Oct 15 19:43:37 2025 +0300

    home: add test cases

commit 233263bd5f
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Oct 15 10:22:41 2025 +0300

    home: add test

commit 354e13a60e
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Oct 10 23:07:21 2025 +0300

    home: imp logs

commit 8541861248
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Oct 8 15:36:50 2025 +0300

    home: rm global conf filepath
This commit is contained in:
Stanislav Chzhen 2025-10-23 18:59:44 +03:00
parent 5c9fef62f1
commit 8f1940f759
11 changed files with 337 additions and 160 deletions

View File

@ -337,6 +337,8 @@ func TestAuth_ServeHTTP_firstRun(t *testing.T) {
mux,
agh.EmptyConfigModifier{},
httpReg,
"",
"",
false,
true,
)
@ -509,6 +511,8 @@ func TestAuth_ServeHTTP_auth(t *testing.T) {
baseMux,
agh.EmptyConfigModifier{},
httpReg,
"",
"",
false,
false,
)
@ -667,6 +671,8 @@ func TestAuth_ServeHTTP_logout(t *testing.T) {
baseMux,
agh.EmptyConfigModifier{},
httpReg,
"",
"",
false,
false,
)

View File

@ -24,7 +24,6 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/stats"
"github.com/AdguardTeam/dnsproxy/fastip"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/timeutil"
@ -593,25 +592,36 @@ var config = &configuration{
Theme: ThemeAuto,
}
// configFilePath returns the absolute path to the symlink-evaluated path to the
// current config file.
func configFilePath() (confPath string) {
confPath, err := filepath.EvalSymlinks(globalContext.confFilePath)
// configFilePath returns the absolute, symlink-resolved path to the current
// configuration file. l must not be nil.
//
// TODO(s.chzhen): Fix the bug where the wrong file may be resolved:
// [filepath.EvalSymlinks] resolves a relative path against the current working
// directory, not workDir. Make the path absolute relative to workDir before
// calling EvalSymlinks.
func configFilePath(
ctx context.Context,
l *slog.Logger,
workDir string,
confPath string,
) (resolved string) {
resolved, err := filepath.EvalSymlinks(confPath)
if err != nil {
confPath = globalContext.confFilePath
logFunc := log.Error
if errors.Is(err, os.ErrNotExist) {
logFunc = log.Debug
}
l.DebugContext(
ctx,
"symlink resolve failed; using original path",
"path", confPath,
slogutil.KeyError, err,
)
logFunc("evaluating config path: %s; using %q", err, confPath)
resolved = confPath
}
if !filepath.IsAbs(confPath) {
confPath = filepath.Join(globalContext.workDir, confPath)
resolved = filepath.Join(workDir, confPath)
}
return confPath
return resolved
}
// validateBindHosts returns error if any of binding hosts from configuration is
@ -639,17 +649,17 @@ func validateBindHosts(
// parseConfig loads configuration from the YAML file, upgrading it if
// necessary. l must not be nil.
func parseConfig(ctx context.Context, l *slog.Logger) (err error) {
func parseConfig(ctx context.Context, l *slog.Logger, workDir, confPath string) (err error) {
// Do the upgrade if necessary.
config.fileData, err = readConfigFile()
config.fileData, err = readConfigFile(ctx, l, workDir, confPath)
if err != nil {
return err
}
migrator := configmigrate.New(&configmigrate.Config{
Logger: l.With(slogutil.KeyPrefix, "config_migrator"),
WorkingDir: globalContext.workDir,
DataDir: globalContext.getDataDir(),
WorkingDir: workDir,
DataDir: filepath.Join(workDir, dataDir),
})
var upgraded bool
@ -662,7 +672,7 @@ func parseConfig(ctx context.Context, l *slog.Logger) (err error) {
// Don't wrap the error, because it's informative enough as is.
return err
} else if upgraded {
confPath := configFilePath()
confPath = configFilePath(ctx, l, workDir, confPath)
l.DebugContext(ctx, "writing config file after config upgrade", "path", confPath)
err = maybe.WriteFile(confPath, config.fileData, aghos.DefaultPermFile)
@ -808,27 +818,39 @@ func addPorts[T tcpPort | udpPort](uc aghalg.UniqChecker[T], ports ...T) {
}
}
// readConfigFile reads configuration file contents.
func readConfigFile() (fileData []byte, err error) {
// readConfigFile reads configuration file contents. l must not be nil.
func readConfigFile(
ctx context.Context,
l *slog.Logger,
workDir string,
confPath string,
) (fileData []byte, err error) {
if len(config.fileData) > 0 {
return config.fileData, nil
}
confPath := configFilePath()
log.Debug("reading config file %q", confPath)
confPath = configFilePath(ctx, l, workDir, confPath)
l.DebugContext(ctx, "reading config file", "path", confPath)
// Do not wrap the error because it's informative enough as is.
return os.ReadFile(confPath)
}
// Saves configuration to the YAML file and also saves the user filter contents to a file
func (c *configuration) write(tlsMgr *tlsManager, auth *auth) (err error) {
// write saves configuration to the YAML file and also saves the user filter
// contents to a file. l must not be nil.
func (c *configuration) write(
ctx context.Context,
l *slog.Logger,
tlsMgr *tlsManager,
auth *auth,
workDir string,
confPath string,
) (err error) {
c.Lock()
defer c.Unlock()
if auth != nil {
// TODO(s.chzhen): Pass context.
config.Users = auth.usersList(context.TODO())
config.Users = auth.usersList(ctx)
}
if tlsMgr != nil {
@ -883,8 +905,8 @@ func (c *configuration) write(tlsMgr *tlsManager, auth *auth) (err error) {
config.Clients.Persistent = globalContext.clients.forConfig()
confPath := configFilePath()
log.Debug("writing config file %q", confPath)
confPath = configFilePath(ctx, l, workDir, confPath)
l.DebugContext(ctx, "writing config file", "path", confPath)
buf := &bytes.Buffer{}
enc := yaml.NewEncoder(buf)
@ -919,10 +941,12 @@ func validateTLSCipherIDs(cipherIDs []string) (err error) {
// defaultConfigModifier is a default [agh.ConfigModifier] implementation.
type defaultConfigModifier struct {
auth *auth
config *configuration
logger *slog.Logger
tlsMgr *tlsManager
auth *auth
config *configuration
logger *slog.Logger
tlsMgr *tlsManager
workDir string
confPath string
}
// newDefaultConfigModifier returns the new properly initialized
@ -932,10 +956,14 @@ type defaultConfigModifier struct {
func newDefaultConfigModifier(
conf *configuration,
l *slog.Logger,
workDir string,
confPath string,
) (cm *defaultConfigModifier) {
return &defaultConfigModifier{
config: conf,
logger: l,
config: conf,
logger: l,
workDir: workDir,
confPath: confPath,
}
}
@ -945,7 +973,7 @@ var _ agh.ConfigModifier = (*defaultConfigModifier)(nil)
// Apply implements the [agh.ConfigModifier] interface for
// *defaultConfigModifier.
func (cm *defaultConfigModifier) Apply(ctx context.Context) {
err := cm.config.write(cm.tlsMgr, cm.auth)
err := cm.config.write(ctx, cm.logger, cm.tlsMgr, cm.auth, cm.workDir, cm.confPath)
if err != nil {
cm.logger.ErrorContext(ctx, "writing config", slogutil.KeyError, err)
}

View File

@ -0,0 +1,91 @@
package home
import (
"os"
"path/filepath"
"testing"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestConfigFilePath(t *testing.T) {
const (
realConf = "real.yaml"
linkConf = "conf.link"
missingConf = "missing.yaml"
brokenLinkConf = "broken.link"
)
workDir := t.TempDir()
targetPath := filepath.Join(workDir, realConf)
linkPath := filepath.Join(workDir, linkConf)
missingPath := filepath.Join(workDir, missingConf)
brokenLinkPath := filepath.Join(workDir, brokenLinkConf)
err := os.Symlink(targetPath, linkPath)
require.NoError(t, err)
err = os.Symlink(missingPath, brokenLinkPath)
require.NoError(t, err)
f, err := os.Create(targetPath)
require.NoError(t, err)
testutil.CleanupAndRequireSuccess(t, f.Close)
otherDir := t.TempDir()
// Canonicalize the absolute path (e.g., on macOS: /var -> /private/var; on
// Windows: RUNNER~1 -> runneradmin).
wantAbs := targetPath
p, err := filepath.EvalSymlinks(wantAbs)
if err == nil {
wantAbs = p
}
testCases := []struct {
name string
chDir string
confPath string
want string
}{{
name: "absolute_path",
chDir: "",
confPath: targetPath,
want: wantAbs,
}, {
name: "relative_path",
chDir: "",
confPath: realConf,
want: targetPath,
}, {
name: "symlink",
chDir: "",
confPath: linkConf,
want: linkPath,
}, {
name: "symlink_broken",
chDir: "",
confPath: brokenLinkConf,
want: brokenLinkPath,
}, {
name: "symlink_before_join",
chDir: otherDir,
confPath: linkConf,
want: linkPath,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if tc.chDir != "" {
t.Chdir(tc.chDir)
}
ctx := testutil.ContextWithTimeout(t, testTimeout)
got := configFilePath(ctx, testLogger, workDir, tc.confPath)
assert.Equal(t, tc.want, got)
})
}
}

View File

@ -489,7 +489,7 @@ func (web *webAPI) finalizeInstall(
config.DNS.Port = req.DNS.Port
config.Filtering.Logger = web.baseLogger.With(slogutil.KeyPrefix, "filtering")
config.Filtering.SafeFSPatterns = []string{
filepath.Join(globalContext.workDir, userFilterDataDir, "*"),
filepath.Join(web.conf.workDir, userFilterDataDir, "*"),
}
config.HTTPConfig.Address = netip.AddrPortFrom(req.Web.IP, req.Web.Port)
@ -507,14 +507,28 @@ func (web *webAPI) finalizeInstall(
// moment we'll allow setting up TLS in the initial configuration or the
// configuration itself will use HTTPS protocol, because the underlying
// functions potentially restart the HTTPS server.
err = startMods(ctx, web.baseLogger, web.tlsManager, web.confModifier, web.httpReg)
err = startMods(
ctx,
web.baseLogger,
web.tlsManager,
web.confModifier,
web.httpReg,
web.conf.workDir,
)
if err != nil {
aghhttp.ErrorAndLog(ctx, l, r, w, http.StatusInternalServerError, "%s", err)
return
}
err = config.write(web.tlsManager, web.auth)
err = config.write(
ctx,
web.logger,
web.tlsManager,
web.auth,
web.conf.workDir,
web.conf.confPath,
)
if err != nil {
aghhttp.ErrorAndLog(
ctx,
@ -597,8 +611,9 @@ func startMods(
tlsMgr *tlsManager,
confModifier agh.ConfigModifier,
httpReg aghhttp.Registrar,
workDir string,
) (err error) {
statsDir, querylogDir, err := checkStatsAndQuerylogDirs(&globalContext, config)
statsDir, querylogDir, err := checkStatsAndQuerylogDirs(config, workDir)
if err != nil {
return err
}

View File

@ -177,7 +177,7 @@ func initDNSServer(
// failed to prepare as is. See TODO on [dnsforward.PrivateRDNSError].
err = globalContext.dnsServer.Prepare(ctx, dnsConf)
if privRDNSErr := (&dnsforward.PrivateRDNSError{}); errors.As(err, &privRDNSErr) {
log.Info("WARNING: %s; trying to disable private RDNS resolution", err)
l.WarnContext(ctx, "private rdns resolution failed; disabling", slogutil.KeyError, err)
dnsConf.UsePrivateRDNS = false
err = globalContext.dnsServer.Prepare(ctx, dnsConf)
@ -521,10 +521,10 @@ func closeDNSServer(ctx context.Context) {
// checkStatsAndQuerylogDirs checks and returns directory paths to store
// statistics and query log.
func checkStatsAndQuerylogDirs(
ctx *homeContext,
conf *configuration,
workDir string,
) (statsDir, querylogDir string, err error) {
baseDir := ctx.getDataDir()
baseDir := filepath.Join(workDir, dataDir)
statsDir = conf.Stats.DirPath
if statsDir == "" {

View File

@ -69,20 +69,10 @@ type homeContext struct {
// Runtime properties
// --
// confFilePath is the configuration file path as set by default or from the
// command-line options.
confFilePath string
workDir string // Location of our directory, used to protect against CWD being somewhere else
pidFileName string // PID file name. Empty if no PID file was created.
controlLock sync.Mutex
}
// getDataDir returns path to the directory where we store databases and filters
func (c *homeContext) getDataDir() string {
return filepath.Join(c.workDir, dataDir)
}
// globalContext is a global context object.
//
// TODO(a.garipov): Refactor.
@ -90,6 +80,8 @@ var globalContext homeContext
// Main is the entry point
func Main(clientBuildFS fs.FS) {
ctx := context.Background()
initCmdLineOpts()
// The configuration file path can be overridden, but other command-line
@ -100,7 +92,18 @@ func Main(clientBuildFS fs.FS) {
// package flag.
opts := loadCmdLineOpts()
ls := getLogSettings(opts)
// TODO(s.chzhen): Construct logger from command-line options.
l := slog.Default()
workDir, err := initWorkingDir(opts)
if err != nil {
l.ErrorContext(ctx, "failed to init working directory", slogutil.KeyError, err)
os.Exit(osutil.ExitCodeFailure)
}
confPath := initConfigFilename(ctx, l, opts, workDir)
ls := getLogSettings(ctx, l, opts, workDir, confPath)
// TODO(a.garipov): Use slog everywhere.
baseLogger := newSlogLogger(ls)
@ -110,7 +113,6 @@ func Main(clientBuildFS fs.FS) {
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
ctx := context.Background()
sigHdlrLogger := baseLogger.With(slogutil.KeyPrefix, "signalhdlr")
sigHdlr := newSignalHandler(sigHdlrLogger, signals, func(ctx context.Context) {
cleanup(ctx)
@ -131,13 +133,15 @@ func Main(clientBuildFS fs.FS) {
signals,
done,
sigHdlr,
workDir,
confPath,
)
return
}
// run the protection
run(ctx, baseLogger, opts, clientBuildFS, done, sigHdlr)
run(ctx, baseLogger, opts, clientBuildFS, done, sigHdlr, workDir, confPath)
}
// setupContext initializes [globalContext] fields. It also reads and upgrades
@ -146,6 +150,8 @@ func setupContext(
ctx context.Context,
baseLogger *slog.Logger,
opts options,
workDir string,
confPath string,
isFirstRun bool,
) (err error) {
if !opts.noEtcHosts {
@ -157,22 +163,22 @@ func setupContext(
}
if isFirstRun {
log.Info("This is the first time AdGuard Home is launched")
baseLogger.InfoContext(ctx, "this is the first time adguard home has been launched")
checkNetworkPermissions(ctx, baseLogger)
return nil
}
// TODO(s.chzhen): Consider adding a key prefix.
err = parseConfig(ctx, baseLogger)
err = parseConfig(ctx, baseLogger, workDir, confPath)
if err != nil {
log.Error("parsing configuration file: %s", err)
baseLogger.ErrorContext(ctx, "failed to parse configuration file", slogutil.KeyError, err)
os.Exit(osutil.ExitCodeFailure)
}
if opts.checkConfig {
log.Info("configuration file is ok")
baseLogger.InfoContext(ctx, "configuration file is ok")
os.Exit(osutil.ExitCodeSuccess)
}
@ -299,10 +305,11 @@ func initContextClients(
sigHdlr *signalHandler,
confModifier agh.ConfigModifier,
httpReg aghhttp.Registrar,
workDir string,
) (err error) {
//lint:ignore SA1019 Migration is not over.
config.DHCP.WorkDir = globalContext.workDir
config.DHCP.DataDir = globalContext.getDataDir()
config.DHCP.WorkDir = workDir
config.DHCP.DataDir = filepath.Join(workDir, dataDir)
config.DHCP.HTTPReg = httpReg
config.DHCP.CommandConstructor = executil.SystemCommandConstructor{}
config.DHCP.Logger = logger.With(slogutil.KeyPrefix, "dhcpd")
@ -385,6 +392,7 @@ func setupDNSFilteringConf(
tlsMgr *tlsManager,
confModifier agh.ConfigModifier,
httpReg aghhttp.Registrar,
workDir string,
) (err error) {
const (
dnsTimeout = 3 * time.Second
@ -408,7 +416,7 @@ func setupDNSFilteringConf(
conf.ConfModifier = confModifier
conf.HTTPReg = httpReg
conf.DataDir = globalContext.getDataDir()
conf.DataDir = filepath.Join(workDir, dataDir)
conf.Filters = slices.Clone(config.Filters)
conf.WhitelistFilters = slices.Clone(config.WhitelistFilters)
conf.UserRules = slices.Clone(config.UserRules)
@ -563,6 +571,8 @@ func isUpdateEnabled(
}
// initWeb initializes the web module. All arguments must not be nil.
//
// TODO(s.chzhen): Use a configuration structure.
func initWeb(
ctx context.Context,
opts options,
@ -574,6 +584,8 @@ func initWeb(
mux *http.ServeMux,
confModifier agh.ConfigModifier,
httpReg aghhttp.Registrar,
workDir string,
confPath string,
isCustomUpdURL bool,
isFirstRun bool,
) (web *webAPI, err error) {
@ -610,6 +622,9 @@ func initWeb(
BindAddr: config.HTTPConfig.Address,
workDir: workDir,
confPath: confPath,
ReadTimeout: readTimeout,
ReadHeaderTimeout: readHdrTimeout,
WriteTimeout: writeTimeout,
@ -677,32 +692,27 @@ func run(
clientBuildFS fs.FS,
done chan struct{},
sigHdlr *signalHandler,
workDir string,
confPath string,
) {
// Configure working dir.
err := initWorkingDir(opts)
fatalOnError(err)
// Configure config filename.
initConfigFilename(opts)
ls := getLogSettings(opts)
ls := getLogSettings(ctx, slogLogger, opts, workDir, confPath)
// Configure log level and output.
err = configureLogger(ls)
err := configureLogger(ls, workDir)
fatalOnError(err)
// Print the first message after logger is configured.
log.Info("%s", version.Full())
log.Debug("current working directory is %s", globalContext.workDir)
slogLogger.InfoContext(ctx, "starting adguard home", "version", version.Full())
slogLogger.DebugContext(ctx, "current working directory", "path", workDir)
if opts.runningAsService {
log.Info("AdGuard Home is running as a service")
slogLogger.InfoContext(ctx, "adguard home is running as a service")
}
aghtls.Init(ctx, slogLogger.With(slogutil.KeyPrefix, "aghtls"))
isFirstRun := detectFirstRun()
isFirstRun := detectFirstRun(ctx, slogLogger, workDir, confPath)
err = setupContext(ctx, slogLogger, opts, isFirstRun)
err = setupContext(ctx, slogLogger, opts, workDir, confPath, isFirstRun)
fatalOnError(err)
err = configureOS(config)
@ -716,13 +726,15 @@ func run(
confModifier := newDefaultConfigModifier(
config,
slogLogger.With(slogutil.KeyPrefix, "config_modifier"),
workDir,
confPath,
)
mw := &webMw{}
mux := http.NewServeMux()
httpReg := aghhttp.NewDefaultRegistrar(mux, mw.wrap)
err = initContextClients(ctx, slogLogger, sigHdlr, confModifier, httpReg)
err = initContextClients(ctx, slogLogger, sigHdlr, confModifier, httpReg, workDir)
fatalOnError(err)
tlsMgrLogger := slogLogger.With(slogutil.KeyPrefix, "tls_manager")
@ -748,6 +760,7 @@ func run(
tlsMgr,
confModifier,
httpReg,
workDir,
)
fatalOnError(err)
@ -757,17 +770,8 @@ func run(
execPath, err := os.Executable()
fatalOnError(errors.Annotate(err, "getting executable path: %w"))
confPath := configFilePath()
updLogger := slogLogger.With(slogutil.KeyPrefix, "updater")
upd, isCustomURL := newUpdater(
ctx,
updLogger,
config,
globalContext.workDir,
confPath,
execPath,
)
upd, isCustomURL := newUpdater(ctx, updLogger, config, workDir, confPath, execPath)
// TODO(e.burkov): This could be made earlier, probably as the option's
// effect.
@ -775,7 +779,7 @@ func run(
if !isFirstRun {
// Save the updated config.
err = config.write(nil, nil)
err = config.write(ctx, slogLogger, nil, nil, workDir, confPath)
fatalOnError(err)
if config.HTTPConfig.Pprof.Enabled {
@ -783,11 +787,11 @@ func run(
}
}
dataDir := globalContext.getDataDir()
err = os.MkdirAll(dataDir, aghos.DefaultPermDir)
fatalOnError(errors.Annotate(err, "creating DNS data dir at %s: %w", dataDir))
dataDirPath := filepath.Join(workDir, dataDir)
err = os.MkdirAll(dataDirPath, aghos.DefaultPermDir)
fatalOnError(errors.Annotate(err, "creating DNS data dir at %s: %w", dataDirPath))
auth, err := initUsers(ctx, slogLogger, opts.glinetMode)
auth, err := initUsers(ctx, slogLogger, workDir, opts.glinetMode)
fatalOnError(err)
confModifier.setAuth(auth)
@ -803,6 +807,8 @@ func run(
mux,
confModifier,
httpReg,
workDir,
confPath,
isCustomURL,
isFirstRun,
)
@ -815,7 +821,7 @@ func run(
tlsMgr.setWebAPI(web)
sigHdlr.addTLSManager(tlsMgr)
statsDir, querylogDir, err := checkStatsAndQuerylogDirs(&globalContext, config)
statsDir, querylogDir, err := checkStatsAndQuerylogDirs(config, workDir)
fatalOnError(err)
if !isFirstRun {
@ -835,21 +841,13 @@ func run(
if globalContext.dhcpServer != nil {
err = globalContext.dhcpServer.Start(ctx)
if err != nil {
log.Error("starting dhcp server: %s", err)
slogLogger.ErrorContext(ctx, "starting dhcp server", slogutil.KeyError, err)
}
}
}
if !opts.noPermCheck {
checkPermissions(
ctx,
slogLogger,
globalContext.workDir,
confPath,
dataDir,
statsDir,
querylogDir,
)
checkPermissions(ctx, slogLogger, workDir, confPath, dataDirPath, statsDir, querylogDir)
}
web.start(ctx)
@ -919,17 +917,17 @@ func checkPermissions(
baseLogger *slog.Logger,
workDir string,
confPath string,
dataDir string,
dataDirPath string,
statsDir string,
querylogDir string,
) {
l := baseLogger.With(slogutil.KeyPrefix, "permcheck")
if permcheck.NeedsMigration(ctx, l, workDir, confPath) {
permcheck.Migrate(ctx, l, workDir, dataDir, statsDir, querylogDir, confPath)
permcheck.Migrate(ctx, l, workDir, dataDirPath, statsDir, querylogDir, confPath)
}
permcheck.Check(ctx, l, workDir, dataDir, statsDir, querylogDir, confPath)
permcheck.Check(ctx, l, workDir, dataDirPath, statsDir, querylogDir, confPath)
}
// initUsers initializes authentication module and clears the [config.Users]
@ -937,6 +935,7 @@ func checkPermissions(
func initUsers(
ctx context.Context,
baseLogger *slog.Logger,
workDir string,
isGLiNet bool,
) (auth *auth, err error) {
var rateLimiter loginRateLimiter
@ -948,11 +947,12 @@ func initUsers(
rateLimiter = emptyRateLimiter{}
}
dataDirPath := filepath.Join(workDir, dataDir)
auth, err = newAuth(ctx, &authConfig{
baseLogger: baseLogger,
rateLimiter: rateLimiter,
trustedProxies: netutil.SliceSubnetSet(netutil.UnembedPrefixes(config.DNS.TrustedProxies)),
dbFilename: filepath.Join(globalContext.getDataDir(), sessionsDBName),
dbFilename: filepath.Join(dataDirPath, sessionsDBName),
users: config.Users,
sessionTTL: time.Duration(config.HTTPConfig.SessionTTL),
isGLiNet: isGLiNet,
@ -1030,47 +1030,50 @@ func writePIDFile(fn string) bool {
return true
}
// initConfigFilename sets up context config file path. This file path can be
// overridden by command-line arguments, or is set to default. Must only be
// called after initializing the workDir with initWorkingDir.
func initConfigFilename(opts options) {
confPath := opts.confFilename
if confPath == "" {
globalContext.confFilePath = filepath.Join(globalContext.workDir, "AdGuardHome.yaml")
// initConfigFilename returns the configuration file path. If a path is
// provided via command-line argument, it is used; otherwise a default within
// workDir is returned. l must not be nil.
func initConfigFilename(
ctx context.Context,
l *slog.Logger,
opts options,
workDir string,
) (confPath string) {
confPath = opts.confFilename
if confPath != "" {
l.DebugContext(ctx, "config path overridden from cmdline", "path", confPath)
return
return confPath
}
log.Debug("config path overridden to %q from cmdline", confPath)
confPath = filepath.Join(workDir, "AdGuardHome.yaml")
globalContext.confFilePath = confPath
return confPath
}
// initWorkingDir initializes the workDir. If no command-line arguments are
// specified, the directory with the binary file is used.
func initWorkingDir(opts options) (err error) {
execPath, err := os.Executable()
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
}
// initWorkingDir returns the working directory path. If no command-line
// argument is provided, it uses the executable's directory.
func initWorkingDir(opts options) (workDir string, err error) {
if opts.workDir != "" {
// If there is a custom config file, use it's directory as our working dir
globalContext.workDir = opts.workDir
workDir = opts.workDir
} else {
globalContext.workDir = filepath.Dir(execPath)
var execPath string
execPath, err = os.Executable()
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return "", err
}
workDir = filepath.Dir(execPath)
}
workDir, err := filepath.EvalSymlinks(globalContext.workDir)
workDir, err = filepath.EvalSymlinks(workDir)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
return "", err
}
globalContext.workDir = workDir
return nil
return workDir, nil
}
// cleanup stops and resets all the modules.
@ -1194,11 +1197,11 @@ func printHTTPAddresses(proto string, tlsMgr *tlsManager) {
}
}
// detectFirstRun returns true if this is the first run of AdGuard Home.
func detectFirstRun() (ok bool) {
confPath := globalContext.confFilePath
// detectFirstRun returns true if this is the first run of AdGuard Home. l must
// not be nil.
func detectFirstRun(ctx context.Context, l *slog.Logger, workDir, confPath string) (ok bool) {
if !filepath.IsAbs(confPath) {
confPath = filepath.Join(globalContext.workDir, globalContext.confFilePath)
confPath = filepath.Join(workDir, confPath)
}
_, err := os.Stat(confPath)
@ -1208,7 +1211,7 @@ func detectFirstRun() (ok bool) {
return true
}
log.Error("detecting first run: %s; considering first run", err)
l.ErrorContext(ctx, "failed to detect first run; considering first run", slogutil.KeyError, err)
return true
}

View File

@ -2,6 +2,7 @@ package home
import (
"cmp"
"context"
"fmt"
"log/slog"
"path/filepath"
@ -19,6 +20,7 @@ import (
const configSyslog = "syslog"
// newSlogLogger returns new [*slog.Logger] configured with the given settings.
// ls must not be nil.
func newSlogLogger(ls *logSettings) (l *slog.Logger) {
if !ls.Enabled {
return slogutil.NewDiscardLogger()
@ -36,8 +38,8 @@ func newSlogLogger(ls *logSettings) (l *slog.Logger) {
})
}
// configureLogger configures logger level and output.
func configureLogger(ls *logSettings) (err error) {
// configureLogger configures logger level and output. ls must not be nil.
func configureLogger(ls *logSettings, workDir string) (err error) {
// Configure logger level.
if !ls.Enabled {
log.SetLevel(log.OFF)
@ -66,7 +68,7 @@ func configureLogger(ls *logSettings) (err error) {
logFilePath := ls.File
if !filepath.IsAbs(logFilePath) {
logFilePath = filepath.Join(globalContext.workDir, logFilePath)
logFilePath = filepath.Join(workDir, logFilePath)
}
log.SetOutput(&lumberjack.Logger{
@ -82,10 +84,17 @@ func configureLogger(ls *logSettings) (err error) {
}
// getLogSettings returns a log settings object properly initialized from opts.
func getLogSettings(opts options) (ls *logSettings) {
// l must not be nil.
func getLogSettings(
ctx context.Context,
l *slog.Logger,
opts options,
workDir string,
confPath string,
) (ls *logSettings) {
configLogSettings := config.Log
ls = readLogSettings()
ls = readLogSettings(ctx, l, workDir, confPath)
if ls == nil {
// Use default log settings.
ls = &configLogSettings
@ -109,8 +118,13 @@ func getLogSettings(opts options) (ls *logSettings) {
// readLogSettings reads logging settings from the config file. We do it in a
// separate method in order to configure logger before the actual configuration
// is parsed and applied.
func readLogSettings() (ls *logSettings) {
// is parsed and applied. l must not be nil.
func readLogSettings(
ctx context.Context,
l *slog.Logger,
workDir string,
confPath string,
) (ls *logSettings) {
// TODO(s.chzhen): Add a helper function that returns default parameters
// for this structure and for the global configuration structure [config].
conf := &configuration{
@ -120,7 +134,7 @@ func readLogSettings() (ls *logSettings) {
},
}
yamlFile, err := readConfigFile()
yamlFile, err := readConfigFile(ctx, l, workDir, confPath)
if err != nil {
return nil
}

View File

@ -90,6 +90,8 @@ func TestWeb_HandleGetProfile(t *testing.T) {
baseMux,
agh.EmptyConfigModifier{},
aghhttp.EmptyRegistrar{},
"",
"",
false,
false,
)
@ -153,6 +155,8 @@ func TestWeb_HandlePutProfile(t *testing.T) {
mux,
confModifier,
httpReg,
"",
"",
false,
false,
)

View File

@ -45,6 +45,8 @@ type program struct {
baseLogger *slog.Logger
logger *slog.Logger
sigHdlr *signalHandler
workDir string
confPath string
}
// type check
@ -56,7 +58,7 @@ func (p *program) Start(_ service.Service) (err error) {
args := p.opts
args.runningAsService = true
go run(p.ctx, p.baseLogger, args, p.clientBuildFS, p.done, p.sigHdlr)
go run(p.ctx, p.baseLogger, args, p.clientBuildFS, p.done, p.sigHdlr, p.workDir, p.confPath)
return nil
}
@ -218,6 +220,8 @@ func handleServiceControlAction(
signals chan os.Signal,
done chan struct{},
sigHdlr *signalHandler,
workDir string,
confPath string,
) {
// Call chooseSystem explicitly to introduce OpenBSD support for service
// package. It's a noop for other GOOS values.
@ -263,13 +267,15 @@ func handleServiceControlAction(
baseLogger: l,
logger: l.With(slogutil.KeyPrefix, "service"),
sigHdlr: sigHdlr,
workDir: workDir,
confPath: confPath,
}, svcConfig)
if err != nil {
l.ErrorContext(ctx, "initializing service", slogutil.KeyError, err)
os.Exit(osutil.ExitCodeFailure)
}
err = handleServiceCommand(ctx, l, s, action, opts)
err = handleServiceCommand(ctx, l, s, action, workDir, confPath)
if err != nil {
l.ErrorContext(ctx, "handling command", slogutil.KeyError, err)
os.Exit(osutil.ExitCodeFailure)
@ -289,7 +295,8 @@ func handleServiceCommand(
l *slog.Logger,
s service.Service,
action string,
opts options,
workDir string,
confPath string,
) (err error) {
switch action {
case "status":
@ -299,13 +306,7 @@ func handleServiceCommand(
return fmt.Errorf("failed to run service: %w", err)
}
case "install":
if err = initWorkingDir(opts); err != nil {
return fmt.Errorf("failed to init working dir: %w", err)
}
initConfigFilename(opts)
handleServiceInstallCommand(ctx, l, s)
handleServiceInstallCommand(ctx, l, s, workDir, confPath)
case "uninstall":
handleServiceUninstallCommand(ctx, l, s)
default:
@ -345,8 +346,15 @@ func handleServiceStatusCommand(
}
}
// handleServiceInstallCommand handles service "install" command.
func handleServiceInstallCommand(ctx context.Context, l *slog.Logger, s service.Service) {
// handleServiceInstallCommand handles the service "install" command. l must
// not be nil.
func handleServiceInstallCommand(
ctx context.Context,
l *slog.Logger,
s service.Service,
workDir string,
confPath string,
) {
err := svcAction(ctx, l, s, "install")
if err != nil {
l.ErrorContext(ctx, "executing install", slogutil.KeyError, err)
@ -372,7 +380,7 @@ func handleServiceInstallCommand(ctx context.Context, l *slog.Logger, s service.
}
l.InfoContext(ctx, "started")
if detectFirstRun() {
if detectFirstRun(ctx, l, workDir, confPath) {
slogutil.PrintLines(ctx, l, slog.LevelInfo, "", "Almost ready!\n"+
"AdGuard Home is successfully installed and will automatically start on boot.\n"+
"There are a few more things that must be configured before you can use it.\n"+

View File

@ -307,6 +307,8 @@ func initEmptyWeb(tb testing.TB) (web *webAPI) {
http.NewServeMux(),
agh.EmptyConfigModifier{},
aghhttp.EmptyRegistrar{},
"",
"",
false,
false,
)

View File

@ -76,6 +76,12 @@ type webConfig struct {
// BindAddr is the binding address with port for plain HTTP web interface.
BindAddr netip.AddrPort
// workDir is the base working directory.
workDir string
// confPath is the configuration file path.
confPath string
// ReadTimeout is an option to pass to http.Server for setting an
// appropriate field.
ReadTimeout time.Duration