htmgo/cli/tasks/process/process.go

194 lines
3.3 KiB
Go

package process
import (
"errors"
"fmt"
"log/slog"
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
"syscall"
"time"
)
var workingDir string
var commands = make([]*exec.Cmd, 0)
func AppendRunning(cmd *exec.Cmd) {
slog.Debug("running", slog.String("command", strings.Join(cmd.Args, " ")))
commands = append(commands, cmd)
}
func GetWorkingDir() string {
if workingDir == "" {
wd, _ := os.Getwd()
return wd
}
return workingDir
}
func SetWorkingDir(dir string) {
workingDir = dir
}
func GetPathRelativeToCwd(path string) string {
return filepath.Join(GetWorkingDir(), path)
}
func KillAll() {
tries := 0
for {
tries++
allFinished := true
for _, cmd := range commands {
if cmd.Process == nil {
allFinished = false
if tries > 50 {
args := strings.Join(cmd.Args, " ")
slog.Debug("process %v is not running after 50 tries, breaking.\n", args)
allFinished = true
break
} else {
time.Sleep(time.Millisecond * 50)
continue
}
}
}
if allFinished {
break
}
}
for _, command := range commands {
pid := command.Process.Pid
err := syscall.Kill(-pid, syscall.SIGKILL)
if err != nil {
continue
}
}
for {
finished := true
for _, c := range commands {
if c.Process == nil {
continue
}
exists, err := PidExists(int32(c.Process.Pid))
if err != nil {
finished = false
}
if exists {
syscall.Kill(-c.Process.Pid, syscall.SIGKILL)
finished = false
}
}
if finished {
break
} else {
slog.Debug("waiting for all processes to exit\n")
time.Sleep(time.Millisecond * 5)
}
}
commands = make([]*exec.Cmd, 0)
slog.Debug("all processes killed\n")
}
func PidExists(pid int32) (bool, error) {
if pid <= 0 {
return false, fmt.Errorf("invalid pid %v", pid)
}
proc, err := os.FindProcess(int(pid))
if err != nil {
return false, err
}
err = proc.Signal(syscall.Signal(0))
if err == nil {
return true, nil
}
if err.Error() == "os: process already finished" {
return false, nil
}
var errno syscall.Errno
ok := errors.As(err, &errno)
if !ok {
return false, err
}
switch errno {
case syscall.ESRCH:
return false, nil
case syscall.EPERM:
return true, nil
}
return false, err
}
func RunOrExit(command string) {
_ = Run(command, ExitOnError)
}
type RunFlag int
const (
ExitOnError RunFlag = iota
Silent
DontTrack
)
func RunMany(commands []string, flags ...RunFlag) error {
for _, command := range commands {
err := Run(command, flags...)
if err != nil {
if slices.Contains(flags, ExitOnError) {
os.Exit(1)
}
return err
}
}
return nil
}
func Run(command string, flags ...RunFlag) error {
parts := strings.Fields(command)
cmd := exec.Command(parts[0], parts[1:]...)
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
if slices.Contains(flags, Silent) {
cmd.Stdout = nil
cmd.Stderr = nil
} else {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
}
if workingDir != "" {
cmd.Dir = workingDir
}
if !slices.Contains(flags, DontTrack) {
AppendRunning(cmd)
}
err := cmd.Run()
if err == nil {
return nil
}
if strings.Contains(err.Error(), "signal: killed") {
return nil
}
if slices.Contains(flags, ExitOnError) {
slog.Error("Error running command: ",
slog.String("error", err.Error()),
slog.String("command", command))
os.Exit(1)
}
return err
}