package command import ( "context" "errors" "fmt" "io" "os" "os/exec" "strings" "syscall" "alin.ovh/erl/output" ) type Command interface { Start() error Wait() error Stop(context.Context) error } type Cmd struct { *exec.Cmd name string args []string out output.Output opts Options } type Options struct { Output io.Writer Stdout io.Writer Stderr io.Writer } func New(name string, args []string, options Options) *Cmd { if options.Stdout == nil { options.Stdout = os.Stdout } if options.Stderr == nil { options.Stderr = os.Stderr } if options.Output == nil { options.Output = os.Stderr } return &Cmd{ name: name, args: args, opts: options, out: output.New(options.Output), } } func (cmd *Cmd) Stop(ctx context.Context) error { if cmd.Cmd == nil || cmd.Process == nil || cmd.ProcessState != nil { return nil } signal := syscall.SIGINT cmd.out.Info("[sending %s to command]\n", signal) var target int if cmd.SysProcAttr != nil && cmd.SysProcAttr.Pgid != 0 { target = cmd.SysProcAttr.Pgid } else { target = -cmd.Process.Pid } err := syscall.Kill(target, signal) if err != nil { return fmt.Errorf("error killing command: %v", err) } exited := make(chan struct{}) go func() { if cmd.Process != nil { err := cmd.Wait() if err != nil { exErr := new(exec.ExitError) if errors.As(err, &exErr) { cmd.out.Fail("[error waiting for command: %s]\n", err) } } } exited <- struct{}{} close(exited) }() for { select { case <-ctx.Done(): err := ctx.Err() if !errors.Is(err, context.DeadlineExceeded) { return err } cmd.out.Fail("[command not stopped, killing]\n") err = syscall.Kill(target, syscall.SIGKILL) if err != nil && !errors.Is(err, os.ErrProcessDone) { return fmt.Errorf("error killing command: %v", err) } return nil case <-exited: return nil } } } func (cmd *Cmd) Exited() bool { return cmd.Cmd != nil && cmd.ProcessState != nil && cmd.ProcessState.Exited() } func (cmd *Cmd) Wait() error { err := cmd.Cmd.Wait() var exitErr *exec.ExitError if err != nil && !errors.As(err, &exitErr) { return err } switch { case !cmd.Exited(): cmd.out.Info("[command stopped]\n") case cmd.ProcessState.Success(): cmd.out.Success("[command finished]\n") default: cmd.out.Fail("[command exited with code %d]\n", cmd.ProcessState.ExitCode()) } return nil } func (cmd *Cmd) Start() error { cmd.makeCommand() cmd.out.Success("[running: %s]\n", strings.Join(append([]string{cmd.name}, cmd.args...), " ")) err := cmd.Cmd.Start() if err != nil { return fmt.Errorf("error starting command: %v", err) } return err } func (cmd *Cmd) makeCommand() { cmd.Cmd = exec.Command(cmd.name, cmd.args...) cmd.SysProcAttr = &syscall.SysProcAttr{ Setpgid: true, Pgid: 0, } cmd.Stdout = cmd.opts.Stdout cmd.Stderr = cmd.opts.Stderr }