hnakra/cmd/hnctl/spawn/spawn.go
2023-05-31 15:52:33 -04:00

133 lines
3.1 KiB
Go

// Package spawn provides utilities for daemonizing services.
package spawn
import "os"
import "fmt"
import "time"
import "errors"
import "syscall"
import "os/user"
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()
}
// LookupUID returns the uid and gid of the given username, if it exists.
func LookupUID (name string) (uid, gid uint32, err error) {
user, err := user.Lookup(name)
if err != nil {
return 0, 0, err
}
puid, err := strconv.Atoi(user.Uid)
if err != nil {
return 0, 0, err
}
pgid, err := strconv.Atoi(user.Gid)
if err != nil {
return 0, 0, err
}
return uint32(puid), uint32(pgid), nil
}
// 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))
}