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 }