package home import ( "fmt" "os" "os/exec" "runtime" "syscall" "github.com/AdguardTeam/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. if config.appSignalChannel == nil { os.Exit(0) } config.appSignalChannel <- syscall.SIGINT 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 // 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) { status, err := s.Status() if err != nil && service.Platform() == "unix-systemv" { confPath := "/etc/init.d/" + serviceName code, _, err := runCommand("sh", "-c", confPath+" status") if err != nil { return service.StatusStopped, nil } if code != 0 { return service.StatusStopped, nil } return service.StatusRunning, nil } return status, err } // Perform an action on the service // Note: on OpenWrt 'service' utility may not exist - we use our service script directly in this case. func svcAction(s service.Service, action string) error { err := service.Control(s, action) if err != nil && service.Platform() == "unix-systemv" && (action == "start" || action == "stop" || action == "restart") { confPath := "/etc/init.d/" + serviceName _, _, err := runCommand("sh", "-c", confPath+" "+action) return err } return err } // 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 := 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") } } 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 _ = svcAction(s, "stop") } err = svcAction(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" { err := afterInstall() 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") } } 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 // Use modified service file templates c.Option["SystemdScript"] = systemdScript c.Option["SysvScript"] = sysvScript } // On SysV systems supported by kardianos/service package, there must be multiple /etc/rc{N}.d directories. // On OpenWrt, however, there is only /etc/rc.d - we handle this case ourselves. // We also use relative path, because this is how all other service files are set up. func afterInstall() error { if service.Platform() == "unix-systemv" && fileExists("/etc/rc.d") { confPath := "../init.d/" + serviceName 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 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) } } if service.Platform() == "unix-systemv" { fn := "/etc/rc.d/S99" + serviceName err := os.Remove(fn) if err != nil && !os.IsNotExist(err) { log.Printf("os.Remove: %s: %s", fn, err) } } } // Basically the same template as the one defined in github.com/kardianos/service // but with two additional keys - StandardOutPath and StandardErrorPath var launchdConfig = ` Label{{html .Name}} ProgramArguments {{html .Path}} {{range .Config.Arguments}} {{html .}} {{end}} {{if .UserName}}UserName{{html .UserName}}{{end}} {{if .ChRoot}}RootDirectory{{html .ChRoot}}{{end}} {{if .WorkingDirectory}}WorkingDirectory{{html .WorkingDirectory}}{{end}} SessionCreate<{{bool .SessionCreate}}/> KeepAlive<{{bool .KeepAlive}}/> RunAtLoad<{{bool .RunAtLoad}}/> Disabled StandardOutPath ` + launchdStdoutPath + ` StandardErrorPath ` + launchdStderrPath + ` ` // 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 `