--- /dev/null
+//go:build openbsd
+
+package machines
+
+import (
+ "fmt"
+ "os"
+ "strconv"
+ "strings"
+ "syscall"
+ "time"
+)
+
+type pidFileService struct {
+ executable string
+ arguments []string
+ pidFile string
+ daemon bool
+}
+
+const serviceStartTimeout = 10 * time.Second
+const serviceStopTimeout = 10 * time.Second
+
+func (s *pidFileService) EnsureStarted() error {
+ status, err := s.Status()
+ if err != nil {
+ return err
+ }
+
+ if status.Running {
+ return nil
+ }
+
+ args := make([]string, len(s.arguments)+1)
+ args[0] = s.executable
+ copy(args[1:], s.arguments)
+ logger.V(2).Debugf("starting process: %+v", args)
+
+ attrs := &os.ProcAttr{
+ Dir: "/",
+ Env: []string{},
+ Files: []*os.File{
+ os.Stdin,
+ os.Stdout,
+ os.Stderr,
+ },
+ }
+
+ startDeadline := time.Now().Add(serviceStartTimeout)
+ proc, err := os.StartProcess(s.executable, args, attrs)
+ if err != nil {
+ return err
+ }
+
+ if s.daemon {
+ defer proc.Wait()
+ } else {
+ defer proc.Release()
+ }
+ for {
+ if time.Now().After(startDeadline) {
+ return fmt.Errorf("failed to start process after %v seconds: %v", serviceStartTimeout, args)
+ }
+
+ status, err := s.Status()
+ if err == nil && status.Running {
+ logger.V(2).Debugf("process started in %v", time.Now().Add(serviceStartTimeout).Sub(startDeadline))
+ return nil
+ }
+
+ time.Sleep(serviceStartTimeout / 100)
+ }
+}
+
+func (s *pidFileService) EnsureStopped() error {
+ status, err := s.Status()
+ if err != nil {
+ return err
+ }
+ if !status.Running {
+ return nil
+ }
+
+ proc, err := os.FindProcess(status.Pid)
+ if err != nil {
+ return err
+ }
+
+ signal := syscall.SIGTERM
+ logger.V(2).Infof("sending signal %v to pid %d", signal, status.Pid)
+ proc.Signal(signal)
+ stopDeadline := time.Now().Add(serviceStartTimeout)
+ for {
+ err := proc.Signal(syscall.Signal(0))
+ if err != nil {
+ logger.V(2).Infof("pid %d now appears to be dead", status.Pid)
+ proc.Wait()
+ os.Remove(s.pidFile)
+ return nil
+ }
+ if time.Now().After(stopDeadline) {
+ if signal == syscall.SIGTERM {
+ signal = syscall.SIGKILL
+ logger.V(2).Infof("sending signal %v to pid %d", signal, status.Pid)
+ proc.Signal(signal)
+ stopDeadline = time.Now().Add(serviceStartTimeout)
+ } else {
+ return fmt.Errorf("failed to kill pid %d", status.Pid)
+ }
+ }
+ time.Sleep(serviceStartTimeout / 100)
+ }
+}
+
+func (s *pidFileService) ReloadOrRestart(startIfStopped bool) error {
+ status, err := s.Status()
+ if err != nil {
+ return err
+ }
+ if !status.Running && !startIfStopped {
+ return nil
+ }
+
+ if status.Running {
+ if err := s.EnsureStopped(); err != nil {
+ return err
+ }
+ }
+
+ return s.EnsureStarted()
+}
+
+func (s *pidFileService) Status() (*ServiceStatus, error) {
+ st := &ServiceStatus{}
+
+ logger.V(2).Debugf("read pid file: %s", s.pidFile)
+ pidContents, err := os.ReadFile(s.pidFile)
+ if err != nil {
+ return st, nil
+ }
+
+ pidStr := strings.TrimSpace(string(pidContents))
+ pid, err := strconv.Atoi(pidStr)
+ if err != nil {
+ return st, nil
+ }
+ logger.V(2).Debugf("got pid: %d", pid)
+
+ proc, err := os.FindProcess(pid)
+ if err != nil {
+ return st, nil
+ }
+ defer proc.Release()
+
+ logger.V(2).Debugf("sending signal NULL to pid %d", pid)
+ err = proc.Signal(syscall.Signal(0))
+ if err == nil {
+ logger.V(2).Debugf("pid %d is alive", pid)
+ st.Running = true
+ st.Pid = pid
+ } else {
+ logger.V(2).Debugf("pid %d not alive: %v", pid, err)
+ }
+
+ return st, nil
+}
+
+func init() {
+ registerService("dhcpd4", &pidFileService{
+ executable: "/usr/local/sbin/dhcpd",
+ arguments: []string{"-q", "-pf", "/var/run/dhcpd.pid", "-cf", "/etc/dhcpd4.conf"},
+ pidFile: "/var/run/dhcpd.pid",
+ daemon: true,
+ })
+ registerService("dhcpd6", &pidFileService{
+ executable: "/usr/local/sbin/dhcpd",
+ arguments: []string{"-6", "-q", "-pf", "/var/run/dhcpd6.pid", "-cf", "/etc/dhcpd6.conf"},
+ pidFile: "/var/run/dhcpd6.pid",
+ daemon: true,
+ })
+}