diff --git a/pmux-example.yml b/pmux-example.yml index dd0c7bd..8f67e94 100644 --- a/pmux-example.yml +++ b/pmux-example.yml @@ -19,6 +19,8 @@ processes: env: TARGET: example.com + dir: "/tmp" + # pmux uses an exponential backoff when restarting a process, so subsequent # restarts will each take longer and longer. minWait/maxWait indicate the # min/max wait times between restarts of this process, respectively. diff --git a/pmuxproc/pmuxproc.go b/pmuxproc/pmuxproc.go index fe6e6e8..ce1bc66 100644 --- a/pmuxproc/pmuxproc.go +++ b/pmuxproc/pmuxproc.go @@ -27,7 +27,7 @@ type Config struct { // 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. - Dir string + Dir string `yaml:"dir"` // MinWait and MaxWait are the minimum and maximum amount of time between // restarts that RunProcess will wait. @@ -42,6 +42,10 @@ type Config struct { // // Defalts to 10 seconds. SigKillWait time.Duration `yaml:"sigKillWait"` + + // NoRestartOn indicates which exit codes should result in the process not + // being restarted any further. + NoRestartOn []int `yaml:"no_restart_on"` } func (cfg Config) withDefaults() Config { @@ -63,14 +67,15 @@ func (cfg Config) withDefaults() Config { // RunProcessOnce runs the process described by the Config (though it doesn't // use all fields from the Config). The process is killed if the context is -// canceled. +// canceled. The exit status of the process is returned, or -1 if the process +// was never started. // // It returns nil if the process exits normally with a zero status. It returns // an error otherwise. // // The stdout and stderr of the process will be written to the given Logger, as // well as various runtime events. -func RunProcessOnce(ctx context.Context, logger Logger, cfg Config) error { +func RunProcessOnce(ctx context.Context, logger Logger, cfg Config) (int, error) { cfg = cfg.withDefaults() @@ -106,13 +111,13 @@ func RunProcessOnce(ctx context.Context, logger Logger, cfg Config) error { stdout, err := cmd.StdoutPipe() if err != nil { - return fmt.Errorf("getting stdout pipe: %w", err) + return -1, fmt.Errorf("getting stdout pipe: %w", err) } defer stdout.Close() stderr, err := cmd.StderrPipe() if err != nil { - return fmt.Errorf("getting stderr pipe: %w", err) + return -1, fmt.Errorf("getting stderr pipe: %w", err) } defer stderr.Close() @@ -120,7 +125,7 @@ func RunProcessOnce(ctx context.Context, logger Logger, cfg Config) error { fwdOutPipe(stderr) if err := cmd.Start(); err != nil { - return fmt.Errorf("starting process: %w", err) + return -1, fmt.Errorf("starting process: %w", err) } // go-routine which will sent interrupt if the context is cancelled. Also @@ -148,11 +153,14 @@ func RunProcessOnce(ctx context.Context, logger Logger, cfg Config) error { wg.Wait() - if err := cmd.Wait(); err != nil { - return fmt.Errorf("process exited: %w", err) + err = cmd.Wait() + exitCode := cmd.ProcessState.ExitCode() + + if err != nil { + return exitCode, fmt.Errorf("process exited: %w", err) } - return nil + return exitCode, nil } // RunProcess is a process (configured by Config) until the context is canceled, @@ -172,19 +180,25 @@ func RunProcess(ctx context.Context, logger Logger, cfg Config) { for { start := time.Now() - err := RunProcessOnce(ctx, logger, cfg) + exitCode, err := RunProcessOnce(ctx, logger, cfg) took := time.Since(start) if err != nil { - logger.Printf("%v", err) + logger.Printf("exit code %d, %v", exitCode, err) } else { - logger.Println("exit status 0") + logger.Println("exit code 0") } if err := ctx.Err(); err != nil { return } + for i := range cfg.NoRestartOn { + if cfg.NoRestartOn[i] == exitCode { + return + } + } + wait = ((wait * 2) - took).Truncate(time.Millisecond) if wait < cfg.MinWait {