From 277415124e99961964a6d12667e4d6d753710425 Mon Sep 17 00:00:00 2001 From: Andrey Meshkov Date: Mon, 4 Feb 2019 13:54:53 +0300 Subject: [PATCH 1/6] AdGuard Home as a system service 1. Reworked working with command-line arguments 2. Added service control actions: install/uninstall/start/stop/status 3. Added log settings to the configuration file 4. Updated the README file --- .gitignore | 1 + README.md | 40 ++++++ app.go | 371 ++++++++++++++++++++++++++++++++--------------------- config.go | 46 ++++++- dhcp.go | 17 +++ go.mod | 1 + go.sum | 2 + service.go | 76 +++++++++++ 8 files changed, 402 insertions(+), 152 deletions(-) create mode 100644 service.go diff --git a/.gitignore b/.gitignore index b4047c5b..9232c782 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /AdGuardHome /AdGuardHome.exe /AdGuardHome.yaml +/AdGuardHome.log /data/ /build/ /dist/ diff --git a/README.md b/README.md index ccbb062d..412d0942 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,35 @@ sudo ./AdGuardHome Now open the browser and navigate to http://localhost:3000/ to control your AdGuard Home service. +### Command-line arguments + +Here is a list of all available command-line arguments. + +``` +$ ./AdGuardHome -h +Usage: + +./AdGuardHome [options] + +Options: + -c, --config path to config file + -o, --host host address to bind HTTP server on + -p, --port port to serve HTTP pages on + -v, --verbose enable verbose output + -s, --service service control action: status, install, uninstall, start, stop, restart + -l, --logfile path to the log file. If empty, writes to stdout, if 'syslog' -- system log + -h, --help print this help +``` + +Please note, that you can register AdGuard Home as a system service on Windows, Linux/(systemd | Upstart | SysV), and OSX/Launchd. + +* `AdGuardHome -s install` - install as a system service. +* `AdGuardHome -s uninstall` - uninstall's AdGuard Home service. +* `AdGuardHome -s start` - starts the service. +* `AdGuardHome -s stop` - stops the service. +* `AdGuardHome -s restart` - restarts the service. +* `AdGuardHome -s status` - shows the current service status. + ### Running without superuser You can run AdGuard Home without superuser privileges, but you need to either grant the binary a capability (on Linux) or instruct it to use a different port (all platforms). @@ -139,6 +168,7 @@ Settings are stored in [YAML format](https://en.wikipedia.org/wiki/YAML), possib * `auth_name` — Web interface optional authorization username. * `auth_pass` — Web interface optional authorization password. * `dns` — DNS configuration section. + * `bind_host` - DNS interface IP address to listen on. * `port` — DNS server port to listen on. * `protection_enabled` — Whether any kind of filtering and protection should be done, when off it works as a plain dns forwarder. * `filtering_enabled` — Filtering of DNS requests based on filter lists. @@ -159,7 +189,17 @@ Settings are stored in [YAML format](https://en.wikipedia.org/wiki/YAML), possib * `name` — Name of the filter. If it's an adguard syntax filter it will get updated automatically, otherwise it stays unchanged. * `last_updated` — Time when the filter was last updated from server. * `ID` - filter ID (must be unique). + * `dhcp` - Built-in DHCP server configuration. + * `enabled` - DHCP server status. + * `interface_name` - network interface name (eth0, en0 and so on). + * `gateway_ip` - gateway IP address. + * `subnet_mask` - subnet mask. + * `range_start` - start IP address of the controlled range. + * `range_end` - end IP address of the controlled range. + * `lease_duration` - lease duration in seconds. If 0, using default duration (2 hours). * `user_rules` — User-specified filtering rules. + * `log_file` — Path to the log file. If empty, writes to stdout, if 'syslog' -- system log. + * `verbose` — Enable our disables debug verbose output. Removing an entry from settings file will reset it to the default value. Deleting the file will reset all settings to the default values. diff --git a/app.go b/app.go index 70462fee..94cb9348 100644 --- a/app.go +++ b/app.go @@ -3,6 +3,8 @@ package main import ( "bufio" "fmt" + stdlog "log" + "log/syslog" "net" "net/http" "os" @@ -20,58 +22,19 @@ import ( // VersionString will be set through ldflags, contains current version var VersionString = "undefined" +// main is the entry point func main() { - log.Printf("AdGuard Home web interface backend, version %s\n", VersionString) - box := packr.NewBox("build/static") - { - executable, err := os.Executable() - if err != nil { - panic(err) - } - - executableName := filepath.Base(executable) - if executableName == "AdGuardHome" { - // Binary build - config.ourBinaryDir = filepath.Dir(executable) - } else { - // Most likely we're debugging -- using current working directory in this case - workDir, _ := os.Getwd() - config.ourBinaryDir = workDir - } - log.Printf("Current working directory is %s", config.ourBinaryDir) - } - // config can be specified, which reads options from there, but other command line flags have to override config values // therefore, we must do it manually instead of using a lib - loadOptions() + args := loadOptions() - // Load filters from the disk - // And if any filter has zero ID, assign a new one - for i := range config.Filters { - filter := &config.Filters[i] // otherwise we're operating on a copy - if filter.ID == 0 { - filter.ID = assignUniqueFilterID() - } - err := filter.load() - if err != nil { - // This is okay for the first start, the filter will be loaded later - log.Printf("Couldn't load filter %d contents due to %s", filter.ID, err) - // clear LastUpdated so it gets fetched right away - } - if len(filter.Rules) == 0 { - filter.LastUpdated = time.Time{} - } + if args.serviceControlAction != "" { + handleServiceControlAction(args.serviceControlAction) + return } - // Update filters we've just loaded right away, don't wait for periodic update timer - go func() { - refreshFiltersIfNecessary(false) - // Save the updated config - err := config.write() - if err != nil { - log.Fatal(err) - } - }() + // run the protection + run(args) signalChannel := make(chan os.Signal) signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT) @@ -81,110 +44,23 @@ func main() { os.Exit(0) }() - // Save the updated config - err := config.write() - if err != nil { - log.Fatal(err) - } - address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.BindPort)) - - go periodicallyRefreshFilters() - - http.Handle("/", optionalAuthHandler(http.FileServer(box))) - registerControlHandlers() - - err = startDNSServer() - if err != nil { - log.Fatal(err) - } - - err = startDHCPServer() - if err != nil { - log.Fatal(err) - } - URL := fmt.Sprintf("http://%s", address) log.Println("Go to " + URL) log.Fatal(http.ListenAndServe(address, nil)) } -func cleanup() { - err := stopDNSServer() - if err != nil { - log.Printf("Couldn't stop DNS server: %s", err) +// run initializes configuration and runs the AdGuard Home +func run(args options) { + if args.configFilename != "" { + config.ourConfigFilename = args.configFilename } -} -func getInput() (string, error) { - scanner := bufio.NewScanner(os.Stdin) - scanner.Scan() - text := scanner.Text() - err := scanner.Err() - return text, err -} + // configure log level and output + configureLogger(args) -// loadOptions reads command line arguments and initializes configuration -func loadOptions() { - var printHelp func() - var configFilename *string - var bindHost *string - var bindPort *int - var opts = []struct { - longName string - shortName string - description string - callbackWithValue func(value string) - callbackNoValue func() - }{ - {"config", "c", "path to config file", func(value string) { configFilename = &value }, nil}, - {"host", "h", "host address to bind HTTP server on", func(value string) { bindHost = &value }, nil}, - {"port", "p", "port to serve HTTP pages on", func(value string) { - v, err := strconv.Atoi(value) - if err != nil { - panic("Got port that is not a number") - } - bindPort = &v - }, nil}, - {"verbose", "v", "enable verbose output", nil, func() { log.Verbose = true }}, - {"help", "h", "print this help", nil, func() { printHelp(); os.Exit(64) }}, - } - printHelp = func() { - fmt.Printf("Usage:\n\n") - fmt.Printf("%s [options]\n\n", os.Args[0]) - fmt.Printf("Options:\n") - for _, opt := range opts { - fmt.Printf(" -%s, %-30s %s\n", opt.shortName, "--"+opt.longName, opt.description) - } - } - for i := 1; i < len(os.Args); i++ { - v := os.Args[i] - knownParam := false - for _, opt := range opts { - if v == "--"+opt.longName || v == "-"+opt.shortName { - if opt.callbackWithValue != nil { - if i+1 > len(os.Args) { - log.Printf("ERROR: Got %s without argument\n", v) - os.Exit(64) - } - i++ - opt.callbackWithValue(os.Args[i]) - } else if opt.callbackNoValue != nil { - opt.callbackNoValue() - } - knownParam = true - break - } - } - if !knownParam { - log.Printf("ERROR: unknown option %v\n", v) - printHelp() - os.Exit(64) - } - } - if configFilename != nil { - config.ourConfigFilename = *configFilename - } + // print the first message after logger is configured + log.Printf("AdGuard Home, version %s\n", VersionString) err := askUsernamePasswordIfPossible() if err != nil { @@ -204,12 +80,217 @@ func loadOptions() { } // override bind host/port from the console - if bindHost != nil { - config.BindHost = *bindHost + if args.bindHost != "" { + config.BindHost = args.bindHost } - if bindPort != nil { - config.BindPort = *bindPort + if args.bindPort != 0 { + config.BindPort = args.bindPort } + + // Load filters from the disk + // And if any filter has zero ID, assign a new one + for i := range config.Filters { + filter := &config.Filters[i] // otherwise we're operating on a copy + if filter.ID == 0 { + filter.ID = assignUniqueFilterID() + } + err = filter.load() + if err != nil { + // This is okay for the first start, the filter will be loaded later + log.Printf("Couldn't load filter %d contents due to %s", filter.ID, err) + // clear LastUpdated so it gets fetched right away + } + if len(filter.Rules) == 0 { + filter.LastUpdated = time.Time{} + } + } + + // Save the updated config + err = config.write() + if err != nil { + log.Fatal(err) + } + + box := packr.NewBox("build/static") + { + executable, osErr := os.Executable() + if osErr != nil { + panic(osErr) + } + + executableName := filepath.Base(executable) + if executableName == "AdGuardHome" { + // Binary build + config.ourBinaryDir = filepath.Dir(executable) + } else { + // Most likely we're debugging -- using current working directory in this case + workDir, _ := os.Getwd() + config.ourBinaryDir = workDir + } + log.Printf("Current working directory is %s", config.ourBinaryDir) + } + http.Handle("/", optionalAuthHandler(http.FileServer(box))) + registerControlHandlers() + + err = startDNSServer() + if err != nil { + log.Fatal(err) + } + + err = startDHCPServer() + if err != nil { + log.Fatal(err) + } + + // Update filters we've just loaded right away, don't wait for periodic update timer + go func() { + refreshFiltersIfNecessary(false) + // Save the updated config + err := config.write() + if err != nil { + log.Fatal(err) + } + }() + // Schedule automatic filters updates + go periodicallyRefreshFilters() +} + +// configureLogger configures logger level and output +func configureLogger(args options) { + ls := getLogSettings() + + // command-line arguments can override config settings + if args.verbose { + ls.Verbose = true + } + if args.logFile != "" { + ls.LogFile = args.logFile + } + + log.Verbose = ls.Verbose + + if ls.LogFile == "" { + return + } + + // TODO: add windows eventlog support + if ls.LogFile == "syslog" { + w, err := syslog.New(syslog.LOG_INFO, "AdGuard Home") + if err != nil { + log.Fatalf("cannot initialize syslog: %s", err) + } + stdlog.SetOutput(w) + } + + logFilePath := filepath.Join(config.ourBinaryDir, ls.LogFile) + file, err := os.OpenFile(logFilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0755) + if err != nil { + log.Fatalf("cannot create a log file: %s", err) + } + stdlog.SetOutput(file) +} + +func cleanup() { + log.Printf("Stopping AdGuard Home") + + err := stopDNSServer() + if err != nil { + log.Printf("Couldn't stop DNS server: %s", err) + } + err = stopDHCPServer() + if err != nil { + log.Printf("Couldn't stop DHCP server: %s", err) + } +} + +func getInput() (string, error) { + scanner := bufio.NewScanner(os.Stdin) + scanner.Scan() + text := scanner.Text() + err := scanner.Err() + return text, err +} + +// command-line arguments +type options struct { + verbose bool // is verbose logging enabled + configFilename string // path to the config file + bindHost string // host address to bind HTTP server on + bindPort int // port to serve HTTP pages on + logFile string // Path to the log file. If empty, write to stdout. If "syslog", writes to syslog + + // service control action (see service.ControlAction array + "status" command) + serviceControlAction string +} + +// loadOptions reads command line arguments and initializes configuration +func loadOptions() options { + o := options{} + + var printHelp func() + var opts = []struct { + longName string + shortName string + description string + callbackWithValue func(value string) + callbackNoValue func() + }{ + {"config", "c", "path to config file", func(value string) { o.configFilename = value }, nil}, + {"host", "o", "host address to bind HTTP server on", func(value string) { o.bindHost = value }, nil}, + {"port", "p", "port to serve HTTP pages on", func(value string) { + v, err := strconv.Atoi(value) + if err != nil { + panic("Got port that is not a number") + } + o.bindPort = v + }, nil}, + {"service", "s", "service control action: status, install, uninstall, start, stop, restart", func(value string) { + o.serviceControlAction = value + }, nil}, + {"logfile", "l", "path to the log file. If empty, writes to stdout, if 'syslog' -- system log", func(value string) { + o.logFile = value + }, nil}, + {"verbose", "v", "enable verbose output", nil, func() { o.verbose = true }}, + {"help", "h", "print this help", nil, func() { + printHelp() + os.Exit(64) + }}, + } + printHelp = func() { + fmt.Printf("Usage:\n\n") + fmt.Printf("%s [options]\n\n", os.Args[0]) + fmt.Printf("Options:\n") + for _, opt := range opts { + fmt.Printf(" -%s, %-30s %s\n", opt.shortName, "--"+opt.longName, opt.description) + } + } + for i := 1; i < len(os.Args); i++ { + v := os.Args[i] + knownParam := false + for _, opt := range opts { + if v == "--"+opt.longName || v == "-"+opt.shortName { + if opt.callbackWithValue != nil { + if i+1 >= len(os.Args) { + log.Printf("ERROR: Got %s without argument\n", v) + os.Exit(64) + } + i++ + opt.callbackWithValue(os.Args[i]) + } else if opt.callbackNoValue != nil { + opt.callbackNoValue() + } + knownParam = true + break + } + } + if !knownParam { + log.Printf("ERROR: unknown option %v\n", v) + printHelp() + os.Exit(64) + } + } + + return o } func promptAndGet(prompt string) (string, error) { diff --git a/config.go b/config.go index ed96e874..ca9f52e5 100644 --- a/config.go +++ b/config.go @@ -18,6 +18,12 @@ const ( filterDir = "filters" // cache location for downloaded filters, it's under DataDir ) +// logSettings +type logSettings struct { + LogFile string `yaml:"log_file"` // Path to the log file. If empty, write to stdout. If "syslog", writes to syslog + Verbose bool `yaml:"verbose"` // If true, verbose logging is enabled +} + // configuration is loaded from YAML // field ordering is important -- yaml fields will mirror ordering from here type configuration struct { @@ -34,6 +40,8 @@ type configuration struct { UserRules []string `yaml:"user_rules"` DHCP dhcpd.ServerConfig `yaml:"dhcp"` + logSettings `yaml:",inline"` + sync.RWMutex `yaml:"-"` SchemaVersion int `yaml:"schema_version"` // keeping last so that users will be less tempted to change it -- used when upgrading between versions @@ -79,20 +87,34 @@ var config = configuration{ SchemaVersion: currentSchemaVersion, } -// Loads configuration from the YAML file +// getLogSettings 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 getLogSettings() logSettings { + l := logSettings{} + yamlFile, err := readConfigFile() + if err != nil || yamlFile == nil { + return l + } + err = yaml.Unmarshal(yamlFile, &l) + if err != nil { + log.Printf("Couldn't get logging settings from the configuration: %s", err) + } + return l +} + +// parseConfig loads configuration from the YAML file func parseConfig() error { configFile := filepath.Join(config.ourBinaryDir, config.ourConfigFilename) log.Printf("Reading YAML file: %s", configFile) - if _, err := os.Stat(configFile); os.IsNotExist(err) { - // do nothing, file doesn't exist - log.Printf("YAML file doesn't exist, skipping: %s", configFile) - return nil - } - yamlFile, err := ioutil.ReadFile(configFile) + yamlFile, err := readConfigFile() if err != nil { log.Printf("Couldn't read config file: %s", err) return err } + if yamlFile == nil { + log.Printf("YAML file doesn't exist, skipping it") + return nil + } err = yaml.Unmarshal(yamlFile, &config) if err != nil { log.Printf("Couldn't parse config file: %s", err) @@ -107,6 +129,16 @@ func parseConfig() error { return nil } +// readConfigFile reads config file contents if it exists +func readConfigFile() ([]byte, error) { + configFile := filepath.Join(config.ourBinaryDir, config.ourConfigFilename) + if _, err := os.Stat(configFile); os.IsNotExist(err) { + // do nothing, file doesn't exist + return nil, nil + } + return ioutil.ReadFile(configFile) +} + // Saves configuration to the YAML file and also saves the user filter contents to a file func (c *configuration) write() error { c.Lock() diff --git a/dhcp.go b/dhcp.go index 90d6a88a..744e7a35 100644 --- a/dhcp.go +++ b/dhcp.go @@ -165,3 +165,20 @@ func startDHCPServer() error { } return nil } + +func stopDHCPServer() error { + if !config.DHCP.Enabled { + return nil + } + + if !dhcpServer.Enabled { + return nil + } + + err := dhcpServer.Stop() + if err != nil { + return errorx.Decorate(err, "Couldn't stop DHCP server") + } + + return nil +} diff --git a/go.mod b/go.mod index 24caab1a..a0dbd5d0 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/gobuffalo/packr v1.19.0 github.com/hmage/golibs v0.0.0-20181229160906-c8491df0bfc4 github.com/joomcode/errorx v0.1.0 + github.com/kardianos/service v0.0.0-20181115005516-4c239ee84e7b github.com/krolaw/dhcp4 v0.0.0-20180925202202-7cead472c414 github.com/miekg/dns v1.1.1 github.com/shirou/gopsutil v2.18.10+incompatible diff --git a/go.sum b/go.sum index 805c376a..fef136a4 100644 --- a/go.sum +++ b/go.sum @@ -37,6 +37,8 @@ github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/joomcode/errorx v0.1.0 h1:QmJMiI1DE1UFje2aI1ZWO/VMT5a32qBoXUclGOt8vsc= github.com/joomcode/errorx v0.1.0/go.mod h1:kgco15ekB6cs+4Xjzo7SPeXzx38PbJzBwbnu9qfVNHQ= +github.com/kardianos/service v0.0.0-20181115005516-4c239ee84e7b h1:vfiqKno48aUndBMjTeWFpCExNnTf2Xnd6d228L4EfTQ= +github.com/kardianos/service v0.0.0-20181115005516-4c239ee84e7b/go.mod h1:10UU/bEkzh2iEN6aYzbevY7J6p03KO5siTxQWXMEerg= github.com/krolaw/dhcp4 v0.0.0-20180925202202-7cead472c414 h1:6wnYc2S/lVM7BvR32BM74ph7bPgqMztWopMYKgVyEho= github.com/krolaw/dhcp4 v0.0.0-20180925202202-7cead472c414/go.mod h1:0AqAH3ZogsCrvrtUpvc6EtVKbc3w6xwZhkvGLuqyi3o= github.com/markbates/oncer v0.0.0-20181014194634-05fccaae8fc4 h1:Mlji5gkcpzkqTROyE4ZxZ8hN7osunMb2RuGVrbvMvCc= diff --git a/service.go b/service.go new file mode 100644 index 00000000..dd6e4403 --- /dev/null +++ b/service.go @@ -0,0 +1,76 @@ +package main + +import ( + "os" + + "github.com/hmage/golibs/log" + "github.com/kardianos/service" +) + +// Represents the program that will be launched by a service or daemon +type program struct { +} + +// Start should quickly start the program +func (p *program) Start(s service.Service) error { + // Start should not block. Do the actual work async. + args := options{} + go run(args) + return nil +} + +// Stop stops the program +func (p *program) Stop(s service.Service) error { + // Stop should not block. Return with a few seconds. + cleanup() + return nil +} + +// handleServiceControlAction one of the possible control actions: +// install -- installs a service/daemon +// 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 +func handleServiceControlAction(action string) { + log.Printf("Service control action: %s", action) + + pwd, err := os.Getwd() + if err != nil { + log.Fatal("Unable to find the path to the current directory") + } + svcConfig := &service.Config{ + Name: "AdGuardHome", + DisplayName: "AdGuard Home service", + Description: "AdGuard Home: Network-level blocker", + WorkingDirectory: pwd, + } + prg := &program{} + s, err := service.New(prg, svcConfig) + if err != nil { + log.Fatal(err) + } + + if action == "status" { + status, errSt := s.Status() + if errSt != nil { + log.Fatalf("failed to get service status: %s", errSt) + } + + switch status { + case service.StatusUnknown: + log.Printf("Service status is unknown") + case service.StatusStopped: + log.Printf("Service is stopped") + case service.StatusRunning: + log.Printf("Service is running") + } + } else { + err = service.Control(s, action) + if err != nil { + log.Fatal(err) + } + log.Printf("Action %s has been done successfully", action) + } +} From a4dc4c61d8ef3fb9f528b91c59da45c984730520 Mon Sep 17 00:00:00 2001 From: Andrey Meshkov Date: Mon, 4 Feb 2019 13:57:35 +0300 Subject: [PATCH 2/6] Added github.com/kardianos/service to README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 412d0942..bfbb2165 100644 --- a/README.md +++ b/README.md @@ -279,6 +279,7 @@ This software wouldn't have been possible without: * [gcache](https://github.com/bluele/gcache) * [miekg's dns](https://github.com/miekg/dns) * [go-yaml](https://github.com/go-yaml/yaml) + * [service](https://godoc.org/github.com/kardianos/service) * [Node.js](https://nodejs.org/) and it's libraries: * [React.js](https://reactjs.org) * [Tabler](https://github.com/tabler/tabler) From 448a6caeb89a00a59abf9fdbb145dac5630e623a Mon Sep 17 00:00:00 2001 From: Andrey Meshkov Date: Tue, 5 Feb 2019 14:09:05 +0300 Subject: [PATCH 3/6] Fixed review comments Fixed running as a windows service Added logging to windows evenlog --- .gometalinter.json | 5 ++ README.md | 22 +++++---- app.go | 111 +++++++++++++++++++++++++++++---------------- go.mod | 2 + go.sum | 2 + service.go | 105 +++++++++++++++++++++++++++++++++++++++++- syslog_others.go | 18 ++++++++ syslog_windows.go | 39 ++++++++++++++++ 8 files changed, 252 insertions(+), 52 deletions(-) create mode 100644 syslog_others.go create mode 100644 syslog_windows.go diff --git a/.gometalinter.json b/.gometalinter.json index e420bd51..2c3f557e 100644 --- a/.gometalinter.json +++ b/.gometalinter.json @@ -17,6 +17,11 @@ "WarnUnmatchedDirective": true, "EnableAll": true, + "DisableAll": false, + "Disable": [ + "maligned", + "goconst" + ], "Cyclo": 20, "LineLength": 200 diff --git a/README.md b/README.md index bfbb2165..6523c26e 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,17 @@ sudo ./AdGuardHome Now open the browser and navigate to http://localhost:3000/ to control your AdGuard Home service. +### Running as a service + +You can register AdGuard Home as a system service on Windows, Linux/(systemd | Upstart | SysV), and OSX/Launchd. + +* `AdGuardHome -s install` - install AdGuard Home as a system service. +* `AdGuardHome -s uninstall` - uninstalls the AdGuard Home service. +* `AdGuardHome -s start` - starts the service. +* `AdGuardHome -s stop` - stops the service. +* `AdGuardHome -s restart` - restarts the service. +* `AdGuardHome -s status` - shows the current service status. + ### Command-line arguments Here is a list of all available command-line arguments. @@ -119,15 +130,6 @@ Options: -h, --help print this help ``` -Please note, that you can register AdGuard Home as a system service on Windows, Linux/(systemd | Upstart | SysV), and OSX/Launchd. - -* `AdGuardHome -s install` - install as a system service. -* `AdGuardHome -s uninstall` - uninstall's AdGuard Home service. -* `AdGuardHome -s start` - starts the service. -* `AdGuardHome -s stop` - stops the service. -* `AdGuardHome -s restart` - restarts the service. -* `AdGuardHome -s status` - shows the current service status. - ### Running without superuser You can run AdGuard Home without superuser privileges, but you need to either grant the binary a capability (on Linux) or instruct it to use a different port (all platforms). @@ -198,7 +200,7 @@ Settings are stored in [YAML format](https://en.wikipedia.org/wiki/YAML), possib * `range_end` - end IP address of the controlled range. * `lease_duration` - lease duration in seconds. If 0, using default duration (2 hours). * `user_rules` — User-specified filtering rules. - * `log_file` — Path to the log file. If empty, writes to stdout, if 'syslog' -- system log. + * `log_file` — Path to the log file. If empty, writes to stdout, if `syslog` -- system log (or eventlog on Windows). * `verbose` — Enable our disables debug verbose output. Removing an entry from settings file will reset it to the default value. Deleting the file will reset all settings to the default values. diff --git a/app.go b/app.go index 94cb9348..1c74e14d 100644 --- a/app.go +++ b/app.go @@ -4,17 +4,20 @@ import ( "bufio" "fmt" stdlog "log" - "log/syslog" "net" "net/http" "os" "os/signal" + "path" "path/filepath" + "runtime" "strconv" + "strings" "syscall" "time" "github.com/gobuffalo/packr" + "github.com/hmage/golibs/log" "golang.org/x/crypto/ssh/terminal" ) @@ -22,6 +25,14 @@ import ( // VersionString will be set through ldflags, contains current version var VersionString = "undefined" +const ( + // We use it to detect the working dir + executableName = "AdGuardHome" + + // Used in config to indicate that syslog or eventlog (win) should be used for logger output + configSyslog = "syslog" +) + // main is the entry point func main() { // config can be specified, which reads options from there, but other command line flags have to override config values @@ -33,9 +44,6 @@ func main() { return } - // run the protection - run(args) - signalChannel := make(chan os.Signal) signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT) go func() { @@ -44,23 +52,30 @@ func main() { os.Exit(0) }() - address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.BindPort)) - URL := fmt.Sprintf("http://%s", address) - log.Println("Go to " + URL) - log.Fatal(http.ListenAndServe(address, nil)) + // run the protection + run(args) } // run initializes configuration and runs the AdGuard Home +// run is a blocking method and it won't exit until the service is stopped! func run(args options) { + // config file path can be overridden by command-line arguments: if args.configFilename != "" { config.ourConfigFilename = args.configFilename } + // configure working dir and config path + initWorkingDir() + // configure log level and output configureLogger(args) // print the first message after logger is configured log.Printf("AdGuard Home, version %s\n", VersionString) + log.Printf("Current working directory is %s", config.ourBinaryDir) + if args.runningAsService { + log.Printf("AdGuard Home is running as a service") + } err := askUsernamePasswordIfPossible() if err != nil { @@ -111,27 +126,6 @@ func run(args options) { log.Fatal(err) } - box := packr.NewBox("build/static") - { - executable, osErr := os.Executable() - if osErr != nil { - panic(osErr) - } - - executableName := filepath.Base(executable) - if executableName == "AdGuardHome" { - // Binary build - config.ourBinaryDir = filepath.Dir(executable) - } else { - // Most likely we're debugging -- using current working directory in this case - workDir, _ := os.Getwd() - config.ourBinaryDir = workDir - } - log.Printf("Current working directory is %s", config.ourBinaryDir) - } - http.Handle("/", optionalAuthHandler(http.FileServer(box))) - registerControlHandlers() - err = startDNSServer() if err != nil { log.Fatal(err) @@ -153,6 +147,35 @@ func run(args options) { }() // Schedule automatic filters updates go periodicallyRefreshFilters() + + // Initialize and run the admin Web interface + box := packr.NewBox("build/static") + http.Handle("/", optionalAuthHandler(http.FileServer(box))) + registerControlHandlers() + + address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.BindPort)) + URL := fmt.Sprintf("http://%s", address) + log.Println("Go to " + URL) + log.Fatal(http.ListenAndServe(address, nil)) +} + +// initWorkingDir initializes the ourBinaryDir (basically, we use it as a working dir) +func initWorkingDir() { + exec, err := os.Executable() + if err != nil { + panic(err) + } + + currentExecutableName := filepath.Base(exec) + currentExecutableName = strings.TrimSuffix(currentExecutableName, path.Ext(currentExecutableName)) + if currentExecutableName == executableName { + // Binary build + config.ourBinaryDir = filepath.Dir(exec) + } else { + // Most likely we're debugging -- using current working directory in this case + workDir, _ := os.Getwd() + config.ourBinaryDir = workDir + } } // configureLogger configures logger level and output @@ -169,25 +192,30 @@ func configureLogger(args options) { log.Verbose = ls.Verbose + if args.runningAsService && ls.LogFile == "" && runtime.GOOS == "windows" { + // When running as a Windows service, use eventlog by default if nothing else is configured + // Otherwise, we'll simply loose the log output + ls.LogFile = configSyslog + } + if ls.LogFile == "" { return } - // TODO: add windows eventlog support - if ls.LogFile == "syslog" { - w, err := syslog.New(syslog.LOG_INFO, "AdGuard Home") + if ls.LogFile == configSyslog { + // Use syslog where it is possible and eventlog on Windows + err := configureSyslog() if err != nil { log.Fatalf("cannot initialize syslog: %s", err) } - stdlog.SetOutput(w) + } else { + logFilePath := filepath.Join(config.ourBinaryDir, ls.LogFile) + file, err := os.OpenFile(logFilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0755) + if err != nil { + log.Fatalf("cannot create a log file: %s", err) + } + stdlog.SetOutput(file) } - - logFilePath := filepath.Join(config.ourBinaryDir, ls.LogFile) - file, err := os.OpenFile(logFilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0755) - if err != nil { - log.Fatalf("cannot create a log file: %s", err) - } - stdlog.SetOutput(file) } func cleanup() { @@ -221,6 +249,9 @@ type options struct { // service control action (see service.ControlAction array + "status" command) serviceControlAction string + + // runningAsService flag is set to true when options are passed from the service runner + runningAsService bool } // loadOptions reads command line arguments and initializes configuration diff --git a/go.mod b/go.mod index a0dbd5d0..cc8f3327 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/gobuffalo/packr v1.19.0 github.com/hmage/golibs v0.0.0-20181229160906-c8491df0bfc4 github.com/joomcode/errorx v0.1.0 + github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 // indirect github.com/kardianos/service v0.0.0-20181115005516-4c239ee84e7b github.com/krolaw/dhcp4 v0.0.0-20180925202202-7cead472c414 github.com/miekg/dns v1.1.1 @@ -17,6 +18,7 @@ require ( go.uber.org/goleak v0.10.0 golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 golang.org/x/net v0.0.0-20181220203305-927f97764cc3 + golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb gopkg.in/asaskevich/govalidator.v4 v4.0.0-20160518190739-766470278477 gopkg.in/yaml.v2 v2.2.1 ) diff --git a/go.sum b/go.sum index fef136a4..29de0319 100644 --- a/go.sum +++ b/go.sum @@ -37,6 +37,8 @@ github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/joomcode/errorx v0.1.0 h1:QmJMiI1DE1UFje2aI1ZWO/VMT5a32qBoXUclGOt8vsc= github.com/joomcode/errorx v0.1.0/go.mod h1:kgco15ekB6cs+4Xjzo7SPeXzx38PbJzBwbnu9qfVNHQ= +github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 h1:PJPDf8OUfOK1bb/NeTKd4f1QXZItOX389VN3B6qC8ro= +github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/kardianos/service v0.0.0-20181115005516-4c239ee84e7b h1:vfiqKno48aUndBMjTeWFpCExNnTf2Xnd6d228L4EfTQ= github.com/kardianos/service v0.0.0-20181115005516-4c239ee84e7b/go.mod h1:10UU/bEkzh2iEN6aYzbevY7J6p03KO5siTxQWXMEerg= github.com/krolaw/dhcp4 v0.0.0-20180925202202-7cead472c414 h1:6wnYc2S/lVM7BvR32BM74ph7bPgqMztWopMYKgVyEho= diff --git a/service.go b/service.go index dd6e4403..f7ffb8da 100644 --- a/service.go +++ b/service.go @@ -2,11 +2,17 @@ package main import ( "os" + "runtime" "github.com/hmage/golibs/log" "github.com/kardianos/service" ) +const ( + launchdStdoutPath = "/var/log/AdGuardHome.stdout.log" + launchdStderrPath = "/var/log/AdGuardHome.stderr.log" +) + // Represents the program that will be launched by a service or daemon type program struct { } @@ -14,7 +20,7 @@ type program struct { // Start should quickly start the program func (p *program) Start(s service.Service) error { // Start should not block. Do the actual work async. - args := options{} + args := options{runningAsService: true} go run(args) return nil } @@ -23,6 +29,7 @@ func (p *program) Start(s service.Service) error { func (p *program) Stop(s service.Service) error { // Stop should not block. Return with a few seconds. cleanup() + os.Exit(0) return nil } @@ -33,6 +40,9 @@ func (p *program) Stop(s service.Service) error { // start -- starts the previously installed service // stop -- stops the previously installed service // restart - restarts the previously installed service +// 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/daemon. func handleServiceControlAction(action string) { log.Printf("Service control action: %s", action) @@ -45,7 +55,9 @@ func handleServiceControlAction(action string) { DisplayName: "AdGuard Home service", Description: "AdGuard Home: Network-level blocker", WorkingDirectory: pwd, + Arguments: []string{"-s", "run"}, } + configureService(svcConfig) prg := &program{} s, err := service.New(prg, svcConfig) if err != nil { @@ -66,11 +78,100 @@ func handleServiceControlAction(action string) { case service.StatusRunning: log.Printf("Service is running") } + } else if action == "run" { + err = s.Run() + if err != nil { + log.Fatalf("Failed to run service: %s", err) + } } else { + if action == "uninstall" { + // In case of Windows and Linux when a running service is being uninstalled, + // it is just marked for deletion but not stopped + // So we explicitly stop it here + _ = s.Stop() + } + err = service.Control(s, action) if err != nil { log.Fatal(err) } - log.Printf("Action %s has been done successfully", action) + log.Printf("Action %s has been done successfully on %s", action, service.ChosenSystem().String()) + + if action == "install" { + // Start automatically after install + err = service.Control(s, "start") + if err != nil { + log.Fatalf("Failed to start the service: %s", err) + } + log.Printf("Service has been started") + } else if action == "uninstall" { + cleanupService() + } } } + +// configureService defines additional settings of the service +func configureService(c *service.Config) { + c.Option = service.KeyValue{} + + // OS X + // Redefines the launchd config file template + // The purpose is to enable stdout/stderr redirect by default + c.Option["LaunchdConfig"] = launchdConfig + // This key is used to start the job as soon as it has been loaded. For daemons this means execution at boot time, for agents execution at login. + c.Option["RunAtLoad"] = true + + // POSIX + // Redirect StdErr & StdOut to files. + c.Option["LogOutput"] = true + + // Windows + if runtime.GOOS == "windows" { + c.UserName = "NT AUTHORITY\\NetworkService" + } +} + +// cleanupService called on the service uninstall, cleans up additional files if needed +func cleanupService() { + if runtime.GOOS == "darwin" { + // Removing log files on cleanup and ignore errors + err := os.Remove(launchdStdoutPath) + if err != nil && !os.IsNotExist(err) { + log.Printf("cannot remove %s", launchdStdoutPath) + } + err = os.Remove(launchdStderrPath) + if err != nil && !os.IsNotExist(err) { + log.Printf("cannot remove %s", launchdStderrPath) + } + } +} + +// Basically the same template as the one defined in github.com/kardianos/service +// but with two additional keys - StandardOutPath and StandardErrorPath +var launchdConfig = ` + + + +Label{{html .Name}} +ProgramArguments + + {{html .Path}} +{{range .Config.Arguments}} + {{html .}} +{{end}} + +{{if .UserName}}UserName{{html .UserName}}{{end}} +{{if .ChRoot}}RootDirectory{{html .ChRoot}}{{end}} +{{if .WorkingDirectory}}WorkingDirectory{{html .WorkingDirectory}}{{end}} +SessionCreate<{{bool .SessionCreate}}/> +KeepAlive<{{bool .KeepAlive}}/> +RunAtLoad<{{bool .RunAtLoad}}/> +Disabled +StandardOutPath +` + launchdStdoutPath + ` +StandardErrorPath +` + launchdStderrPath + ` + + +` diff --git a/syslog_others.go b/syslog_others.go new file mode 100644 index 00000000..629d170d --- /dev/null +++ b/syslog_others.go @@ -0,0 +1,18 @@ +// +build !windows,!nacl,!plan9 + +package main + +import ( + "log" + "log/syslog" +) + +// configureSyslog reroutes standard logger output to syslog +func configureSyslog() error { + w, err := syslog.New(syslog.LOG_NOTICE|syslog.LOG_USER, "AdGuard Home") + if err != nil { + return err + } + log.SetOutput(w) + return nil +} diff --git a/syslog_windows.go b/syslog_windows.go new file mode 100644 index 00000000..b6f1d885 --- /dev/null +++ b/syslog_windows.go @@ -0,0 +1,39 @@ +package main + +import ( + "log" + "strings" + + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/svc/eventlog" +) + +// should be the same as the service name! +const eventLogSrc = "AdGuardHome" + +type eventLogWriter struct { + el *eventlog.Log +} + +// Write sends a log message to the Event Log. +func (w *eventLogWriter) Write(b []byte) (int, error) { + return len(b), w.el.Info(1, string(b)) +} + +func configureSyslog() error { + // Continue if we receive "registry key already exists" or if we get + // ERROR_ACCESS_DENIED so that we can log without administrative permissions + // for pre-existing eventlog sources. + if err := eventlog.InstallAsEventCreate(eventLogSrc, eventlog.Info|eventlog.Warning|eventlog.Error); err != nil { + if !strings.Contains(err.Error(), "registry key already exists") && err != windows.ERROR_ACCESS_DENIED { + return err + } + } + el, err := eventlog.Open(eventLogSrc) + if err != nil { + return err + } + + log.SetOutput(&eventLogWriter{el: el}) + return nil +} From aab8da4c7c20685befffc24f351f5d4d01758dd8 Mon Sep 17 00:00:00 2001 From: Andrey Meshkov Date: Tue, 5 Feb 2019 14:15:22 +0300 Subject: [PATCH 4/6] Added logging description to README --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 6523c26e..d6379ae5 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,18 @@ You can register AdGuard Home as a system service on Windows, Linux/(systemd | U * `AdGuardHome -s restart` - restarts the service. * `AdGuardHome -s status` - shows the current service status. +### Logging + +By default, the log is written to `stderr` when you run AdGuard Home as a console application. + +If you run it as a service/daemon, the log output depends on the platform: + +* Linux: the log is written to syslog. +* MacOS: the log is written to `/var/log/AdGuardHome.*.log` files. +* Windows: the log is written to the Windows event log. + +You can redefine this behavior in AdGuard Home configuration file (see below). + ### Command-line arguments Here is a list of all available command-line arguments. @@ -130,6 +142,8 @@ Options: -h, --help print this help ``` +Please note, that the command-line arguments override settings from the configuration file. + ### Running without superuser You can run AdGuard Home without superuser privileges, but you need to either grant the binary a capability (on Linux) or instruct it to use a different port (all platforms). From 93ea27077fc727a072ba01bb029cd0c81800f1dc Mon Sep 17 00:00:00 2001 From: Andrey Meshkov Date: Tue, 5 Feb 2019 14:21:07 +0300 Subject: [PATCH 5/6] service properties to constants --- service.go | 13 ++++++++----- syslog_others.go | 2 +- syslog_windows.go | 10 +++++----- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/service.go b/service.go index f7ffb8da..fd233f4e 100644 --- a/service.go +++ b/service.go @@ -9,8 +9,11 @@ import ( ) const ( - launchdStdoutPath = "/var/log/AdGuardHome.stdout.log" - launchdStderrPath = "/var/log/AdGuardHome.stderr.log" + launchdStdoutPath = "/var/log/AdGuardHome.stdout.log" + launchdStderrPath = "/var/log/AdGuardHome.stderr.log" + serviceName = "AdGuardHome" + serviceDisplayName = "AdGuard Home service" + serviceDescription = "AdGuard Home: Network-level blocker" ) // Represents the program that will be launched by a service or daemon @@ -51,9 +54,9 @@ func handleServiceControlAction(action string) { log.Fatal("Unable to find the path to the current directory") } svcConfig := &service.Config{ - Name: "AdGuardHome", - DisplayName: "AdGuard Home service", - Description: "AdGuard Home: Network-level blocker", + Name: serviceName, + DisplayName: serviceDisplayName, + Description: serviceDescription, WorkingDirectory: pwd, Arguments: []string{"-s", "run"}, } diff --git a/syslog_others.go b/syslog_others.go index 629d170d..1813bb99 100644 --- a/syslog_others.go +++ b/syslog_others.go @@ -9,7 +9,7 @@ import ( // configureSyslog reroutes standard logger output to syslog func configureSyslog() error { - w, err := syslog.New(syslog.LOG_NOTICE|syslog.LOG_USER, "AdGuard Home") + w, err := syslog.New(syslog.LOG_NOTICE|syslog.LOG_USER, serviceName) if err != nil { return err } diff --git a/syslog_windows.go b/syslog_windows.go index b6f1d885..e81b63ee 100644 --- a/syslog_windows.go +++ b/syslog_windows.go @@ -8,9 +8,6 @@ import ( "golang.org/x/sys/windows/svc/eventlog" ) -// should be the same as the service name! -const eventLogSrc = "AdGuardHome" - type eventLogWriter struct { el *eventlog.Log } @@ -21,15 +18,18 @@ func (w *eventLogWriter) Write(b []byte) (int, error) { } func configureSyslog() error { + // Note that the eventlog src is the same as the service name + // Otherwise, we will get "the description for event id cannot be found" warning in every log record + // Continue if we receive "registry key already exists" or if we get // ERROR_ACCESS_DENIED so that we can log without administrative permissions // for pre-existing eventlog sources. - if err := eventlog.InstallAsEventCreate(eventLogSrc, eventlog.Info|eventlog.Warning|eventlog.Error); err != nil { + if err := eventlog.InstallAsEventCreate(serviceName, eventlog.Info|eventlog.Warning|eventlog.Error); err != nil { if !strings.Contains(err.Error(), "registry key already exists") && err != windows.ERROR_ACCESS_DENIED { return err } } - el, err := eventlog.Open(eventLogSrc) + el, err := eventlog.Open(serviceName) if err != nil { return err } From 0fbfa057b1e2e709357fda08caea20fcbc61e9f4 Mon Sep 17 00:00:00 2001 From: Andrey Meshkov Date: Tue, 5 Feb 2019 20:35:48 +0300 Subject: [PATCH 6/6] Get rid of hardcoded binary name --- app.go | 28 ++++++++-------------------- config.go | 15 ++++++++++++--- upgrade.go | 4 ++-- 3 files changed, 22 insertions(+), 25 deletions(-) diff --git a/app.go b/app.go index 1c74e14d..2e72e1de 100644 --- a/app.go +++ b/app.go @@ -8,11 +8,9 @@ import ( "net/http" "os" "os/signal" - "path" "path/filepath" "runtime" "strconv" - "strings" "syscall" "time" @@ -26,9 +24,6 @@ import ( var VersionString = "undefined" const ( - // We use it to detect the working dir - executableName = "AdGuardHome" - // Used in config to indicate that syslog or eventlog (win) should be used for logger output configSyslog = "syslog" ) @@ -65,7 +60,7 @@ func run(args options) { } // configure working dir and config path - initWorkingDir() + initWorkingDir(args) // configure log level and output configureLogger(args) @@ -160,21 +155,17 @@ func run(args options) { } // initWorkingDir initializes the ourBinaryDir (basically, we use it as a working dir) -func initWorkingDir() { +func initWorkingDir(args options) { exec, err := os.Executable() if err != nil { panic(err) } - currentExecutableName := filepath.Base(exec) - currentExecutableName = strings.TrimSuffix(currentExecutableName, path.Ext(currentExecutableName)) - if currentExecutableName == executableName { - // Binary build - config.ourBinaryDir = filepath.Dir(exec) + if args.configFilename != "" { + // If there is a custom config file, use it's directory as our working dir + config.ourBinaryDir = filepath.Dir(args.configFilename) } else { - // Most likely we're debugging -- using current working directory in this case - workDir, _ := os.Getwd() - config.ourBinaryDir = workDir + config.ourBinaryDir = filepath.Dir(exec) } } @@ -356,11 +347,8 @@ func promptAndGetPassword(prompt string) (string, error) { } func askUsernamePasswordIfPossible() error { - configfile := config.ourConfigFilename - if !filepath.IsAbs(configfile) { - configfile = filepath.Join(config.ourBinaryDir, config.ourConfigFilename) - } - _, err := os.Stat(configfile) + configFile := config.getConfigFilename() + _, err := os.Stat(configFile) if !os.IsNotExist(err) { // do nothing, file exists return nil diff --git a/config.go b/config.go index ca9f52e5..33d717ef 100644 --- a/config.go +++ b/config.go @@ -87,6 +87,15 @@ var config = configuration{ SchemaVersion: currentSchemaVersion, } +// getConfigFilename returns path to the current config file +func (c *configuration) getConfigFilename() string { + configFile := config.ourConfigFilename + if !filepath.IsAbs(configFile) { + configFile = filepath.Join(config.ourBinaryDir, config.ourConfigFilename) + } + return configFile +} + // getLogSettings 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 getLogSettings() logSettings { @@ -104,7 +113,7 @@ func getLogSettings() logSettings { // parseConfig loads configuration from the YAML file func parseConfig() error { - configFile := filepath.Join(config.ourBinaryDir, config.ourConfigFilename) + configFile := config.getConfigFilename() log.Printf("Reading YAML file: %s", configFile) yamlFile, err := readConfigFile() if err != nil { @@ -131,7 +140,7 @@ func parseConfig() error { // readConfigFile reads config file contents if it exists func readConfigFile() ([]byte, error) { - configFile := filepath.Join(config.ourBinaryDir, config.ourConfigFilename) + configFile := config.getConfigFilename() if _, err := os.Stat(configFile); os.IsNotExist(err) { // do nothing, file doesn't exist return nil, nil @@ -143,7 +152,7 @@ func readConfigFile() ([]byte, error) { func (c *configuration) write() error { c.Lock() defer c.Unlock() - configFile := filepath.Join(config.ourBinaryDir, config.ourConfigFilename) + configFile := config.getConfigFilename() log.Printf("Writing YAML file: %s", configFile) yamlText, err := yaml.Marshal(&config) if err != nil { diff --git a/upgrade.go b/upgrade.go index 4b142de6..3d79e744 100644 --- a/upgrade.go +++ b/upgrade.go @@ -15,7 +15,7 @@ const currentSchemaVersion = 2 // used for upgrading from old configs to new con // Performs necessary upgrade operations if needed func upgradeConfig() error { // read a config file into an interface map, so we can manipulate values without losing any - configFile := filepath.Join(config.ourBinaryDir, config.ourConfigFilename) + configFile := config.getConfigFilename() if _, err := os.Stat(configFile); os.IsNotExist(err) { log.Printf("config file %s does not exist, nothing to upgrade", configFile) return nil @@ -74,7 +74,7 @@ func upgradeConfigSchema(oldVersion int, diskConfig *map[string]interface{}) err return err } - configFile := filepath.Join(config.ourBinaryDir, config.ourConfigFilename) + configFile := config.getConfigFilename() body, err := yaml.Marshal(diskConfig) if err != nil { log.Printf("Couldn't generate YAML file: %s", err)