From 2902f030be7db231d0d257d11cb8d69702dc77fd Mon Sep 17 00:00:00 2001 From: Ainar Garipov Date: Tue, 20 Jun 2023 14:11:34 +0300 Subject: [PATCH] Pull request 1879: nextapi-opts Merge in DNS/adguard-home from nextapi-opts to master Squashed commit of the following: commit 01f27e374785f47f41470126ddc17cf447e84dd7 Merge: 17d3b06e0 371261b2c Author: Ainar Garipov Date: Tue Jun 20 13:43:08 2023 +0300 Merge branch 'master' into nextapi-opts commit 17d3b06e0551908b06be67233181ce4067b6c1cc Author: Ainar Garipov Date: Tue Jun 20 13:15:31 2023 +0300 next: imp chlog commit 19d5ea9db0ee077c55c9f1d54a07a3c6b56b4e2e Author: Ainar Garipov Date: Tue Jun 20 12:58:48 2023 +0300 cmd: typo commit 082ad5b5fc634a1533127e428680ccf55c0811ab Author: Ainar Garipov Date: Mon Jun 19 22:49:16 2023 +0300 cmd: imp api, docs, names commit a49b3cbcc591f36530306c2e08cf1ea7dc8f6d30 Author: Ainar Garipov Date: Mon Jun 19 17:13:50 2023 +0300 next/cmd: add opt parsing --- internal/next/changelog.md | 38 +++ internal/next/cmd/cmd.go | 27 +- internal/next/cmd/log.go | 39 +++ internal/next/cmd/opt.go | 403 +++++++++++++++++++++++++++ internal/next/configmgr/configmgr.go | 2 +- 5 files changed, 500 insertions(+), 9 deletions(-) create mode 100644 internal/next/changelog.md create mode 100644 internal/next/cmd/log.go create mode 100644 internal/next/cmd/opt.go diff --git a/internal/next/changelog.md b/internal/next/changelog.md new file mode 100644 index 00000000..39e1aff0 --- /dev/null +++ b/internal/next/changelog.md @@ -0,0 +1,38 @@ +# AdGuard Home v0.108.0 Changelog DRAFT + +This changelog should be merged into the main one once the next API matures +enough. + +## [v0.108.0] - TODO + +### Added + +- The ability to log to stderr using `--logFile=stderr`. +- The new `--web-addr` flag to set the Web UI address in a `host:port` form. +- `SIGHUP` now reloads all configuration from the configuration file ([#5676]). + +### Changed + +#### New HTTP API + +**TODO(a.garipov):** Describe the new API and add a link to the new OpenAPI doc. + +#### Other changes + +- `-h` is now an alias for `--help` instead of the removed `--host`, see below. + Use `--web-addr=host:port` to set an address on which to serve the Web UI. + +### Fixed + +- Inconsistent application of `--work-dir/-w` ([#2902]). +- The order of `-v/--verbose` and `--version` being significant ([#2893]). + +### Removed + +- The deprecated `--no-mem-optimization` and `--no-etc-hosts` flags. +- `--host` and `-p/--port` flags. Use `--web-addr=host:port` to set an address + on which to serve the Web UI. `-h` is now an alias for `--help`, see above. + +[#2893]: https://github.com/AdguardTeam/AdGuardHome/issues/2893 +[#2902]: https://github.com/AdguardTeam/AdGuardHome/issues/2902 +[#5676]: https://github.com/AdguardTeam/AdGuardHome/issues/5676 diff --git a/internal/next/cmd/cmd.go b/internal/next/cmd/cmd.go index 5c561782..1d60cfd7 100644 --- a/internal/next/cmd/cmd.go +++ b/internal/next/cmd/cmd.go @@ -17,20 +17,31 @@ import ( // Main is the entry point of AdGuard Home. func Main(frontend fs.FS) { - // Initial Configuration - start := time.Now() - // TODO(a.garipov): Set up logging. + // Initial Configuration + + cmdName := os.Args[0] + opts, err := parseOptions(cmdName, os.Args[1:]) + exitCode, needExit := processOptions(opts, cmdName, err) + if needExit { + os.Exit(exitCode) + } + + err = setLog(opts) + check(err) log.Info("starting adguard home, version %s, pid %d", version.Version(), os.Getpid()) + if opts.workDir != "" { + log.Info("changing working directory to %q", opts.workDir) + err = os.Chdir(opts.workDir) + check(err) + } + // Web Service - // TODO(a.garipov): Set up configuration file name. - const confFile = "AdGuardHome.1.yaml" - - confMgr, err := configmgr.New(confFile, frontend, start) + confMgr, err := configmgr.New(opts.confFile, frontend, start) check(err) web := confMgr.Web() @@ -42,7 +53,7 @@ func Main(frontend fs.FS) { check(err) sigHdlr := newSignalHandler( - confFile, + opts.confFile, frontend, start, web, diff --git a/internal/next/cmd/log.go b/internal/next/cmd/log.go new file mode 100644 index 00000000..3aa2a0e5 --- /dev/null +++ b/internal/next/cmd/log.go @@ -0,0 +1,39 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/AdguardTeam/AdGuardHome/internal/aghos" + "github.com/AdguardTeam/golibs/log" +) + +// 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) { + switch opts.confFile { + case "stdout": + log.SetOutput(os.Stdout) + case "stderr": + log.SetOutput(os.Stderr) + case "syslog": + err = aghos.ConfigureSyslog(syslogServiceName) + if err != nil { + return fmt.Errorf("initializing syslog: %w", err) + } + default: + // TODO(a.garipov): Use the path. + } + + if opts.verbose { + log.SetLevel(log.DEBUG) + log.Debug("verbose logging enabled") + } + + return nil +} diff --git a/internal/next/cmd/opt.go b/internal/next/cmd/opt.go new file mode 100644 index 00000000..5bbed703 --- /dev/null +++ b/internal/next/cmd/opt.go @@ -0,0 +1,403 @@ +package cmd + +import ( + "flag" + "fmt" + "io" + "net/netip" + "os" + "strings" + + "github.com/AdguardTeam/AdGuardHome/internal/version" + "golang.org/x/exp/slices" +) + +// options contains all command-line options for the AdGuardHome(.exe) binary. +type options struct { + // confFile is the path to the configuration file. + confFile string + + // logFile is the path to the log file. Special values: + // + // - "stdout": Write to stdout (the default). + // - "stderr": Write to stderr. + // - "syslog": Write to the system log. + logFile string + + // pidFile is the path to the file where to store the PID. + // + // TODO(a.garipov): Use. + pidFile string + + // serviceAction is the service control action to perform: + // + // - "install": Installs AdGuard Home as a system service. + // - "uninstall": Uninstalls it. + // - "status": Prints the service status. + // - "start": Starts the previously installed service. + // - "stop": Stops the previously installed service. + // - "restart": Restarts the previously installed service. + // - "reload": Reloads the configuration. + // - "run": This is a special command that is not supposed to be used + // directly it is specified when we register a service, and it indicates + // to the app that it is being run as a service. + // + // TODO(a.garipov): Use. + serviceAction string + + // workDir is the path to the working directory. It is applied before all + // other configuration is read, so all relative paths are relative to it. + workDir string + + // webAddrs contains the addresses on which to serve the web UI. + // + // TODO(a.garipov): Use. + webAddrs []netip.AddrPort + + // checkConfig, if true, instructs AdGuard Home to check the configuration + // file and exit with a corresponding exit code. + // + // TODO(a.garipov): Use. + checkConfig bool + + // disableUpdate, if true, prevents AdGuard Home from automatically checking + // for updates. + // + // TODO(a.garipov): Use. + disableUpdate bool + + // glinetMode enables the GL-Inet compatibility mode. + // + // TODO(a.garipov): Use. + glinetMode bool + + // help, if true, instructs AdGuard Home to print the command-line option + // help message and quit with a successful exit-code. + help bool + + // localFrontend, if true, instructs AdGuard Home to use the local frontend + // directory instead of the files compiled into the binary. + // + // TODO(a.garipov): Use. + localFrontend bool + + // performUpdate, if true, instructs AdGuard Home to update the current + // binary and restart the service in case it's installed. + // + // TODO(a.garipov): Use. + performUpdate bool + + // verbose, if true, instructs AdGuard Home to enable verbose logging. + verbose bool + + // version, if true, instructs AdGuard Home to print the version to stdout + // and quit with a successful exit-code. If verbose is also true, print a + // more detailed version description. + version bool +} + +// Indexes to help with the [commandLineOptions] initialization. +const ( + confFileIdx = iota + logFileIdx + pidFileIdx + serviceActionIdx + workDirIdx + webAddrsIdx + checkConfigIdx + disableUpdateIdx + glinetModeIdx + helpIdx + localFrontend + performUpdateIdx + verboseIdx + versionIdx +) + +// commandLineOption contains information about a command-line option: its long +// and, if there is one, short forms, the value type, the description, and the +// default value. +type commandLineOption struct { + defaultValue any + description string + long string + short string + valueType string +} + +// commandLineOptions are all command-line options currently supported by +// AdGuard Home. +var commandLineOptions = []*commandLineOption{ + confFileIdx: { + // TODO(a.garipov): Remove the ".1" when the new code is ready. + defaultValue: "AdGuardHome.1.yaml", + description: "Path to the config file.", + long: "config", + short: "c", + valueType: "path", + }, + + logFileIdx: { + defaultValue: "stdout", + description: `Path to log file. Special values include "stdout", "stderr", and "syslog".`, + long: "logfile", + short: "l", + valueType: "path", + }, + + pidFileIdx: { + defaultValue: "", + description: "Path to the file where to store the PID.", + long: "pidfile", + short: "", + valueType: "path", + }, + + serviceActionIdx: { + defaultValue: "", + description: `Service control action: "status", "install" (as a service), ` + + `"uninstall" (as a service), "start", "stop", "restart", "reload" (configuration).`, + long: "service", + short: "s", + valueType: "action", + }, + + workDirIdx: { + defaultValue: "", + description: `Path to the working directory. ` + + `It is applied before all other configuration is read, ` + + `so all relative paths are relative to it.`, + long: "work-dir", + short: "w", + valueType: "path", + }, + + webAddrsIdx: { + defaultValue: []netip.AddrPort(nil), + description: `Address(es) to serve the web UI on, in the host:port format. ` + + `Can be used multiple times.`, + long: "web-addr", + short: "", + valueType: "host:port", + }, + + checkConfigIdx: { + defaultValue: false, + description: "Check configuration and quit.", + long: "check-config", + short: "", + valueType: "", + }, + + disableUpdateIdx: { + defaultValue: false, + description: "Disable automatic update checking.", + long: "no-check-update", + short: "", + valueType: "", + }, + + glinetModeIdx: { + defaultValue: false, + description: "Run in GL-Inet compatibility mode.", + long: "glinet", + short: "", + valueType: "", + }, + + helpIdx: { + defaultValue: false, + description: "Print this help message and quit.", + long: "help", + short: "h", + valueType: "", + }, + + localFrontend: { + defaultValue: false, + description: "Use local frontend directories.", + long: "local-frontend", + short: "", + valueType: "", + }, + + performUpdateIdx: { + defaultValue: false, + description: "Update the current binary and restart the service in case it's installed.", + long: "update", + short: "", + valueType: "", + }, + + verboseIdx: { + defaultValue: false, + description: "Enable verbose logging.", + long: "verbose", + short: "v", + valueType: "", + }, + + versionIdx: { + defaultValue: false, + description: `Print the version to stdout and quit. ` + + `Print a more detailed version description with -v.`, + long: "version", + short: "", + valueType: "", + }, +} + +// parseOptions parses the command-line options for AdGuardHome. +func parseOptions(cmdName string, args []string) (opts *options, err error) { + flags := flag.NewFlagSet(cmdName, flag.ContinueOnError) + + opts = &options{} + for i, fieldPtr := range []any{ + confFileIdx: &opts.confFile, + logFileIdx: &opts.logFile, + pidFileIdx: &opts.pidFile, + serviceActionIdx: &opts.serviceAction, + workDirIdx: &opts.workDir, + webAddrsIdx: &opts.webAddrs, + checkConfigIdx: &opts.checkConfig, + disableUpdateIdx: &opts.disableUpdate, + glinetModeIdx: &opts.glinetMode, + helpIdx: &opts.help, + localFrontend: &opts.localFrontend, + performUpdateIdx: &opts.performUpdate, + verboseIdx: &opts.verbose, + versionIdx: &opts.version, + } { + addOption(flags, fieldPtr, commandLineOptions[i]) + } + + flags.Usage = func() { usage(cmdName, os.Stderr) } + + err = flags.Parse(args) + if err != nil { + // Don't wrap the error, because it's informative enough as is. + return nil, err + } + + return opts, nil +} + +// addOption adds the command-line option described by o to flags using fieldPtr +// as the pointer to the value. +func addOption(flags *flag.FlagSet, fieldPtr any, o *commandLineOption) { + switch fieldPtr := fieldPtr.(type) { + case *string: + flags.StringVar(fieldPtr, o.long, o.defaultValue.(string), o.description) + if o.short != "" { + flags.StringVar(fieldPtr, o.short, o.defaultValue.(string), o.description) + } + case *[]netip.AddrPort: + flags.Func(o.long, o.description, func(s string) (err error) { + addr, err := netip.ParseAddrPort(s) + if err != nil { + // Don't wrap the error, because it's informative enough as is. + return err + } + + *fieldPtr = append(*fieldPtr, addr) + + return nil + }) + case *bool: + flags.BoolVar(fieldPtr, o.long, o.defaultValue.(bool), o.description) + if o.short != "" { + flags.BoolVar(fieldPtr, o.short, o.defaultValue.(bool), o.description) + } + default: + panic(fmt.Errorf("unexpected field pointer type %T", fieldPtr)) + } +} + +// usage prints a usage message similar to the one printed by package flag but +// taking long vs. short versions into account as well as using more informative +// value hints. +func usage(cmdName string, output io.Writer) { + options := slices.Clone(commandLineOptions) + slices.SortStableFunc(options, func(a, b *commandLineOption) (sortsBefore bool) { + return a.long < b.long + }) + + b := &strings.Builder{} + _, _ = fmt.Fprintf(b, "Usage of %s:\n", cmdName) + + for _, o := range options { + writeUsageLine(b, o) + + // Use four spaces before the tab to trigger good alignment for both 4- + // and 8-space tab stops. + if shouldIncludeDefault(o.defaultValue) { + _, _ = fmt.Fprintf(b, " \t%s (Default value: %q)\n", o.description, o.defaultValue) + } else { + _, _ = fmt.Fprintf(b, " \t%s\n", o.description) + } + } + + _, _ = io.WriteString(output, b.String()) +} + +// shouldIncludeDefault returns true if this default value should be printed. +func shouldIncludeDefault(v any) (ok bool) { + switch v := v.(type) { + case bool: + return v + case string: + return v != "" + default: + return v == nil + } +} + +// writeUsageLine writes the usage line for the provided command-line option. +func writeUsageLine(b *strings.Builder, o *commandLineOption) { + if o.short == "" { + if o.valueType == "" { + _, _ = fmt.Fprintf(b, " --%s\n", o.long) + } else { + _, _ = fmt.Fprintf(b, " --%s=%s\n", o.long, o.valueType) + } + + return + } + + if o.valueType == "" { + _, _ = fmt.Fprintf(b, " --%s/-%s\n", o.long, o.short) + } else { + _, _ = fmt.Fprintf(b, " --%[1]s=%[3]s/-%[2]s %[3]s\n", o.long, o.short, o.valueType) + } +} + +// processOptions decides if AdGuard Home should exit depending on the results +// of command-line option parsing. +func processOptions( + opts *options, + cmdName string, + parseErr error, +) (exitCode int, needExit bool) { + if parseErr != nil { + // Assume that usage has already been printed. + return 2, true + } + + if opts.help { + usage(cmdName, os.Stdout) + + return 0, true + } + + if opts.version { + if opts.verbose { + fmt.Println(version.Verbose()) + } else { + fmt.Printf("AdGuard Home %s\n", version.Version()) + } + + return 0, true + } + + return 0, false +} diff --git a/internal/next/configmgr/configmgr.go b/internal/next/configmgr/configmgr.go index 84388e42..95088b96 100644 --- a/internal/next/configmgr/configmgr.go +++ b/internal/next/configmgr/configmgr.go @@ -48,7 +48,7 @@ func New( frontend fs.FS, start time.Time, ) (m *Manager, err error) { - defer func() { err = errors.Annotate(err, "reading config") }() + defer func() { err = errors.Annotate(err, "reading config: %w") }() conf := &config{} f, err := os.Open(fileName)