AdGuardHome/internal/home/service.go
Ainar Garipov 7e64205d44 Pull request: home: rm var shadowing, vol. 4
Closes #2803.

Squashed commit of the following:

commit cb36cc8811160bb39a32fb8eddf962d0ebe9035a
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Mar 12 14:21:46 2021 +0300

    home: imp more

commit 9ea7ccec8bb293881cf724d7ad57e6744243d8b9
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Mar 12 13:58:10 2021 +0300

    all: imp naming, refactor http srv shutdown

commit f29221007c16fd3e7230bf2c1ac37b365f3e29aa
Merge: 2247c05b bfbf73f3
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Mar 12 13:35:17 2021 +0300

    Merge branch 'master' into 2803-shadow-4

commit 2247c05b5521346aaf362d81ccdd64fee31f1e6d
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Jan 29 20:53:21 2021 +0300

    home: rm var shadowing, vol. 4
2021-03-12 14:32:08 +03:00

536 lines
14 KiB
Go

package home
import (
"fmt"
"io/ioutil"
"os"
"runtime"
"strconv"
"strings"
"syscall"
"github.com/AdguardTeam/AdGuardHome/internal/sysutil"
"github.com/AdguardTeam/AdGuardHome/internal/util"
"github.com/AdguardTeam/golibs/log"
"github.com/kardianos/service"
)
// TODO(a.garipov): Move shell templates into actual files. Either during the
// v0.106.0 cycle using packr or during the following cycle using go:embed.
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 {
opts options
}
// Start should quickly start the program
func (p *program) Start(s service.Service) error {
// Start should not block. Do the actual work async.
args := p.opts
args.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.
if Context.appSignalChannel == nil {
os.Exit(0)
}
Context.appSignalChannel <- syscall.SIGINT
return nil
}
// svcStatus check the service's status.
//
// On OpenWrt, the service utility may not exist. We use our service script
// directly in this case.
func svcStatus(s service.Service) (status service.Status, err error) {
status, err = s.Status()
if err != nil && service.Platform() == "unix-systemv" {
var code int
code, err = runInitdCommand("status")
if err != nil || code != 0 {
return service.StatusStopped, nil
}
return service.StatusRunning, nil
}
return status, err
}
// svcAction performs the action on the service.
//
// On OpenWrt, the service utility may not exist. We use our service script
// directly in this case.
func svcAction(s service.Service, action string) (err error) {
err = service.Control(s, action)
if err != nil && service.Platform() == "unix-systemv" &&
(action == "start" || action == "stop" || action == "restart") {
_, err = runInitdCommand(action)
return err
}
return err
}
// Send SIGHUP to a process with ID taken from our pid-file
// If pid-file doesn't exist, find our PID using 'ps' command
func sendSigReload() {
if runtime.GOOS == "windows" {
log.Error("Not implemented on Windows")
return
}
pidfile := fmt.Sprintf("/var/run/%s.pid", serviceName)
data, err := ioutil.ReadFile(pidfile)
if os.IsNotExist(err) {
var code int
var psdata string
code, psdata, err = util.RunCommand("ps", "-C", serviceName, "-o", "pid=")
if err != nil || code != 0 {
log.Error("Can't find AdGuardHome process: %s code:%d", err, code)
return
}
data = []byte(psdata)
} else if err != nil {
log.Error("Can't read PID file %s: %s", pidfile, err)
return
}
parts := strings.SplitN(string(data), "\n", 2)
if len(parts) == 0 {
log.Error("Can't read PID file %s: bad value", pidfile)
return
}
pid, err := strconv.Atoi(strings.TrimSpace(parts[0]))
if err != nil {
log.Error("Can't read PID file %s: %s", pidfile, err)
return
}
err = sysutil.SendProcessSignal(pid, syscall.SIGHUP)
if err != nil {
log.Error("Can't send signal to PID %d: %s", pid, err)
return
}
log.Debug("Sent signal to PID %d", pid)
}
// 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(opts options) {
action := opts.serviceControlAction
log.Printf("Service control action: %s", action)
if action == "reload" {
sendSigReload()
return
}
pwd, err := os.Getwd()
if err != nil {
log.Fatal("Unable to find the path to the current directory")
}
runOpts := opts
runOpts.serviceControlAction = "run"
svcConfig := &service.Config{
Name: serviceName,
DisplayName: serviceDisplayName,
Description: serviceDescription,
WorkingDirectory: pwd,
Arguments: serialize(runOpts),
}
configureService(svcConfig)
prg := &program{runOpts}
s, err := service.New(prg, svcConfig)
if err != nil {
log.Fatal(err)
}
if action == "status" {
handleServiceStatusCommand(s)
} else if action == "run" {
err = s.Run()
if err != nil {
log.Fatalf("Failed to run service: %s", err)
}
} else if action == "install" {
initConfigFilename(opts)
initWorkingDir(opts)
handleServiceInstallCommand(s)
} else if action == "uninstall" {
handleServiceUninstallCommand(s)
} else {
err = svcAction(s, action)
if err != nil {
log.Fatal(err)
}
}
log.Printf("Action %s has been done successfully on %s", action, service.ChosenSystem().String())
}
// handleServiceStatusCommand handles service "status" command
func handleServiceStatusCommand(s service.Service) {
status, errSt := svcStatus(s)
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")
}
}
// handleServiceStatusCommand handles service "install" command
func handleServiceInstallCommand(s service.Service) {
err := svcAction(s, "install")
if err != nil {
log.Fatal(err)
}
if util.IsOpenWrt() {
// On OpenWrt it is important to run enable after the service installation
// Otherwise, the service won't start on the system startup
_, err = runInitdCommand("enable")
if err != nil {
log.Fatal(err)
}
}
// Start automatically after install
err = svcAction(s, "start")
if err != nil {
log.Fatalf("Failed to start the service: %s", err)
}
log.Printf("Service has been started")
if detectFirstRun() {
log.Printf(`Almost ready!
AdGuard Home is successfully installed and will automatically start on boot.
There are a few more things that must be configured before you can use it.
Click on the link below and follow the Installation Wizard steps to finish setup.`)
printHTTPAddresses("http")
}
}
// handleServiceStatusCommand handles service "uninstall" command
func handleServiceUninstallCommand(s service.Service) {
if util.IsOpenWrt() {
// On OpenWrt it is important to run disable command first
// as it will remove the symlink
_, err := runInitdCommand("disable")
if err != nil {
log.Fatal(err)
}
}
err := svcAction(s, "uninstall")
if err != nil {
log.Fatal(err)
}
if runtime.GOOS == "darwin" {
// Remove log files on cleanup and log errors.
err = os.Remove(launchdStdoutPath)
if err != nil && !os.IsNotExist(err) {
log.Printf("removing stdout file: %s", err)
}
err = os.Remove(launchdStderrPath)
if err != nil && !os.IsNotExist(err) {
log.Printf("removing stderr file: %s", err)
}
}
}
// 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
// Use modified service file templates.
c.Option["SystemdScript"] = systemdScript
c.Option["SysvScript"] = sysvScript
// On OpenWrt we're using a different type of sysvScript.
if util.IsOpenWrt() {
c.Option["SysvScript"] = openWrtScript
} else if runtime.GOOS == "freebsd" {
c.Option["SysvScript"] = freeBSDScript
}
}
// runInitdCommand runs init.d service command
// returns command code or error if any
func runInitdCommand(action string) (int, error) {
confPath := "/etc/init.d/" + serviceName
code, _, err := util.RunCommand("sh", "-c", confPath+" "+action)
return code, err
}
// 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>
`
// Note: we should keep it in sync with the template from service_systemd_linux.go file
// Add "After=" setting for systemd service file, because we must be started only after network is online
// Set "RestartSec" to 10
const systemdScript = `[Unit]
Description={{.Description}}
ConditionFileIsExecutable={{.Path|cmdEscape}}
After=syslog.target network-online.target
[Service]
StartLimitInterval=5
StartLimitBurst=10
ExecStart={{.Path|cmdEscape}}{{range .Arguments}} {{.|cmd}}{{end}}
{{if .ChRoot}}RootDirectory={{.ChRoot|cmd}}{{end}}
{{if .WorkingDirectory}}WorkingDirectory={{.WorkingDirectory|cmdEscape}}{{end}}
{{if .UserName}}User={{.UserName}}{{end}}
{{if .ReloadSignal}}ExecReload=/bin/kill -{{.ReloadSignal}} "$MAINPID"{{end}}
{{if .PIDFile}}PIDFile={{.PIDFile|cmd}}{{end}}
{{if and .LogOutput .HasOutputFileSupport -}}
StandardOutput=file:/var/log/{{.Name}}.out
StandardError=file:/var/log/{{.Name}}.err
{{- end}}
Restart=always
RestartSec=10
EnvironmentFile=-/etc/sysconfig/{{.Name}}
[Install]
WantedBy=multi-user.target
`
// Note: we should keep it in sync with the template from service_sysv_linux.go file
// Use "ps | grep -v grep | grep $(get_pid)" because "ps PID" may not work on OpenWrt
const sysvScript = `#!/bin/sh
# For RedHat and cousins:
# chkconfig: - 99 01
# description: {{.Description}}
# processname: {{.Path}}
### BEGIN INIT INFO
# Provides: {{.Path}}
# Required-Start:
# Required-Stop:
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: {{.DisplayName}}
# Description: {{.Description}}
### END INIT INFO
cmd="{{.Path}}{{range .Arguments}} {{.|cmd}}{{end}}"
name=$(basename $(readlink -f $0))
pid_file="/var/run/$name.pid"
stdout_log="/var/log/$name.log"
stderr_log="/var/log/$name.err"
[ -e /etc/sysconfig/$name ] && . /etc/sysconfig/$name
get_pid() {
cat "$pid_file"
}
is_running() {
[ -f "$pid_file" ] && ps | grep -v grep | grep $(get_pid) > /dev/null 2>&1
}
case "$1" in
start)
if is_running; then
echo "Already started"
else
echo "Starting $name"
{{if .WorkingDirectory}}cd '{{.WorkingDirectory}}'{{end}}
$cmd >> "$stdout_log" 2>> "$stderr_log" &
echo $! > "$pid_file"
if ! is_running; then
echo "Unable to start, see $stdout_log and $stderr_log"
exit 1
fi
fi
;;
stop)
if is_running; then
echo -n "Stopping $name.."
kill $(get_pid)
for i in $(seq 1 10)
do
if ! is_running; then
break
fi
echo -n "."
sleep 1
done
echo
if is_running; then
echo "Not stopped; may still be shutting down or shutdown may have failed"
exit 1
else
echo "Stopped"
if [ -f "$pid_file" ]; then
rm "$pid_file"
fi
fi
else
echo "Not running"
fi
;;
restart)
$0 stop
if is_running; then
echo "Unable to stop, will not attempt to start"
exit 1
fi
$0 start
;;
status)
if is_running; then
echo "Running"
else
echo "Stopped"
exit 1
fi
;;
*)
echo "Usage: $0 {start|stop|restart|status}"
exit 1
;;
esac
exit 0
`
// OpenWrt procd init script
// https://github.com/AdguardTeam/AdGuardHome/internal/issues/1386
const openWrtScript = `#!/bin/sh /etc/rc.common
USE_PROCD=1
START=95
STOP=01
cmd="{{.Path}}{{range .Arguments}} {{.|cmd}}{{end}}"
name="{{.Name}}"
pid_file="/var/run/${name}.pid"
start_service() {
echo "Starting ${name}"
procd_open_instance
procd_set_param command ${cmd}
procd_set_param respawn # respawn automatically if something died
procd_set_param stdout 1 # forward stdout of the command to logd
procd_set_param stderr 1 # same for stderr
procd_set_param pidfile ${pid_file} # write a pid file on instance start and remove it on stop
procd_close_instance
echo "${name} has been started"
}
stop_service() {
echo "Stopping ${name}"
}
EXTRA_COMMANDS="status"
EXTRA_HELP=" status Print the service status"
get_pid() {
cat "${pid_file}"
}
is_running() {
[ -f "${pid_file}" ] && ps | grep -v grep | grep $(get_pid) >/dev/null 2>&1
}
status() {
if is_running; then
echo "Running"
else
echo "Stopped"
exit 1
fi
}
`
// TODO(a.garipov): Don't use .WorkingDirectory here. There are currently no
// guarantees that it will actually be the required directory.
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/2614.
const freeBSDScript = `#!/bin/sh
# PROVIDE: {{.Name}}
# REQUIRE: networking
# KEYWORD: shutdown
. /etc/rc.subr
name="{{.Name}}"
{{.Name}}_env="IS_DAEMON=1"
{{.Name}}_user="root"
pidfile="/var/run/${name}.pid"
command="/usr/sbin/daemon"
command_args="-P ${pidfile} -f -r {{.WorkingDirectory}}/{{.Name}}"
run_rc_command "$1"
`