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.exe
/AdGuardHome.yaml
/AdGuardHome.log
/data/
/build/
/dist/

View File

@ -17,6 +17,11 @@
"WarnUnmatchedDirective": true,
"EnableAll": true,
"DisableAll": false,
"Disable": [
"maligned",
"goconst"
],
"Cyclo": 20,
"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.
### 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
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_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 +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.
* `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 (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.
@ -239,6 +295,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)

406
app.go
View File

@ -3,16 +3,19 @@ package main
import (
"bufio"
"fmt"
stdlog "log"
"net"
"net/http"
"os"
"os/signal"
"path/filepath"
"runtime"
"strconv"
"syscall"
"time"
"github.com/gobuffalo/packr"
"github.com/hmage/golibs/log"
"golang.org/x/crypto/ssh/terminal"
)
@ -20,58 +23,21 @@ import (
// VersionString will be set through ldflags, contains current version
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() {
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()
if args.serviceControlAction != "" {
handleServiceControlAction(args.serviceControlAction)
return
}
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)
signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
@ -81,109 +47,29 @@ func main() {
os.Exit(0)
}()
// Save the updated config
err := config.write()
if err != nil {
log.Fatal(err)
// run the protection
run(args)
}
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)
// 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
}
err = startDHCPServer()
if err != nil {
log.Fatal(err)
}
// configure working dir and config path
initWorkingDir(args)
URL := fmt.Sprintf("http://%s", address)
log.Println("Go to " + URL)
log.Fatal(http.ListenAndServe(address, nil))
}
// configure log level and output
configureLogger(args)
func cleanup() {
err := stopDNSServer()
if err != nil {
log.Printf("Couldn't stop DNS server: %s", err)
}
}
func getInput() (string, error) {
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
text := scanner.Text()
err := scanner.Err()
return text, err
}
// 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)
log.Printf("Current working directory is %s", config.ourBinaryDir)
if args.runningAsService {
log.Printf("AdGuard Home is running as a service")
}
err := askUsernamePasswordIfPossible()
@ -204,12 +90,229 @@ 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)
}
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) {
@ -244,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

View File

@ -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,43 @@ var config = configuration{
SchemaVersion: currentSchemaVersion,
}
// 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
// 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)
}
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 {
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,11 +138,21 @@ func parseConfig() error {
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
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 {

17
dhcp.go
View File

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

3
go.mod
View File

@ -9,6 +9,8 @@ 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
github.com/shirou/gopsutil v2.18.10+incompatible
@ -16,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
)

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/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=
github.com/krolaw/dhcp4 v0.0.0-20180925202202-7cead472c414/go.mod h1:0AqAH3ZogsCrvrtUpvc6EtVKbc3w6xwZhkvGLuqyi3o=
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
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)