2022-06-19 01:52:04 +00:00
|
|
|
package pmuxlib
|
2022-01-23 03:03:13 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"context"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"os"
|
|
|
|
"os/exec"
|
|
|
|
"strings"
|
|
|
|
"sync"
|
2022-06-30 19:31:32 +00:00
|
|
|
"syscall"
|
2022-01-23 03:03:13 +00:00
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
2022-06-19 01:52:04 +00:00
|
|
|
// ProcessConfig is used to configure a process via RunProcess.
|
|
|
|
type ProcessConfig struct {
|
|
|
|
|
|
|
|
// Name of the process to be run. This only gets used by RunPmux.
|
|
|
|
Name string
|
2022-01-23 03:03:13 +00:00
|
|
|
|
|
|
|
// Cmd and Args describe the actual process to run.
|
|
|
|
Cmd string `yaml:"cmd"`
|
|
|
|
Args []string `yaml:"args"`
|
|
|
|
|
|
|
|
// Env describes the environment variables to set on the process.
|
|
|
|
Env map[string]string `yaml:"env"`
|
|
|
|
|
2022-01-23 03:39:34 +00:00
|
|
|
// Dir is the directory the process will be run in. If not set then the
|
|
|
|
// process is run in the same directory as this parent process.
|
2022-02-25 03:48:37 +00:00
|
|
|
Dir string `yaml:"dir"`
|
2022-01-23 03:39:34 +00:00
|
|
|
|
2022-01-23 03:03:13 +00:00
|
|
|
// MinWait and MaxWait are the minimum and maximum amount of time between
|
|
|
|
// restarts that RunProcess will wait.
|
|
|
|
//
|
|
|
|
// MinWait defaults to 1 second.
|
|
|
|
// MaxWait defaults to 64 seconds.
|
|
|
|
MinWait time.Duration `yaml:"minWait"`
|
|
|
|
MaxWait time.Duration `yaml:"maxWait"`
|
|
|
|
|
|
|
|
// SigKillWait is the amount of time after the process is sent a SIGINT
|
|
|
|
// before RunProcess sends it a SIGKILL.
|
|
|
|
//
|
|
|
|
// Defalts to 10 seconds.
|
|
|
|
SigKillWait time.Duration `yaml:"sigKillWait"`
|
2022-02-25 03:48:37 +00:00
|
|
|
|
|
|
|
// NoRestartOn indicates which exit codes should result in the process not
|
|
|
|
// being restarted any further.
|
2022-02-27 20:26:18 +00:00
|
|
|
NoRestartOn []int `yaml:"noRestartOn"`
|
2022-01-23 03:03:13 +00:00
|
|
|
}
|
|
|
|
|
2022-06-19 01:52:04 +00:00
|
|
|
func (cfg ProcessConfig) withDefaults() ProcessConfig {
|
2022-01-23 03:03:13 +00:00
|
|
|
|
|
|
|
if cfg.MinWait == 0 {
|
|
|
|
cfg.MinWait = 1 * time.Second
|
|
|
|
}
|
|
|
|
|
|
|
|
if cfg.MaxWait == 0 {
|
|
|
|
cfg.MaxWait = 64 * time.Second
|
|
|
|
}
|
|
|
|
|
|
|
|
if cfg.SigKillWait == 0 {
|
|
|
|
cfg.SigKillWait = 10 * time.Second
|
|
|
|
}
|
|
|
|
|
|
|
|
return cfg
|
|
|
|
}
|
|
|
|
|
2022-06-30 19:31:32 +00:00
|
|
|
func sigProcessGroup(sysLogger Logger, proc *os.Process, sig syscall.Signal) {
|
|
|
|
sysLogger.Printf("sending %v signal", sig)
|
|
|
|
|
|
|
|
// Because we use Setpgid when starting child processes, child processes
|
|
|
|
// will have the same PGID as their PID. To send a signal to all processes
|
|
|
|
// in a group, you send the signal to the negation of the PGID, which in
|
|
|
|
// this case is equivalent to -PID.
|
|
|
|
//
|
|
|
|
// POSIX is a fucking joke.
|
|
|
|
if err := syscall.Kill(-proc.Pid, sig); err != nil {
|
|
|
|
|
|
|
|
panic(fmt.Errorf(
|
|
|
|
"failed to send %v signal to %d: %w",
|
|
|
|
sig, -proc.Pid, err,
|
|
|
|
))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-19 01:52:04 +00:00
|
|
|
// RunProcessOnce runs the process described by the ProcessConfig (though it
|
2022-06-30 19:42:57 +00:00
|
|
|
// doesn't use all fields from the ProcessConfig).
|
2022-01-23 03:36:07 +00:00
|
|
|
//
|
2022-06-30 19:42:57 +00:00
|
|
|
// The process is killed if-and-only-if the context is canceled, returning -1
|
|
|
|
// and context.Canceled. Otherwise the exit status of the process is returned,
|
|
|
|
// or -1 and an error.
|
2022-01-23 03:36:07 +00:00
|
|
|
//
|
2022-02-27 18:12:26 +00:00
|
|
|
// The stdout and stderr of the process will be written to the corresponding
|
|
|
|
// Loggers. Various runtime events will be written to the sysLogger.
|
|
|
|
func RunProcessOnce(
|
|
|
|
ctx context.Context,
|
|
|
|
stdoutLogger, stderrLogger, sysLogger Logger,
|
2022-06-19 01:52:04 +00:00
|
|
|
cfg ProcessConfig,
|
2022-02-27 18:12:26 +00:00
|
|
|
) (
|
|
|
|
int, error,
|
|
|
|
) {
|
2022-01-23 03:36:07 +00:00
|
|
|
|
|
|
|
cfg = cfg.withDefaults()
|
2022-01-23 03:03:13 +00:00
|
|
|
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
|
2022-02-27 18:12:26 +00:00
|
|
|
fwdOutPipe := func(logger Logger, r io.Reader) {
|
2022-01-23 03:03:13 +00:00
|
|
|
wg.Add(1)
|
|
|
|
go func() {
|
|
|
|
defer wg.Done()
|
|
|
|
bufR := bufio.NewReader(r)
|
|
|
|
for {
|
|
|
|
line, err := bufR.ReadString('\n')
|
|
|
|
if errors.Is(err, io.EOF) {
|
|
|
|
return
|
|
|
|
} else if err != nil {
|
|
|
|
logger.Printf("reading output: %v", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
logger.Println(strings.TrimSuffix(line, "\n"))
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
|
|
|
cmd := exec.Command(cfg.Cmd, cfg.Args...)
|
2022-01-23 03:57:59 +00:00
|
|
|
|
2022-01-23 03:39:34 +00:00
|
|
|
cmd.Dir = cfg.Dir
|
2022-01-23 03:03:13 +00:00
|
|
|
|
2022-06-30 19:31:32 +00:00
|
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
|
|
|
// Indicates that the child process should be a part of a separate
|
|
|
|
// process group than the parent, so that it does not receive signals
|
|
|
|
// that the parent receives. This is what ensures that context
|
|
|
|
// cancellation is the only way to interrupt the child processes.
|
|
|
|
Setpgid: true,
|
|
|
|
}
|
|
|
|
|
|
|
|
cmd.Env = os.Environ()
|
2022-01-23 03:03:13 +00:00
|
|
|
for k, v := range cfg.Env {
|
|
|
|
cmd.Env = append(cmd.Env, k+"="+v)
|
|
|
|
}
|
|
|
|
|
|
|
|
stdout, err := cmd.StdoutPipe()
|
|
|
|
if err != nil {
|
2022-02-25 03:48:37 +00:00
|
|
|
return -1, fmt.Errorf("getting stdout pipe: %w", err)
|
2022-01-23 03:03:13 +00:00
|
|
|
}
|
|
|
|
defer stdout.Close()
|
|
|
|
|
|
|
|
stderr, err := cmd.StderrPipe()
|
|
|
|
if err != nil {
|
2022-02-25 03:48:37 +00:00
|
|
|
return -1, fmt.Errorf("getting stderr pipe: %w", err)
|
2022-01-23 03:03:13 +00:00
|
|
|
}
|
|
|
|
defer stderr.Close()
|
|
|
|
|
2022-02-27 18:12:26 +00:00
|
|
|
fwdOutPipe(stdoutLogger, stdout)
|
|
|
|
fwdOutPipe(stderrLogger, stderr)
|
2022-01-23 03:03:13 +00:00
|
|
|
|
|
|
|
if err := cmd.Start(); err != nil {
|
2022-02-25 03:48:37 +00:00
|
|
|
return -1, fmt.Errorf("starting process: %w", err)
|
2022-01-23 03:03:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
stopCh := make(chan struct{})
|
2022-01-23 03:36:07 +00:00
|
|
|
|
2022-01-23 03:03:13 +00:00
|
|
|
go func(proc *os.Process) {
|
2022-06-30 19:31:32 +00:00
|
|
|
|
2022-01-23 03:03:13 +00:00
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
2022-06-30 19:31:32 +00:00
|
|
|
sigProcessGroup(sysLogger, proc, syscall.SIGINT)
|
2022-01-23 03:03:13 +00:00
|
|
|
case <-stopCh:
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
select {
|
|
|
|
case <-time.After(cfg.SigKillWait):
|
2022-06-30 19:31:32 +00:00
|
|
|
sigProcessGroup(sysLogger, proc, syscall.SIGKILL)
|
2022-01-23 03:03:13 +00:00
|
|
|
case <-stopCh:
|
|
|
|
}
|
2022-06-30 19:31:32 +00:00
|
|
|
|
2022-01-23 03:03:13 +00:00
|
|
|
}(cmd.Process)
|
|
|
|
|
|
|
|
wg.Wait()
|
|
|
|
|
2022-02-25 03:48:37 +00:00
|
|
|
err = cmd.Wait()
|
2022-02-27 18:45:33 +00:00
|
|
|
close(stopCh)
|
|
|
|
|
2022-06-30 19:42:57 +00:00
|
|
|
if err := ctx.Err(); err != nil {
|
|
|
|
return -1, err
|
|
|
|
}
|
2022-02-25 03:48:37 +00:00
|
|
|
|
|
|
|
if err != nil {
|
2022-06-30 19:42:57 +00:00
|
|
|
return -1, fmt.Errorf("process exited: %w", err)
|
2022-01-23 03:03:13 +00:00
|
|
|
}
|
|
|
|
|
2022-06-30 19:42:57 +00:00
|
|
|
return cmd.ProcessState.ExitCode(), nil
|
2022-01-23 03:03:13 +00:00
|
|
|
}
|
|
|
|
|
2022-06-30 19:31:32 +00:00
|
|
|
// RunProcess runs a process (configured by ProcessConfig) until the context is
|
2022-06-19 01:52:04 +00:00
|
|
|
// canceled, at which point the process is killed and RunProcess returns.
|
2022-01-23 03:03:13 +00:00
|
|
|
//
|
|
|
|
// The process will be restarted if it exits of its own accord. There will be a
|
2022-06-30 19:31:32 +00:00
|
|
|
// brief wait time between each restart, with an exponential backoff mechanism
|
|
|
|
// so that the wait time increases upon repeated restarts.
|
2022-01-23 03:03:13 +00:00
|
|
|
//
|
2022-02-27 18:12:26 +00:00
|
|
|
// The stdout and stderr of the process will be written to the corresponding
|
|
|
|
// Loggers. Various runtime events will be written to the sysLogger.
|
|
|
|
func RunProcess(
|
|
|
|
ctx context.Context,
|
|
|
|
stdoutLogger, stderrLogger, sysLogger Logger,
|
2022-06-19 01:52:04 +00:00
|
|
|
cfg ProcessConfig,
|
2022-02-27 18:12:26 +00:00
|
|
|
) {
|
2022-01-23 03:03:13 +00:00
|
|
|
|
|
|
|
cfg = cfg.withDefaults()
|
|
|
|
|
|
|
|
var wait time.Duration
|
|
|
|
|
|
|
|
for {
|
|
|
|
start := time.Now()
|
2022-02-27 18:12:26 +00:00
|
|
|
exitCode, err := RunProcessOnce(
|
|
|
|
ctx,
|
|
|
|
stdoutLogger, stderrLogger, sysLogger,
|
|
|
|
cfg,
|
|
|
|
)
|
2022-01-23 03:03:13 +00:00
|
|
|
took := time.Since(start)
|
|
|
|
|
|
|
|
if err != nil {
|
2022-06-30 19:42:57 +00:00
|
|
|
sysLogger.Printf("exited: %v", err)
|
2022-01-23 03:03:13 +00:00
|
|
|
} else {
|
2022-06-30 19:42:57 +00:00
|
|
|
sysLogger.Printf("exit code: %d", exitCode)
|
2022-01-23 03:03:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if err := ctx.Err(); err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-02-25 03:48:37 +00:00
|
|
|
for i := range cfg.NoRestartOn {
|
|
|
|
if cfg.NoRestartOn[i] == exitCode {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-23 03:03:13 +00:00
|
|
|
wait = ((wait * 2) - took).Truncate(time.Millisecond)
|
|
|
|
|
|
|
|
if wait < cfg.MinWait {
|
|
|
|
wait = cfg.MinWait
|
|
|
|
} else if wait > cfg.MaxWait {
|
|
|
|
wait = cfg.MaxWait
|
|
|
|
}
|
|
|
|
|
2022-02-27 18:12:26 +00:00
|
|
|
sysLogger.Printf("will restart process in %v", wait)
|
2022-01-23 03:03:13 +00:00
|
|
|
|
|
|
|
select {
|
|
|
|
case <-time.After(wait):
|
|
|
|
case <-ctx.Done():
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|