Merge pull request #144 in DNS/adguard-dns from feature/490 to master

* commit '0fbfa057b1e2e709357fda08caea20fcbc61e9f4':
  Get rid of hardcoded binary name
  service properties to constants
  Added logging description to README
  Fixed review comments Fixed running as a windows service Added logging to windows evenlog
  Added github.com/kardianos/service to README
  AdGuard Home as a system service
This commit is contained in:
Andrey Meshkov 2019-02-05 22:02:01 +03:00
commit a8cdc5b01c
12 changed files with 631 additions and 166 deletions

1
.gitignore vendored
View File

@ -4,6 +4,7 @@
/AdGuardHome /AdGuardHome
/AdGuardHome.exe /AdGuardHome.exe
/AdGuardHome.yaml /AdGuardHome.yaml
/AdGuardHome.log
/data/ /data/
/build/ /build/
/dist/ /dist/

View File

@ -17,6 +17,11 @@
"WarnUnmatchedDirective": true, "WarnUnmatchedDirective": true,
"EnableAll": true, "EnableAll": true,
"DisableAll": false,
"Disable": [
"maligned",
"goconst"
],
"Cyclo": 20, "Cyclo": 20,
"LineLength": 200 "LineLength": 200

View File

@ -99,6 +99,51 @@ sudo ./AdGuardHome
Now open the browser and navigate to http://localhost:3000/ to control your AdGuard Home service. 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.
### 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.
```
$ ./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 the command-line arguments override settings from the configuration file.
### Running without superuser ### 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). 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 +184,7 @@ Settings are stored in [YAML format](https://en.wikipedia.org/wiki/YAML), possib
* `auth_name` — Web interface optional authorization username. * `auth_name` — Web interface optional authorization username.
* `auth_pass` — Web interface optional authorization password. * `auth_pass` — Web interface optional authorization password.
* `dns` — DNS configuration section. * `dns` — DNS configuration section.
* `bind_host` - DNS interface IP address to listen on.
* `port` — DNS server port 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. * `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. * `filtering_enabled` — Filtering of DNS requests based on filter lists.
@ -159,7 +205,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. * `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. * `last_updated` — Time when the filter was last updated from server.
* `ID` - filter ID (must be unique). * `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. * `user_rules` — User-specified filtering rules.
* `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. Removing an entry from settings file will reset it to the default value. Deleting the file will reset all settings to the default values.
@ -239,6 +295,7 @@ This software wouldn't have been possible without:
* [gcache](https://github.com/bluele/gcache) * [gcache](https://github.com/bluele/gcache)
* [miekg's dns](https://github.com/miekg/dns) * [miekg's dns](https://github.com/miekg/dns)
* [go-yaml](https://github.com/go-yaml/yaml) * [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: * [Node.js](https://nodejs.org/) and it's libraries:
* [React.js](https://reactjs.org) * [React.js](https://reactjs.org)
* [Tabler](https://github.com/tabler/tabler) * [Tabler](https://github.com/tabler/tabler)

408
app.go
View File

@ -3,16 +3,19 @@ package main
import ( import (
"bufio" "bufio"
"fmt" "fmt"
stdlog "log"
"net" "net"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"runtime"
"strconv" "strconv"
"syscall" "syscall"
"time" "time"
"github.com/gobuffalo/packr" "github.com/gobuffalo/packr"
"github.com/hmage/golibs/log" "github.com/hmage/golibs/log"
"golang.org/x/crypto/ssh/terminal" "golang.org/x/crypto/ssh/terminal"
) )
@ -20,59 +23,22 @@ import (
// VersionString will be set through ldflags, contains current version // VersionString will be set through ldflags, contains current version
var VersionString = "undefined" var VersionString = "undefined"
const (
// 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() { 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 // 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 // therefore, we must do it manually instead of using a lib
loadOptions() args := loadOptions()
// Load filters from the disk if args.serviceControlAction != "" {
// And if any filter has zero ID, assign a new one handleServiceControlAction(args.serviceControlAction)
for i := range config.Filters { return
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{}
}
} }
// 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)
}
}()
signalChannel := make(chan os.Signal) signalChannel := make(chan os.Signal)
signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT) signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
go func() { go func() {
@ -81,109 +47,29 @@ func main() {
os.Exit(0) os.Exit(0)
}() }()
// Save the updated config // run the protection
err := config.write() run(args)
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() { // run initializes configuration and runs the AdGuard Home
err := stopDNSServer() // run is a blocking method and it won't exit until the service is stopped!
if err != nil { func run(args options) {
log.Printf("Couldn't stop DNS server: %s", err) // config file path can be overridden by command-line arguments:
if args.configFilename != "" {
config.ourConfigFilename = args.configFilename
} }
}
func getInput() (string, error) { // configure working dir and config path
scanner := bufio.NewScanner(os.Stdin) initWorkingDir(args)
scanner.Scan()
text := scanner.Text()
err := scanner.Err()
return text, err
}
// loadOptions reads command line arguments and initializes configuration // configure log level and output
func loadOptions() { configureLogger(args)
var printHelp func()
var configFilename *string // print the first message after logger is configured
var bindHost *string log.Printf("AdGuard Home, version %s\n", VersionString)
var bindPort *int log.Printf("Current working directory is %s", config.ourBinaryDir)
var opts = []struct { if args.runningAsService {
longName string log.Printf("AdGuard Home is running as a service")
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
} }
err := askUsernamePasswordIfPossible() err := askUsernamePasswordIfPossible()
@ -204,12 +90,229 @@ func loadOptions() {
} }
// override bind host/port from the console // override bind host/port from the console
if bindHost != nil { if args.bindHost != "" {
config.BindHost = *bindHost config.BindHost = args.bindHost
} }
if bindPort != nil { if args.bindPort != 0 {
config.BindPort = *bindPort 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)
}
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()
// 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(args options) {
exec, err := os.Executable()
if err != nil {
panic(err)
}
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 {
config.ourBinaryDir = filepath.Dir(exec)
}
}
// 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 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
}
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)
}
} 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)
}
}
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
// runningAsService flag is set to true when options are passed from the service runner
runningAsService bool
}
// 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) { func promptAndGet(prompt string) (string, error) {
@ -244,11 +347,8 @@ func promptAndGetPassword(prompt string) (string, error) {
} }
func askUsernamePasswordIfPossible() error { func askUsernamePasswordIfPossible() error {
configfile := config.ourConfigFilename configFile := config.getConfigFilename()
if !filepath.IsAbs(configfile) { _, err := os.Stat(configFile)
configfile = filepath.Join(config.ourBinaryDir, config.ourConfigFilename)
}
_, err := os.Stat(configfile)
if !os.IsNotExist(err) { if !os.IsNotExist(err) {
// do nothing, file exists // do nothing, file exists
return nil return nil

View File

@ -18,6 +18,12 @@ const (
filterDir = "filters" // cache location for downloaded filters, it's under DataDir 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 // configuration is loaded from YAML
// field ordering is important -- yaml fields will mirror ordering from here // field ordering is important -- yaml fields will mirror ordering from here
type configuration struct { type configuration struct {
@ -34,6 +40,8 @@ type configuration struct {
UserRules []string `yaml:"user_rules"` UserRules []string `yaml:"user_rules"`
DHCP dhcpd.ServerConfig `yaml:"dhcp"` DHCP dhcpd.ServerConfig `yaml:"dhcp"`
logSettings `yaml:",inline"`
sync.RWMutex `yaml:"-"` 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 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,43 @@ var config = configuration{
SchemaVersion: currentSchemaVersion, SchemaVersion: currentSchemaVersion,
} }
// Loads configuration from the YAML file // getConfigFilename returns path to the current config file
func parseConfig() error { func (c *configuration) getConfigFilename() string {
configFile := filepath.Join(config.ourBinaryDir, config.ourConfigFilename) configFile := config.ourConfigFilename
log.Printf("Reading YAML file: %s", configFile) if !filepath.IsAbs(configFile) {
if _, err := os.Stat(configFile); os.IsNotExist(err) { configFile = filepath.Join(config.ourBinaryDir, config.ourConfigFilename)
// do nothing, file doesn't exist
log.Printf("YAML file doesn't exist, skipping: %s", configFile)
return nil
} }
yamlFile, err := ioutil.ReadFile(configFile) 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 {
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 := config.getConfigFilename()
log.Printf("Reading YAML file: %s", configFile)
yamlFile, err := readConfigFile()
if err != nil { if err != nil {
log.Printf("Couldn't read config file: %s", err) log.Printf("Couldn't read config file: %s", err)
return err return err
} }
if yamlFile == nil {
log.Printf("YAML file doesn't exist, skipping it")
return nil
}
err = yaml.Unmarshal(yamlFile, &config) err = yaml.Unmarshal(yamlFile, &config)
if err != nil { if err != nil {
log.Printf("Couldn't parse config file: %s", err) log.Printf("Couldn't parse config file: %s", err)
@ -107,11 +138,21 @@ func parseConfig() error {
return nil return nil
} }
// readConfigFile reads config file contents if it exists
func readConfigFile() ([]byte, error) {
configFile := config.getConfigFilename()
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 // Saves configuration to the YAML file and also saves the user filter contents to a file
func (c *configuration) write() error { func (c *configuration) write() error {
c.Lock() c.Lock()
defer c.Unlock() defer c.Unlock()
configFile := filepath.Join(config.ourBinaryDir, config.ourConfigFilename) configFile := config.getConfigFilename()
log.Printf("Writing YAML file: %s", configFile) log.Printf("Writing YAML file: %s", configFile)
yamlText, err := yaml.Marshal(&config) yamlText, err := yaml.Marshal(&config)
if err != nil { if err != nil {

17
dhcp.go
View File

@ -165,3 +165,20 @@ func startDHCPServer() error {
} }
return nil 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
}

3
go.mod
View File

@ -9,6 +9,8 @@ require (
github.com/gobuffalo/packr v1.19.0 github.com/gobuffalo/packr v1.19.0
github.com/hmage/golibs v0.0.0-20181229160906-c8491df0bfc4 github.com/hmage/golibs v0.0.0-20181229160906-c8491df0bfc4
github.com/joomcode/errorx v0.1.0 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/krolaw/dhcp4 v0.0.0-20180925202202-7cead472c414
github.com/miekg/dns v1.1.1 github.com/miekg/dns v1.1.1
github.com/shirou/gopsutil v2.18.10+incompatible github.com/shirou/gopsutil v2.18.10+incompatible
@ -16,6 +18,7 @@ require (
go.uber.org/goleak v0.10.0 go.uber.org/goleak v0.10.0
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9
golang.org/x/net v0.0.0-20181220203305-927f97764cc3 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/asaskevich/govalidator.v4 v4.0.0-20160518190739-766470278477
gopkg.in/yaml.v2 v2.2.1 gopkg.in/yaml.v2 v2.2.1
) )

4
go.sum
View File

@ -37,6 +37,10 @@ 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/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 h1:QmJMiI1DE1UFje2aI1ZWO/VMT5a32qBoXUclGOt8vsc=
github.com/joomcode/errorx v0.1.0/go.mod h1:kgco15ekB6cs+4Xjzo7SPeXzx38PbJzBwbnu9qfVNHQ= 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= 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/krolaw/dhcp4 v0.0.0-20180925202202-7cead472c414/go.mod h1:0AqAH3ZogsCrvrtUpvc6EtVKbc3w6xwZhkvGLuqyi3o=
github.com/markbates/oncer v0.0.0-20181014194634-05fccaae8fc4 h1:Mlji5gkcpzkqTROyE4ZxZ8hN7osunMb2RuGVrbvMvCc= github.com/markbates/oncer v0.0.0-20181014194634-05fccaae8fc4 h1:Mlji5gkcpzkqTROyE4ZxZ8hN7osunMb2RuGVrbvMvCc=

180
service.go Normal file
View File

@ -0,0 +1,180 @@
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"
serviceName = "AdGuardHome"
serviceDisplayName = "AdGuard Home service"
serviceDescription = "AdGuard Home: Network-level blocker"
)
// 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{runningAsService: true}
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()
os.Exit(0)
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
// 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)
pwd, err := os.Getwd()
if err != nil {
log.Fatal("Unable to find the path to the current directory")
}
svcConfig := &service.Config{
Name: serviceName,
DisplayName: serviceDisplayName,
Description: serviceDescription,
WorkingDirectory: pwd,
Arguments: []string{"-s", "run"},
}
configureService(svcConfig)
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 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 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 = `<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd" >
<plist version='1.0'>
<dict>
<key>Label</key><string>{{html .Name}}</string>
<key>ProgramArguments</key>
<array>
<string>{{html .Path}}</string>
{{range .Config.Arguments}}
<string>{{html .}}</string>
{{end}}
</array>
{{if .UserName}}<key>UserName</key><string>{{html .UserName}}</string>{{end}}
{{if .ChRoot}}<key>RootDirectory</key><string>{{html .ChRoot}}</string>{{end}}
{{if .WorkingDirectory}}<key>WorkingDirectory</key><string>{{html .WorkingDirectory}}</string>{{end}}
<key>SessionCreate</key><{{bool .SessionCreate}}/>
<key>KeepAlive</key><{{bool .KeepAlive}}/>
<key>RunAtLoad</key><{{bool .RunAtLoad}}/>
<key>Disabled</key><false/>
<key>StandardOutPath</key>
<string>` + launchdStdoutPath + `</string>
<key>StandardErrorPath</key>
<string>` + launchdStderrPath + `</string>
</dict>
</plist>
`

18
syslog_others.go Normal file
View File

@ -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, serviceName)
if err != nil {
return err
}
log.SetOutput(w)
return nil
}

39
syslog_windows.go Normal file
View File

@ -0,0 +1,39 @@
package main
import (
"log"
"strings"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/svc/eventlog"
)
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 {
// 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(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(serviceName)
if err != nil {
return err
}
log.SetOutput(&eventLogWriter{el: el})
return nil
}

View File

@ -15,7 +15,7 @@ const currentSchemaVersion = 2 // used for upgrading from old configs to new con
// Performs necessary upgrade operations if needed // Performs necessary upgrade operations if needed
func upgradeConfig() error { func upgradeConfig() error {
// read a config file into an interface map, so we can manipulate values without losing any // 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) { if _, err := os.Stat(configFile); os.IsNotExist(err) {
log.Printf("config file %s does not exist, nothing to upgrade", configFile) log.Printf("config file %s does not exist, nothing to upgrade", configFile)
return nil return nil
@ -74,7 +74,7 @@ func upgradeConfigSchema(oldVersion int, diskConfig *map[string]interface{}) err
return err return err
} }
configFile := filepath.Join(config.ourBinaryDir, config.ourConfigFilename) configFile := config.getConfigFilename()
body, err := yaml.Marshal(diskConfig) body, err := yaml.Marshal(diskConfig)
if err != nil { if err != nil {
log.Printf("Couldn't generate YAML file: %s", err) log.Printf("Couldn't generate YAML file: %s", err)