// Package spawn provides utilities for daemonizing services.
package spawn

import "os"
import "fmt"
import "time"
import "errors"
import "syscall"
import "strconv"
import "path/filepath"

// Spawn spawns a process in the background and returns its PID.
func Spawn (path string, uid, gid uint32, env []string, args ...string) (pid int, err error) {
	cred := &syscall.Credential{
		Uid:         uid,
		Gid:         gid,
		Groups:      []uint32{},
		NoSetGroups: false,
	}

	// the Noctty flag is used to detach the process from parent tty
	sysproc := &syscall.SysProcAttr{
		Credential: cred,
		Noctty:     true,
	}
	attr := os.ProcAttr{
		Dir: ".",
		Env: os.Environ(),
		Files: []*os.File{
			os.Stdin,
			nil,
			nil,
		},
		Sys: sysproc,
	}

	process, err := os.StartProcess(path, args, &attr)
	if err != nil {
		return 0, err
	}

	// Release() is what actually detatches the process and places it under
	// init
	return process.Pid, process.Release()
}

// PidFile returns the path of a pidfile under the specified name. More
// specifically, it returns `/run/<name>.pid`.
func PidFile (name string) string {
	return filepath.Join("/run/", name + ".pid")
}

// PidOf returns the PID stored in the pidfile of the given name as defined by
// PidFile.
func PidOf (name string) (pid int, err error) {
	content, err := os.ReadFile(PidFile(name))
	if err != nil {
		return 0, err
	}
	pid, err = strconv.Atoi(string(content))
	if err != nil {
		return 0, err
	}
	return pid, nil
}

// Running returns whether or not a process with the given PID is running.
func Running (pid int) bool {
	directoryInfo, err := os.Stat("/proc/")
	if os.IsNotExist(err) || !directoryInfo.IsDir() {
		// if /proc/ does not exist, fallback to sending a signal
		process, err := os.FindProcess(pid)
		if err != nil {
			return false
		}
		err = process.Signal(syscall.Signal(0))
		if err != nil {
			return false
		}
	} else {
		// if /proc/ exists, see if the process's directory exists there
		_, err = os.Stat("/proc/" + strconv.Itoa(pid))
		if err != nil {
			return false
		}
	}

	return true
}

// KillAndWait kills a process and waits for it to finish, with a timeout. If
// the timeout is zero, it will wait indefinetly. This function will poll every
// 100 milliseconds to see if the process has finished.
func KillAndWait (process *os.Process, timeout time.Duration) error {
	pid := process.Pid
	err := process.Kill()
	if err != nil {
		return err
	}

	// wait for the process to exit, with a timeout
	timeoutPoint := time.Now()
	for timeout == 0 || time.Since(timeoutPoint) < 16 * time.Second {
		if !Running(pid) {
			return nil
		}
		
		time.Sleep(100 * time.Millisecond)
	}

	return errors.New(fmt.Sprintf (
		"timeout exceeded while waiting for process %d to finish", pid))
}
