command/command.go (view raw)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | 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
}
|