kardianos / service

Run go programs as a service on major platforms.
zlib License
4.42k stars 673 forks source link

OpenBSD doesn't call Stop() on SIGTERM #139

Open publicarray opened 6 years ago

publicarray commented 6 years ago

This code or it's equivalent is not present in all platforms. https://github.com/kardianos/service/blob/7a88211485df5d3da6ec68f4f8a5ff9bec297327/service_darwin.go#L199-L206

Background: https://github.com/jedisct1/dnscrypt-proxy/pull/559

SteelPhase commented 6 years ago

@jedisct1 can you build and run this file for me on your OpenBSD box?

if it returns anything other than 0 let me know.

package main

import "github.com/kardianos/service"

func main() {
    println(len(service.AvailableSystems()))
}
SteelPhase commented 6 years ago

I've started an initial Service and System for OpenBSD RC service_openbsd.go It's still going to need some work to know it works, but things should be similar enough to sysv to use it as a starting point.

// Copyright 2015 Daniel Theophanes.
// Use of this source code is governed by a zlib-style
// license that can be found in the LICENSE file.

package service

import (
        "errors"
        "fmt"
        "os"
        "os/signal"
        "strings"
        "syscall"
        "text/template"
        "time"
)

const version = "openbsd-runcom"

var errNoUserServiceRunCom = errors.New("user services are not supported on RunCom")

var tf = map[string]interface{}{
        "cmd": func(s string) string {
                return `"` + strings.Replace(s, `"`, `\"`, -1) + `"`
        },
        "cmdEscape": func(s string) string {
                return strings.Replace(s, " ", `\x20`, -1)
        },
}

func init() {
        ChooseSystem(openbsdSystem{})
}

type openbsdSystem struct{}

type openbsdRunComService struct {
        i Interface
        *Config
}

func (openbsdSystem) String() string {
        return version
}

func (openbsdSystem) Detect() bool {
        return true
}

func (openbsdSystem) Interactive() bool {
        return os.Getppid() != 1
}

func (openbsdSystem) New(i Interface, c *Config) (Service, error) {
        return &openbsdRunComService{
                i:      i,
                Config: c,
        }, nil
}

func (s *openbsdRunComService) String() string {
        if len(s.DisplayName) > 0 {
                return s.DisplayName
        }
        return s.Name
}

func (s *openbsdRunComService) configPath() (cp string, err error) {
        if s.Option.bool(optionUserService, optionUserServiceDefault) {
                err = errNoUserServiceRunCom
                return
        }
        cp = "/etc/rc.d/" + s.Config.Name
        return
}

func (s *openbsdRunComService) template() *template.Template {
        customScript := s.Option.string(optionRunComScript, "")

        if customScript != "" {
                return template.Must(template.New("").Funcs(tf).Parse(customScript))
        }

        return template.Must(template.New("").Funcs(tf).Parse(runComScript))
}

func (s *openbsdRunComService) Install() error {
        confPath, err := s.configPath()
        if err != nil {
                return err
        }
        _, err = os.Stat(confPath)
        if err == nil {
                return fmt.Errorf("script already exists: %s", confPath)
        }

        f, err := os.Create(confPath)
        if err != nil {
                return err
        }
        defer f.Close()

        path, err := s.execPath()
        if err != nil {
                return err
        }

        var to = &struct {
                *Config
                Path string
        }{
                s.Config,
                path,
        }

        err = s.template().Execute(f, to)
        if err != nil {
                return err
        }

        if err = os.Chmod(confPath, 0755); err != nil {
                return err
        }

        // TODO: Is there more to do???

        return nil
}

func (s *openbsdRunComService) Uninstall() error {
        cp, err := s.configPath()
        if err != nil {
                return err
        }
        if err := os.Remove(cp); err != nil {
                return err
        }
        return nil
}

func (s *openbsdRunComService) Logger(errs chan<- error) (Logger, error) {
        if system.Interactive() {
                return ConsoleLogger, nil
        }
        return s.SystemLogger(errs)
}
func (s *openbsdRunComService) SystemLogger(errs chan<- error) (Logger, error) {
        return newSysLogger(s.Name, errs)
}

func (s *openbsdRunComService) Run() (err error) {
        err = s.i.Start(s)
        if err != nil {
                return err
        }

        s.Option.funcSingle(optionRunWait, func() {
                var sigChan = make(chan os.Signal, 3)
                signal.Notify(sigChan, syscall.SIGTERM, os.Interrupt)
                <-sigChan
        })()

        return s.i.Stop(s)
}

func (s *openbsdRunComService) Status() (Status, error) {
        cp, err := s.configPath()
        if err != nil {
                return StatusUnknown, err
        }

        _, out, err := runWithOutput(cp, "status")
        if err != nil {
                return StatusUnknown, err
        }

        switch {
        case strings.HasPrefix(out, "Running"):
                return StatusRunning, nil
        case strings.HasPrefix(out, "Stopped"):
                return StatusStopped, nil
        default:
                return StatusUnknown, ErrNotInstalled
        }
}

func (s *openbsdRunComService) Start() error {
        cp, err := s.configPath()
        if err != nil {
                return err
        }

        return run(cp, "start")
}

func (s *openbsdRunComService) Stop() error {
        cp, err := s.configPath()
        if err != nil {
                return err
        }

        return run(cp, "stop")
}

func (s *openbsdRunComService) Restart() error {
        err := s.Stop()
        if err != nil {
                return err
        }
        time.Sleep(50 * time.Millisecond)
        return s.Start()
}

const runComScript = `#!/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 $(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
`
kardianos commented 6 years ago

This looks reasonable to me. At the moment I'll try to do sanity checks, but my actual knowledge or experience of OpenBSD is zero. I'll largely trust your judgement here.

jedisct1 commented 6 years ago

@SteelPhase It does return 0 :)