*(global): fixed service implementation for OpenWrt

We now use a procd init script for OpenWrt just like it's recommended in
the documentation. The service is automatically enabled on the install
command.

 Closes: https://github.com/AdguardTeam/AdGuardHome/issues/1386
This commit is contained in:
Andrey Meshkov 2020-02-05 17:38:23 +03:00
parent 54c285001d
commit fc88f59f61
3 changed files with 180 additions and 95 deletions

View File

@ -152,15 +152,6 @@ type updateInfo struct {
newBinName string // Full path to the new executable file newBinName string // Full path to the new executable file
} }
// Return TRUE if file exists
func fileExists(fn string) bool {
_, err := os.Stat(fn)
if err != nil {
return false
}
return true
}
// Fill in updateInfo object // Fill in updateInfo object
func getUpdateInfo(jsonData []byte) (*updateInfo, error) { func getUpdateInfo(jsonData []byte) (*updateInfo, error) {
var u updateInfo var u updateInfo

View File

@ -8,6 +8,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"os/exec"
"path" "path"
"path/filepath" "path/filepath"
"runtime" "runtime"
@ -242,7 +243,7 @@ func checkPortAvailable(host string, port int) error {
if err != nil { if err != nil {
return err return err
} }
ln.Close() _ = ln.Close()
// It seems that net.Listener.Close() doesn't close file descriptors right away. // It seems that net.Listener.Close() doesn't close file descriptors right away.
// We wait for some time and hope that this fd will be closed. // We wait for some time and hope that this fd will be closed.
@ -255,7 +256,7 @@ func checkPacketPortAvailable(host string, port int) error {
if err != nil { if err != nil {
return err return err
} }
ln.Close() _ = ln.Close()
// It seems that net.Listener.Close() doesn't close file descriptors right away. // It seems that net.Listener.Close() doesn't close file descriptors right away.
// We wait for some time and hope that this fd will be closed. // We wait for some time and hope that this fd will be closed.
@ -329,6 +330,30 @@ func errorIsAddrInUse(err error) bool {
return errErrno == syscall.EADDRINUSE return errErrno == syscall.EADDRINUSE
} }
// ---------------------
// general helpers
// ---------------------
// fileExists returns TRUE if file exists
func fileExists(fn string) bool {
_, err := os.Stat(fn)
if err != nil {
return false
}
return true
}
// runCommand runs shell command
func runCommand(command string, arguments ...string) (int, string, error) {
cmd := exec.Command(command, arguments...)
out, err := cmd.Output()
if err != nil {
return 1, "", fmt.Errorf("exec.Command(%s) failed: %s", command, err)
}
return cmd.ProcessState.ExitCode(), string(out), nil
}
// --------------------- // ---------------------
// debug logging helpers // debug logging helpers
// --------------------- // ---------------------

View File

@ -1,10 +1,10 @@
package home package home
import ( import (
"fmt" "io/ioutil"
"os" "os"
"os/exec"
"runtime" "runtime"
"strings"
"syscall" "syscall"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
@ -41,23 +41,12 @@ func (p *program) Stop(s service.Service) error {
return nil return nil
} }
func runCommand(command string, arguments ...string) (int, string, error) {
cmd := exec.Command(command, arguments...)
out, err := cmd.Output()
if err != nil {
return 1, "", fmt.Errorf("exec.Command(%s) failed: %s", command, err)
}
return cmd.ProcessState.ExitCode(), string(out), nil
}
// Check the service's status // Check the service's status
// Note: on OpenWrt 'service' utility may not exist - we use our service script directly in this case. // Note: on OpenWrt 'service' utility may not exist - we use our service script directly in this case.
func svcStatus(s service.Service) (service.Status, error) { func svcStatus(s service.Service) (service.Status, error) {
status, err := s.Status() status, err := s.Status()
if err != nil && service.Platform() == "unix-systemv" { if err != nil && service.Platform() == "unix-systemv" {
confPath := "/etc/init.d/" + serviceName code, err := runInitdCommand("status")
code, _, err := runCommand("sh", "-c", confPath+" status")
if err != nil { if err != nil {
return service.StatusStopped, nil return service.StatusStopped, nil
} }
@ -75,8 +64,7 @@ func svcAction(s service.Service, action string) error {
err := service.Control(s, action) err := service.Control(s, action)
if err != nil && service.Platform() == "unix-systemv" && if err != nil && service.Platform() == "unix-systemv" &&
(action == "start" || action == "stop" || action == "restart") { (action == "start" || action == "stop" || action == "restart") {
confPath := "/etc/init.d/" + serviceName _, err := runInitdCommand(action)
_, _, err := runCommand("sh", "-c", confPath+" "+action)
return err return err
} }
return err return err
@ -114,61 +102,100 @@ func handleServiceControlAction(action string) {
} }
if action == "status" { if action == "status" {
status, errSt := svcStatus(s) handleServiceStatusCommand(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")
}
} else if action == "run" { } else if action == "run" {
err = s.Run() err = s.Run()
if err != nil { if err != nil {
log.Fatalf("Failed to run service: %s", err) log.Fatalf("Failed to run service: %s", err)
} }
} else if action == "install" {
handleServiceInstallCommand(s)
} else if action == "uninstall" {
handleServiceUninstallCommand(s)
} else { } 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
_ = svcAction(s, "stop")
}
err = svcAction(s, action) err = svcAction(s, action)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
log.Printf("Action %s has been done successfully on %s", action, service.ChosenSystem().String()) }
if action == "install" { log.Printf("Action %s has been done successfully on %s", action, service.ChosenSystem().String())
err := afterInstall() }
if err != nil {
log.Fatal(err)
}
// Start automatically after install // handleServiceStatusCommand handles service "status" command
err = svcAction(s, "start") func handleServiceStatusCommand(s service.Service) {
if err != nil { status, errSt := svcStatus(s)
log.Fatalf("Failed to start the service: %s", err) if errSt != nil {
} log.Fatalf("failed to get service status: %s", errSt)
log.Printf("Service has been started") }
if detectFirstRun() { switch status {
log.Printf(`Almost ready! 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 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. 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. 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.`) Click on the link below and follow the Installation Wizard steps to finish setup.`)
printHTTPAddresses("http") printHTTPAddresses("http")
} }
}
} else if action == "uninstall" { // handleServiceStatusCommand handles service "uninstall" command
cleanupService() func handleServiceUninstallCommand(s service.Service) {
if 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" {
// 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)
} }
} }
} }
@ -191,43 +218,33 @@ func configureService(c *service.Config) {
// Use modified service file templates // Use modified service file templates
c.Option["SystemdScript"] = systemdScript c.Option["SystemdScript"] = systemdScript
c.Option["SysvScript"] = sysvScript c.Option["SysvScript"] = sysvScript
// On OpenWrt we're using a different type of sysvScript
if isOpenWrt() {
c.Option["SysvScript"] = openWrtScript
}
} }
// On SysV systems supported by kardianos/service package, there must be multiple /etc/rc{N}.d directories. // runInitdCommand runs init.d service command
// On OpenWrt, however, there is only /etc/rc.d - we handle this case ourselves. // returns command code or error if any
// We also use relative path, because this is how all other service files are set up. func runInitdCommand(action string) (int, error) {
func afterInstall() error { confPath := "/etc/init.d/" + serviceName
if service.Platform() == "unix-systemv" && fileExists("/etc/rc.d") { code, _, err := runCommand("sh", "-c", confPath+" "+action)
confPath := "../init.d/" + serviceName return code, err
err := os.Symlink(confPath, "/etc/rc.d/S99"+serviceName)
if err != nil {
return err
}
}
return nil
} }
// cleanupService called on the service uninstall, cleans up additional files if needed // isOpenWrt checks if OS is OpenWRT
func cleanupService() { func isOpenWrt() bool {
if runtime.GOOS == "darwin" { if runtime.GOOS != "linux" {
// Removing log files on cleanup and ignore errors return false
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)
}
} }
if service.Platform() == "unix-systemv" { body, err := ioutil.ReadFile("/etc/os-release")
fn := "/etc/rc.d/S99" + serviceName if err != nil {
err := os.Remove(fn) return false
if err != nil && !os.IsNotExist(err) {
log.Printf("os.Remove: %s: %s", fn, err)
}
} }
return strings.Contains(string(body), "OpenWrt")
} }
// Basically the same template as the one defined in github.com/kardianos/service // Basically the same template as the one defined in github.com/kardianos/service
@ -388,3 +405,55 @@ case "$1" in
esac esac
exit 0 exit 0
` `
// OpenWrt procd init script
// https://github.com/AdguardTeam/AdGuardHome/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
}
`