bitbucket.org/ai69/amoy@v0.2.3/exec_start.go (about)

     1  package amoy
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"os/exec"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/1set/gut/ystring"
    13  )
    14  
    15  // Command represents an external command being running or exited.
    16  type Command struct {
    17  	sync.Mutex
    18  	cmd     *exec.Cmd
    19  	bufOut  bytes.Buffer
    20  	bufErr  bytes.Buffer
    21  	err     error
    22  	pid     int
    23  	startAt time.Time
    24  	stopAt  time.Time
    25  	done    chan struct{}
    26  }
    27  
    28  // Done returns a channel that's closed when the command exits.
    29  func (c *Command) Done() <-chan struct{} {
    30  	return c.done
    31  }
    32  
    33  // Stdout returns a slice holding the content of the standard output of the command.
    34  func (c *Command) Stdout() []byte {
    35  	return c.bufOut.Bytes()
    36  }
    37  
    38  // Stderr returns a slice holding the content of the standard error of the command.
    39  func (c *Command) Stderr() []byte {
    40  	return c.bufErr.Bytes()
    41  }
    42  
    43  // Error returns the error after the command exits if it exists.
    44  func (c *Command) Error() error {
    45  	return c.err
    46  }
    47  
    48  // ProcessID indicates PID of the command.
    49  func (c *Command) ProcessID() int {
    50  	return c.pid
    51  }
    52  
    53  // StartedAt indicates the moment that the command started.
    54  func (c *Command) StartedAt() time.Time {
    55  	return c.startAt
    56  }
    57  
    58  // StoppedAt indicates the moment that the command stopped or zero if it's still running.
    59  func (c *Command) StoppedAt() time.Time {
    60  	return c.stopAt
    61  }
    62  
    63  // Exited reports whether the command has exited.
    64  func (c *Command) Exited() bool {
    65  	if c.cmd.ProcessState != nil {
    66  		return c.cmd.ProcessState.Exited()
    67  	}
    68  	return false
    69  }
    70  
    71  // Kill causes the command to exit immediately, and does not wait until it has actually exited.
    72  func (c *Command) Kill() error {
    73  	// TODO: use sync.Once to wrapper it, Status() func to get the status of the command: running, exited, crashed, killed
    74  	if c.cmd.Process != nil {
    75  		return c.cmd.Process.Kill()
    76  	}
    77  	return errMissingProcInfo
    78  }
    79  
    80  // CommandOptions represents custom options to execute external command.
    81  type CommandOptions struct {
    82  	// WorkDir is the working directory of the command.
    83  	WorkDir string
    84  	// EnvVar appends the environment variables of the command.
    85  	EnvVar map[string]string
    86  	// Stdout is the writer to write standard output to. Use os.Pipe() to create a pipe for this, if you want to handle the result synchronously.
    87  	Stdout io.Writer
    88  	// Stderr is the writer to write standard error to. Use os.Pipe() to create a pipe for this, if you want to handle the result synchronously.
    89  	Stderr io.Writer
    90  	// DisableResult indicates whether to disable the buffered result of the command, especially important for long-running commands.
    91  	DisableResult bool
    92  	// Stdin is the input to the command's standard input.
    93  	Stdin io.Reader
    94  	// TODO: not implemented
    95  	Timeout time.Time
    96  }
    97  
    98  // StartSimpleCommand starts the specified command but does not wait for it to complete, and simultaneously writes its standard output and standard error to given writers.
    99  func StartSimpleCommand(command string, writerStdout, writerStderr io.Writer) (*Command, error) {
   100  	return StartCommand(command, &CommandOptions{
   101  		Stdout: writerStdout,
   102  		Stderr: writerStderr,
   103  	})
   104  }
   105  
   106  // StartCommand starts the specified command with given options but does not wait for it to complete.
   107  func StartCommand(command string, opts ...*CommandOptions) (*Command, error) {
   108  	cmd, err := parseSingleRawCommand(command)
   109  	if err != nil {
   110  		return nil, err
   111  	}
   112  
   113  	var (
   114  		// handler for cmd
   115  		handler = Command{
   116  			cmd:  cmd,
   117  			done: make(chan struct{}),
   118  		}
   119  
   120  		// pipes that will be connected
   121  		pipeStdout, _ = cmd.StdoutPipe()
   122  		pipeStderr, _ = cmd.StderrPipe()
   123  
   124  		// combined writers that duplicate given writers and buffers
   125  		comStdout io.Writer
   126  		comStderr io.Writer
   127  	)
   128  
   129  	// use the first option if it exists
   130  	if len(opts) > 0 {
   131  		opt := opts[0]
   132  
   133  		// for env var
   134  		if len(opt.EnvVar) > 0 {
   135  			cmd.Env = os.Environ()
   136  			for k, v := range opt.EnvVar {
   137  				cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v))
   138  			}
   139  		}
   140  
   141  		// for standard input stream
   142  		if opt.Stdin != nil {
   143  			cmd.Stdin = opt.Stdin
   144  		}
   145  
   146  		// set working directory
   147  		if ystring.IsNotBlank(opt.WorkDir) {
   148  			cmd.Dir = opt.WorkDir
   149  		}
   150  
   151  		// handle standard output
   152  		if opt.Stdout != nil {
   153  			// if the given writer exists
   154  			if opt.DisableResult {
   155  				// if the buffered result is disabled, just redirects to the given writer
   156  				comStdout = opt.Stdout
   157  			} else {
   158  				// otherwise, writes both to buffered result and given writer
   159  				comStdout = io.MultiWriter(opt.Stdout, &handler.bufOut)
   160  			}
   161  		} else {
   162  			// if the given writer doesn't exist
   163  			if opt.DisableResult {
   164  				// if the buffered result is disabled, writes to /dev/null
   165  				comStdout = DiscardWriter
   166  			} else {
   167  				// otherwise, writes to buffered result
   168  				comStdout = &handler.bufOut
   169  			}
   170  		}
   171  
   172  		// handle standard error
   173  		if opt.Stderr != nil {
   174  			// if the given writer exists
   175  			if opt.DisableResult {
   176  				// if the buffered result is disabled, just redirects to the given writer
   177  				comStderr = opt.Stderr
   178  			} else {
   179  				// otherwise, writes both to buffered result and given writer
   180  				comStderr = io.MultiWriter(opt.Stderr, &handler.bufErr)
   181  			}
   182  		} else {
   183  			// if the given writer doesn't exist
   184  			if opt.DisableResult {
   185  				// if the buffered result is disabled, writes to /dev/null
   186  				comStderr = DiscardWriter
   187  			} else {
   188  				// otherwise, writes to buffered result
   189  				comStderr = &handler.bufErr
   190  			}
   191  		}
   192  	}
   193  
   194  	// let's start!
   195  	if err := cmd.Start(); err != nil {
   196  		return nil, fmt.Errorf("fail to start exec: %w", err)
   197  	}
   198  
   199  	if cmd.Process != nil {
   200  		handler.startAt = time.Now()
   201  		handler.pid = cmd.Process.Pid
   202  	}
   203  
   204  	var (
   205  		wg        sync.WaitGroup
   206  		errStdout error
   207  		errStderr error
   208  	)
   209  	wg.Add(1)
   210  	go func() {
   211  		defer wg.Done()
   212  		_, errStdout = io.Copy(comStdout, pipeStdout)
   213  	}()
   214  
   215  	wg.Add(1)
   216  	go func() {
   217  		defer wg.Done()
   218  		_, errStderr = io.Copy(comStderr, pipeStderr)
   219  	}()
   220  
   221  	go func() {
   222  		defer func() {
   223  			close(handler.done)
   224  		}()
   225  
   226  		wg.Wait()
   227  		handler.stopAt = time.Now()
   228  
   229  		if err := cmd.Wait(); err != nil {
   230  			handler.err = fmt.Errorf("fail to run exec: %w", err)
   231  			return
   232  		}
   233  		if errStdout != nil {
   234  			handler.err = fmt.Errorf("fail to capture stdout: %w", errStdout)
   235  			return
   236  		}
   237  		if errStderr != nil {
   238  			handler.err = fmt.Errorf("fail to capture stderr: %w", errStderr)
   239  			return
   240  		}
   241  	}()
   242  
   243  	return &handler, nil
   244  }