github.com/jmigpin/editor@v1.6.0/util/osutil/cmd.go (about)

     1  package osutil
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"os/exec"
     9  	"strings"
    10  	"sync"
    11  )
    12  
    13  //godebug:annotatefile
    14  
    15  type Cmd struct {
    16  	*exec.Cmd
    17  	ctx         context.Context
    18  	cancelCtx   context.CancelFunc
    19  	setupCalled bool
    20  
    21  	PreOutputCallback func()
    22  
    23  	NoEnsureStop bool
    24  	ensureStop   struct {
    25  		sync.Mutex
    26  		off bool
    27  	}
    28  
    29  	closers []io.Closer
    30  
    31  	copy struct {
    32  		fns     []func()
    33  		closers []io.Closer
    34  	}
    35  }
    36  
    37  // If Start() is not called, Cancel() must be called to clear resources.
    38  func NewCmd(ctx context.Context, args ...string) *Cmd {
    39  	ctx2, cancel := context.WithCancel(ctx)
    40  	c := exec.CommandContext(ctx2, args[0], args[1:]...) // panic on empty args
    41  	cmd := &Cmd{Cmd: c, ctx: ctx2, cancelCtx: cancel}
    42  	return cmd
    43  }
    44  
    45  //----------
    46  
    47  // can be called before a Start(), clears all possible resources
    48  func (cmd *Cmd) Cancel() {
    49  	cmd.cancelCtx()
    50  	cmd.closeCopyClosers()
    51  }
    52  
    53  //----------
    54  
    55  // If Start() returns no error, Wait() must be called to clear resources.
    56  func (cmd *Cmd) Start() error {
    57  	if err := cmd.start2(); err != nil {
    58  		cmd.Cancel()
    59  		return err
    60  	}
    61  	return nil
    62  }
    63  
    64  func (cmd *Cmd) start2() error {
    65  	if cmd.Cmd.SysProcAttr == nil {
    66  		SetupExecCmdSysProcAttr(cmd.Cmd)
    67  	}
    68  
    69  	if err := cmd.Cmd.Start(); err != nil {
    70  		return err
    71  	}
    72  
    73  	// Ensure callback is called before the first stdout/stderr write (works since the process takes longer to launch and write back, but in theory this could be called after some output comes out). Always works if stdin/out/err was setup with SetupStdio() since the copy loop starts after the callback.
    74  	if cmd.PreOutputCallback != nil {
    75  		cmd.PreOutputCallback()
    76  	}
    77  
    78  	cmd.runCopyFns()
    79  
    80  	go func() {
    81  		select {
    82  		case <-cmd.ctx.Done():
    83  			cmd.ensureStopNow()
    84  		}
    85  	}()
    86  
    87  	return nil
    88  }
    89  
    90  func (cmd *Cmd) Wait() error {
    91  	// Explanations on possible hangs.
    92  	// https://github.com/golang/go/issues/18874#issuecomment-277280139
    93  
    94  	defer func() {
    95  		cmd.disableEnsureStop() // no need to kill process anymore
    96  		cmd.Cancel()            // clear resources
    97  	}()
    98  	return cmd.Cmd.Wait()
    99  }
   100  
   101  func (cmd *Cmd) Run() error {
   102  	if err := cmd.Start(); err != nil {
   103  		return err
   104  	}
   105  	return cmd.Wait()
   106  }
   107  
   108  //----------
   109  
   110  func (cmd *Cmd) disableEnsureStop() {
   111  	cmd.ensureStop.Lock()
   112  	defer cmd.ensureStop.Unlock()
   113  	cmd.ensureStop.off = true
   114  }
   115  
   116  func (cmd *Cmd) ensureStopNow() {
   117  	cmd.ensureStop.Lock()
   118  	defer cmd.ensureStop.Unlock()
   119  	if !cmd.ensureStop.off {
   120  		cmd.ensureStop.off = true
   121  		if !cmd.NoEnsureStop {
   122  			if err := KillExecCmd(cmd.Cmd); err != nil {
   123  				// ignoring error: just best effort to stop process
   124  			}
   125  		}
   126  	}
   127  }
   128  
   129  //----------
   130  
   131  func (cmd *Cmd) SetupStdio(ir io.Reader, ow, ew io.Writer) error {
   132  	err := cmd.setupStdio2(ir, ow, ew)
   133  	if err != nil {
   134  		cmd.closeCopyClosers()
   135  	}
   136  	return err
   137  }
   138  
   139  func (cmd *Cmd) setupStdio2(ir io.Reader, ow, ew io.Writer) error {
   140  	// setup only once (don't allow f(w, nil) and later f(nil, w)
   141  	if cmd.setupCalled {
   142  		return fmt.Errorf("setup already called")
   143  	}
   144  	cmd.setupCalled = true
   145  
   146  	// setup stdin
   147  	if ir != nil {
   148  		ipwc, err := cmd.StdinPipe()
   149  		if err != nil {
   150  			return err
   151  		}
   152  		cmd.addCopyCloser(ipwc)
   153  		cmd.copy.fns = append(cmd.copy.fns, func() {
   154  			defer ipwc.Close()
   155  			io.Copy(ipwc, ir)
   156  		})
   157  	}
   158  
   159  	// setup stdout
   160  	cmd.Cmd.Stdout = ow
   161  
   162  	// setup stderr
   163  	cmd.Cmd.Stderr = ew
   164  	return nil
   165  }
   166  
   167  //----------
   168  
   169  func (cmd *Cmd) runCopyFns() {
   170  	for _, fn := range cmd.copy.fns {
   171  		// go fn() // Commented: will call the same fn twice (loop var)
   172  		go func(fn2 func()) {
   173  			fn2()
   174  		}(fn)
   175  	}
   176  }
   177  
   178  //----------
   179  
   180  func (cmd *Cmd) addCopyCloser(c io.Closer) {
   181  	cmd.copy.closers = append(cmd.copy.closers, c)
   182  }
   183  
   184  func (cmd *Cmd) closeCopyClosers() {
   185  	for _, c := range cmd.copy.closers {
   186  		c.Close()
   187  	}
   188  }
   189  
   190  //----------
   191  //----------
   192  //----------
   193  
   194  func RunCmdCombinedOutput(cmd *Cmd, rd io.Reader) ([]byte, error) {
   195  	obuf := &bytes.Buffer{}
   196  	if err := cmd.SetupStdio(rd, obuf, obuf); err != nil {
   197  		return nil, err
   198  	}
   199  	err := cmd.Run()
   200  	return obuf.Bytes(), err
   201  }
   202  
   203  func RunCmdOutputs(cmd *Cmd, rd io.Reader) (sout []byte, serr []byte, _ error) {
   204  	obuf := &bytes.Buffer{}
   205  	ebuf := &bytes.Buffer{}
   206  	if err := cmd.SetupStdio(rd, obuf, ebuf); err != nil {
   207  		return nil, nil, err
   208  	}
   209  	err := cmd.Run()
   210  	return obuf.Bytes(), ebuf.Bytes(), err
   211  }
   212  
   213  // Adds stderr to err if it happens.
   214  func RunCmdStdoutAndStderrInErr(cmd *Cmd, rd io.Reader) ([]byte, error) {
   215  	bout, berr, err := RunCmdOutputs(cmd, rd)
   216  	if err != nil {
   217  		serr := strings.TrimSpace(string(berr))
   218  		if serr != "" {
   219  			err = fmt.Errorf("%w: stderr(%v)", err, serr)
   220  		}
   221  		return nil, err
   222  	}
   223  	return bout, nil
   224  }