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 }