Spawn child processes in separate process groups
This way signals sent to the parent will not be propagated to the children. The only way to kill children is by cancelling the context passed into the Run method being used.
This commit is contained in:
parent
8fb99b53d7
commit
881bd83086
@ -13,7 +13,9 @@ type Config struct {
|
|||||||
Processes []ProcessConfig `yaml:"processes"`
|
Processes []ProcessConfig `yaml:"processes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run runs the given configuration as if this was a real pmux process.
|
// Run runs the given configuration as if this was a real pmux process. It will
|
||||||
|
// block until the context is canceled and all child processes have been cleaned
|
||||||
|
// up.
|
||||||
func Run(ctx context.Context, cfg Config) {
|
func Run(ctx context.Context, cfg Config) {
|
||||||
|
|
||||||
stdoutLogger := newLogger(os.Stdout, logSepStdout, cfg.TimeFormat)
|
stdoutLogger := newLogger(os.Stdout, logSepStdout, cfg.TimeFormat)
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -66,8 +67,26 @@ func (cfg ProcessConfig) withDefaults() ProcessConfig {
|
|||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// RunProcessOnce runs the process described by the ProcessConfig (though it
|
// RunProcessOnce runs the process described by the ProcessConfig (though it
|
||||||
// doesn't use all fields from the ProcessConfig). The process is killed if the
|
// doesn't use all fields from the ProcessConfig). The process is killed iff the
|
||||||
// context is canceled. The exit status of the process is returned, or -1 if the
|
// context is canceled. The exit status of the process is returned, or -1 if the
|
||||||
// process was never started.
|
// process was never started.
|
||||||
//
|
//
|
||||||
@ -110,8 +129,16 @@ func RunProcessOnce(
|
|||||||
cmd := exec.Command(cfg.Cmd, cfg.Args...)
|
cmd := exec.Command(cfg.Cmd, cfg.Args...)
|
||||||
|
|
||||||
cmd.Dir = cfg.Dir
|
cmd.Dir = cfg.Dir
|
||||||
cmd.Env = os.Environ()
|
|
||||||
|
|
||||||
|
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()
|
||||||
for k, v := range cfg.Env {
|
for k, v := range cfg.Env {
|
||||||
cmd.Env = append(cmd.Env, k+"="+v)
|
cmd.Env = append(cmd.Env, k+"="+v)
|
||||||
}
|
}
|
||||||
@ -138,18 +165,20 @@ func RunProcessOnce(
|
|||||||
stopCh := make(chan struct{})
|
stopCh := make(chan struct{})
|
||||||
|
|
||||||
go func(proc *os.Process) {
|
go func(proc *os.Process) {
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
sigProcessGroup(sysLogger, proc, syscall.SIGINT)
|
||||||
case <-stopCh:
|
case <-stopCh:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-time.After(cfg.SigKillWait):
|
case <-time.After(cfg.SigKillWait):
|
||||||
sysLogger.Println("forcefully killing process")
|
sigProcessGroup(sysLogger, proc, syscall.SIGKILL)
|
||||||
_ = proc.Signal(os.Kill)
|
|
||||||
case <-stopCh:
|
case <-stopCh:
|
||||||
}
|
}
|
||||||
|
|
||||||
}(cmd.Process)
|
}(cmd.Process)
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
@ -166,12 +195,12 @@ func RunProcessOnce(
|
|||||||
return exitCode, nil
|
return exitCode, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunProcess is a process (configured by ProcessConfig) until the context is
|
// RunProcess runs a process (configured by ProcessConfig) until the context is
|
||||||
// canceled, at which point the process is killed and RunProcess returns.
|
// canceled, at which point the process is killed and RunProcess returns.
|
||||||
//
|
//
|
||||||
// The process will be restarted if it exits of its own accord. There will be a
|
// The process will be restarted if it exits of its own accord. There will be a
|
||||||
// brief wait time between each restart, with an exponential backup mechanism so
|
// brief wait time between each restart, with an exponential backoff mechanism
|
||||||
// that the wait time increases upon repeated restarts.
|
// so that the wait time increases upon repeated restarts.
|
||||||
//
|
//
|
||||||
// The stdout and stderr of the process will be written to the corresponding
|
// The stdout and stderr of the process will be written to the corresponding
|
||||||
// Loggers. Various runtime events will be written to the sysLogger.
|
// Loggers. Various runtime events will be written to the sysLogger.
|
||||||
|
Loading…
Reference in New Issue
Block a user