github.com/Benchkram/bob@v0.0.0-20220321080157-7c8f3876e225/pkg/execctl/cmd.go (about)

     1  package execctl
     2  
     3  import (
     4  	"errors"
     5  	"io"
     6  	"os"
     7  	"os/exec"
     8  	"strings"
     9  	"sync"
    10  
    11  	"github.com/Benchkram/bob/pkg/ctl"
    12  	"github.com/Benchkram/bob/pkg/usererror"
    13  )
    14  
    15  var (
    16  	ErrCmdAlreadyStarted = errors.New("cmd already started")
    17  )
    18  
    19  // Command allows to execute a command and interact with it in real-time
    20  // type Command interface {
    21  // 	Start() error
    22  // 	Stop() error
    23  // 	Restart() error
    24  
    25  // 	Shutdown() error
    26  // 	Done() <-chan struct{}
    27  
    28  // 	Stdout() io.Reader
    29  // 	Stderr() io.Reader
    30  // 	Stdin() io.Writer
    31  // }
    32  
    33  // assert Cmd implements the Command interface
    34  var _ ctl.Command = (*Cmd)(nil)
    35  
    36  // Cmd allows to control a process started through os.Exec with additional start, stop and restart capabilities, and
    37  // provides readers/writers for the command's outputs and input, respectively.
    38  type Cmd struct {
    39  	mux         sync.Mutex
    40  	cmd         *exec.Cmd
    41  	name        string
    42  	exe         string
    43  	args        []string
    44  	stdout      pipe
    45  	stderr      pipe
    46  	stdin       pipe
    47  	running     bool
    48  	interrupted bool
    49  	err         chan error
    50  	lastErr     error
    51  }
    52  
    53  type pipe struct {
    54  	r *os.File
    55  	w *os.File
    56  }
    57  
    58  // NewCmd creates a new Cmd, ready to be started
    59  func NewCmd(name string, exe string, args ...string) (c *Cmd, err error) {
    60  	c = &Cmd{
    61  		name: name,
    62  		exe:  exe,
    63  		args: args,
    64  		err:  make(chan error, 1),
    65  	}
    66  
    67  	// create pipes for stdout, stderr and stdin
    68  	c.stdout.r, c.stdout.w, err = os.Pipe()
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  
    73  	c.stderr.r, c.stderr.w, err = os.Pipe()
    74  	if err != nil {
    75  		return nil, err
    76  	}
    77  
    78  	c.stdin.r, c.stdin.w, err = os.Pipe()
    79  	if err != nil {
    80  		return nil, err
    81  	}
    82  
    83  	return c, nil
    84  }
    85  
    86  func (c *Cmd) Name() string {
    87  	c.mux.Lock()
    88  	defer c.mux.Unlock()
    89  
    90  	return c.name
    91  }
    92  
    93  func (c *Cmd) Running() bool {
    94  	c.mux.Lock()
    95  	defer c.mux.Unlock()
    96  
    97  	return c.running
    98  }
    99  
   100  // Start starts the command if it's not already running. It will be a noop if it is.
   101  // It also spins up a goroutine that will receive any error occurred during the command's exit.
   102  func (c *Cmd) Start() error {
   103  	c.mux.Lock()
   104  	defer c.mux.Unlock()
   105  
   106  	if c.running {
   107  		return nil
   108  	}
   109  
   110  	c.running = true
   111  	c.interrupted = false
   112  	c.lastErr = nil
   113  
   114  	// create the command with the found executable and the its args
   115  	cmd := exec.Command(c.exe, c.args...)
   116  	c.cmd = cmd
   117  
   118  	// assign the pipes to the command
   119  	c.cmd.Stdout = c.stdout.w
   120  	c.cmd.Stderr = c.stderr.w
   121  	c.cmd.Stdin = c.stdin.r
   122  
   123  	// start the command
   124  	err := c.cmd.Start()
   125  	if err != nil {
   126  		return usererror.Wrapm(err, "Command execution failed")
   127  	}
   128  
   129  	go func() {
   130  		err = cmd.Wait()
   131  
   132  		c.err <- err
   133  
   134  		c.mux.Lock()
   135  
   136  		c.running = false
   137  
   138  		c.mux.Unlock()
   139  	}()
   140  
   141  	return nil
   142  }
   143  
   144  // Stop stops the running command with an os.Interrupt signal. It does not return an error if the command has
   145  // already exited gracefully.
   146  func (c *Cmd) Stop() error {
   147  	err := c.stop()
   148  	if err != nil {
   149  		return err
   150  	}
   151  
   152  	return c.Wait()
   153  }
   154  
   155  // Restart first interrupts the command if it's already running, and then re-runs the command.
   156  func (c *Cmd) Restart() error {
   157  	err := c.stop()
   158  	if err != nil {
   159  		return err
   160  	}
   161  
   162  	err = c.Wait()
   163  	if err != nil {
   164  		return err
   165  	}
   166  
   167  	return c.Start()
   168  }
   169  
   170  // Stdout returns a reader to the command's stdout. The reader will return an io.EOF error if the command exits.
   171  func (c *Cmd) Stdout() io.Reader {
   172  	c.mux.Lock()
   173  	defer c.mux.Unlock()
   174  
   175  	return c.stdout.r
   176  }
   177  
   178  // Stderr returns a reader to the command's stderr. The reader will return an io.EOF error if the command exits.
   179  func (c *Cmd) Stderr() io.Reader {
   180  	c.mux.Lock()
   181  	defer c.mux.Unlock()
   182  
   183  	return c.stderr.r
   184  }
   185  
   186  // Stdin returns a writer to the command's stdin. The writer will be closed if the command has exited by the time this
   187  // function is called.
   188  func (c *Cmd) Stdin() io.Writer {
   189  	c.mux.Lock()
   190  	defer c.mux.Unlock()
   191  
   192  	return c.stdin.w
   193  }
   194  
   195  // Wait awaits for the command to stop running (either to gracefully exit or be interrupted).
   196  // If the command has already finished when Wait is invoked, it returns the error that was returned when the command
   197  // exited, if any.
   198  func (c *Cmd) Wait() error {
   199  	c.mux.Lock()
   200  
   201  	running := c.running
   202  	errChan := c.err
   203  	lastErr := c.lastErr
   204  	interrupted := c.interrupted
   205  
   206  	if !running {
   207  		c.mux.Unlock()
   208  
   209  		return lastErr
   210  	}
   211  
   212  	c.mux.Unlock()
   213  
   214  	err := <-errChan
   215  
   216  	c.mux.Lock()
   217  
   218  	if err != nil && interrupted && strings.Contains(err.Error(), "signal: interrupt") {
   219  		err = nil
   220  	} else if err != nil {
   221  		c.lastErr = err
   222  	}
   223  
   224  	c.mux.Unlock()
   225  
   226  	return err
   227  }
   228  
   229  // stop requests for the command to stop, if it has already started.
   230  func (c *Cmd) stop() error {
   231  	c.mux.Lock()
   232  
   233  	running := c.running
   234  	cmd := c.cmd
   235  	c.interrupted = true
   236  
   237  	c.mux.Unlock()
   238  
   239  	if !running {
   240  		return nil
   241  	}
   242  
   243  	if cmd != nil && cmd.Process != nil {
   244  		// send an interrupt signal to the command
   245  		err := cmd.Process.Signal(os.Interrupt)
   246  		if err != nil && !strings.Contains(err.Error(), "os: process already finished") {
   247  			return err
   248  		}
   249  	}
   250  
   251  	return nil
   252  }
   253  
   254  // Shutdown stops the cmd
   255  func (c *Cmd) Shutdown() error {
   256  	return c.Stop()
   257  }
   258  
   259  func (c *Cmd) Done() <-chan struct{} {
   260  	done := make(chan struct{})
   261  	go func() {
   262  		_ = c.Wait()
   263  		close(done)
   264  	}()
   265  	return done
   266  }