Fixed review comments

Fixed running as a windows service
Added logging to windows evenlog
This commit is contained in:
Andrey Meshkov 2019-02-05 14:09:05 +03:00
parent a4dc4c61d8
commit 448a6caeb8
8 changed files with 252 additions and 52 deletions

View File

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

View File

@ -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.

111
app.go
View File

@ -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

2
go.mod
View File

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

2
go.sum
View File

@ -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=

View File

@ -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 = `<?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, "AdGuard Home")
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"
)
// 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
}