github.com/bshelton229/agent@v3.5.4+incompatible/process/process.go (about) 1 package process 2 3 // Logic for this file is largely based on: 4 // https://github.com/jarib/childprocess/blob/783f7a00a1678b5d929062564ef5ae76822dfd62/lib/childprocess/unix/process.rb 5 6 import ( 7 "bufio" 8 "bytes" 9 "errors" 10 "fmt" 11 "io" 12 "os" 13 "os/exec" 14 "regexp" 15 "runtime" 16 "strconv" 17 "sync" 18 "sync/atomic" 19 "syscall" 20 "time" 21 22 "github.com/buildkite/agent/logger" 23 ) 24 25 type Process struct { 26 Pid int 27 PTY bool 28 Timestamp bool 29 Script []string 30 Env []string 31 ExitStatus string 32 33 buffer outputBuffer 34 command *exec.Cmd 35 36 // This callback is called when the process offically starts 37 StartCallback func() 38 39 // For every line in the process output, this callback will be called 40 // with the contents of the line if its filter returns true. 41 LineCallback func(string) 42 LinePreProcessor func(string) string 43 LineCallbackFilter func(string) bool 44 45 // Running is stored as an int32 so we can use atomic operations to 46 // set/get it (it's accessed by multiple goroutines) 47 running int32 48 } 49 50 // If you change header parsing here make sure to change it in the 51 // buildkite.com frontend logic, too 52 53 var headerExpansionRegex = regexp.MustCompile("^(?:\\^\\^\\^\\s+\\+\\+\\+)\\s*$") 54 55 func (p *Process) Start() error { 56 p.command = exec.Command(p.Script[0], p.Script[1:]...) 57 58 // Copy the current processes ENV and merge in the new ones. We do this 59 // so the sub process gets PATH and stuff. We merge our path in over 60 // the top of the current one so the ENV from Buildkite and the agent 61 // take precedence over the agent 62 currentEnv := os.Environ() 63 p.command.Env = append(currentEnv, p.Env...) 64 65 var waitGroup sync.WaitGroup 66 67 lineReaderPipe, lineWriterPipe := io.Pipe() 68 69 var multiWriter io.Writer 70 if p.Timestamp { 71 multiWriter = io.MultiWriter(lineWriterPipe) 72 } else { 73 multiWriter = io.MultiWriter(&p.buffer, lineWriterPipe) 74 } 75 76 // Toggle between running in a pty 77 if p.PTY { 78 pty, err := StartPTY(p.command) 79 if err != nil { 80 p.ExitStatus = "1" 81 return err 82 } 83 84 p.Pid = p.command.Process.Pid 85 p.setRunning(true) 86 87 waitGroup.Add(1) 88 89 go func() { 90 logger.Debug("[Process] Starting to copy PTY to the buffer") 91 92 // Copy the pty to our buffer. This will block until it 93 // EOF's or something breaks. 94 _, err = io.Copy(multiWriter, pty) 95 if e, ok := err.(*os.PathError); ok && e.Err == syscall.EIO { 96 // We can safely ignore this error, because 97 // it's just the PTY telling us that it closed 98 // successfully. See: 99 // https://github.com/buildkite/agent/pull/34#issuecomment-46080419 100 err = nil 101 } 102 103 if err != nil { 104 logger.Error("[Process] PTY output copy failed with error: %T: %v", err, err) 105 } else { 106 logger.Debug("[Process] PTY has finished being copied to the buffer") 107 } 108 109 waitGroup.Done() 110 }() 111 } else { 112 p.command.Stdout = multiWriter 113 p.command.Stderr = multiWriter 114 p.command.Stdin = nil 115 116 err := p.command.Start() 117 if err != nil { 118 p.ExitStatus = "1" 119 return err 120 } 121 122 p.Pid = p.command.Process.Pid 123 p.setRunning(true) 124 } 125 126 logger.Info("[Process] Process is running with PID: %d", p.Pid) 127 128 // Add the line callback routine to the waitGroup 129 waitGroup.Add(1) 130 131 go func() { 132 logger.Debug("[LineScanner] Starting to read lines") 133 134 reader := bufio.NewReader(lineReaderPipe) 135 136 var appending []byte 137 var lineCallbackWaitGroup sync.WaitGroup 138 139 for { 140 line, isPrefix, err := reader.ReadLine() 141 if err != nil { 142 if err == io.EOF { 143 logger.Debug("[LineScanner] Encountered EOF") 144 break 145 } 146 147 logger.Error("[LineScanner] Failed to read: (%T: %v)", err, err) 148 } 149 150 // If isPrefix is true, that means we've got a really 151 // long line incoming, and we'll keep appending to it 152 // until isPrefix is false (which means the long line 153 // has ended. 154 if isPrefix && appending == nil { 155 logger.Debug("[LineScanner] Line is too long to read, going to buffer it until it finishes") 156 // bufio.ReadLine returns a slice which is only valid until the next invocation 157 // since it points to its own internal buffer array. To accumulate the entire 158 // result we make a copy of the first prefix, and insure there is spare capacity 159 // for future appends to minimize the need for resizing on append. 160 appending = make([]byte, len(line), (cap(line))*2) 161 copy(appending, line) 162 163 continue 164 } 165 166 // Should we be appending? 167 if appending != nil { 168 appending = append(appending, line...) 169 170 // No more isPrefix! Line is finished! 171 if !isPrefix { 172 logger.Debug("[LineScanner] Finished buffering long line") 173 line = appending 174 175 // Reset appending back to nil 176 appending = nil 177 } else { 178 continue 179 } 180 } 181 182 // If we're timestamping this main thread will take 183 // the hit of running the regex so we can build up 184 // the timestamped buffer without breaking headers, 185 // otherwise we let the goroutines take the perf hit. 186 187 checkedForCallback := false 188 lineHasCallback := false 189 lineString := p.LinePreProcessor(string(line)) 190 191 // Create the prefixed buffer 192 if p.Timestamp { 193 lineHasCallback = p.LineCallbackFilter(lineString) 194 checkedForCallback = true 195 if lineHasCallback || headerExpansionRegex.MatchString(lineString) { 196 // Don't timestamp special lines (e.g. header) 197 p.buffer.WriteString(fmt.Sprintf("%s\n", line)) 198 } else { 199 currentTime := time.Now().UTC().Format(time.RFC3339) 200 p.buffer.WriteString(fmt.Sprintf("[%s] %s\n", currentTime, line)) 201 } 202 } 203 204 if lineHasCallback || !checkedForCallback { 205 lineCallbackWaitGroup.Add(1) 206 go func(line string) { 207 defer lineCallbackWaitGroup.Done() 208 if (checkedForCallback && lineHasCallback) || p.LineCallbackFilter(lineString) { 209 p.LineCallback(line) 210 } 211 }(lineString) 212 } 213 } 214 215 // We need to make sure all the line callbacks have finish before 216 // finish up the process 217 logger.Debug("[LineScanner] Waiting for callbacks to finish") 218 lineCallbackWaitGroup.Wait() 219 220 logger.Debug("[LineScanner] Finished") 221 waitGroup.Done() 222 }() 223 224 // Call the StartCallback 225 go p.StartCallback() 226 227 // Wait until the process has finished. The returned error is nil if the command runs, 228 // has no problems copying stdin, stdout, and stderr, and exits with a zero exit status. 229 waitResult := p.command.Wait() 230 231 // Close the line writer pipe 232 lineWriterPipe.Close() 233 234 // The process is no longer running at this point 235 p.setRunning(false) 236 237 // Find the exit status of the script 238 p.ExitStatus = getExitStatus(waitResult) 239 240 logger.Info("Process with PID: %d finished with Exit Status: %s", p.Pid, p.ExitStatus) 241 242 // Sometimes (in docker containers) io.Copy never seems to finish. This is a mega 243 // hack around it. If it doesn't finish after 1 second, just continue. 244 logger.Debug("[Process] Waiting for routines to finish") 245 err := timeoutWait(&waitGroup) 246 if err != nil { 247 logger.Debug("[Process] Timed out waiting for wait group: (%T: %v)", err, err) 248 } 249 250 // No error occurred so we can return nil 251 return nil 252 } 253 254 func (p *Process) Output() string { 255 return p.buffer.String() 256 } 257 258 func (p *Process) Kill() error { 259 var err error 260 if runtime.GOOS == "windows" { 261 // Sending Interrupt on Windows is not implemented. 262 // https://golang.org/src/os/exec.go?s=3842:3884#L110 263 err = exec.Command("CMD", "/C", "TASKKILL", "/F", "/T", "/PID", strconv.Itoa(p.Pid)).Run() 264 } else { 265 // Send a sigterm 266 err = p.signal(syscall.SIGTERM) 267 } 268 if err != nil { 269 return err 270 } 271 272 // Make a channel that we'll use as a timeout 273 c := make(chan int, 1) 274 checking := true 275 276 // Start a routine that checks to see if the process 277 // is still alive. 278 go func() { 279 for checking { 280 logger.Debug("[Process] Checking to see if PID: %d is still alive", p.Pid) 281 282 foundProcess, err := os.FindProcess(p.Pid) 283 284 // Can't find the process at all 285 if err != nil { 286 logger.Debug("[Process] Could not find process with PID: %d", p.Pid) 287 288 break 289 } 290 291 // We have some information about the process 292 if foundProcess != nil { 293 processState, err := foundProcess.Wait() 294 295 if err != nil || processState.Exited() { 296 logger.Debug("[Process] Process with PID: %d has exited.", p.Pid) 297 298 break 299 } 300 } 301 302 // Retry in a moment 303 sleepTime := time.Duration(1 * time.Second) 304 time.Sleep(sleepTime) 305 } 306 307 c <- 1 308 }() 309 310 // Timeout this process after 3 seconds 311 select { 312 case _ = <-c: 313 // Was successfully terminated 314 case <-time.After(10 * time.Second): 315 // Stop checking in the routine above 316 checking = false 317 318 // Forcefully kill the thing 319 err = p.signal(syscall.SIGKILL) 320 321 if err != nil { 322 return err 323 } 324 } 325 326 return nil 327 } 328 329 func (p *Process) signal(sig os.Signal) error { 330 if p.command != nil && p.command.Process != nil { 331 logger.Debug("[Process] Sending signal: %s to PID: %d", sig.String(), p.Pid) 332 333 err := p.command.Process.Signal(sig) 334 if err != nil { 335 logger.Error("[Process] Failed to send signal: %s to PID: %d (%T: %v)", sig.String(), p.Pid, err, err) 336 return err 337 } 338 } else { 339 logger.Debug("[Process] No process to signal yet") 340 } 341 342 return nil 343 } 344 345 // Returns whether or not the process is running 346 func (p *Process) IsRunning() bool { 347 return atomic.LoadInt32(&p.running) != 0 348 } 349 350 // Sets the running flag of the process 351 func (p *Process) setRunning(r bool) { 352 // Use the atomic package to avoid race conditions when setting the 353 // `running` value from multiple routines 354 if r { 355 atomic.StoreInt32(&p.running, 1) 356 } else { 357 atomic.StoreInt32(&p.running, 0) 358 } 359 } 360 361 // https://github.com/hnakamur/commango/blob/fe42b1cf82bf536ce7e24dceaef6656002e03743/os/executil/executil.go#L29 362 // TODO: Can this be better? 363 func getExitStatus(waitResult error) string { 364 exitStatus := -1 365 366 if waitResult != nil { 367 if err, ok := waitResult.(*exec.ExitError); ok { 368 if s, ok := err.Sys().(syscall.WaitStatus); ok { 369 exitStatus = s.ExitStatus() 370 } else { 371 logger.Error("[Process] Unimplemented for system where exec.ExitError.Sys() is not syscall.WaitStatus.") 372 } 373 } else { 374 logger.Error("[Process] Unexpected error type in getExitStatus: %#v", waitResult) 375 } 376 } else { 377 exitStatus = 0 378 } 379 380 return fmt.Sprintf("%d", exitStatus) 381 } 382 383 func timeoutWait(waitGroup *sync.WaitGroup) error { 384 // Make a chanel that we'll use as a timeout 385 c := make(chan int, 1) 386 387 // Start waiting for the routines to finish 388 go func() { 389 waitGroup.Wait() 390 c <- 1 391 }() 392 393 select { 394 case _ = <-c: 395 return nil 396 case <-time.After(10 * time.Second): 397 return errors.New("Timeout") 398 } 399 400 return nil 401 } 402 403 // outputBuffer is a goroutine safe bytes.Buffer 404 type outputBuffer struct { 405 sync.RWMutex 406 buf bytes.Buffer 407 } 408 409 // Write appends the contents of p to the buffer, growing the buffer as needed. It returns 410 // the number of bytes written. 411 func (ob *outputBuffer) Write(p []byte) (n int, err error) { 412 ob.Lock() 413 defer ob.Unlock() 414 return ob.buf.Write(p) 415 } 416 417 // WriteString appends the contents of s to the buffer, growing the buffer as needed. It returns 418 // the number of bytes written. 419 func (ob *outputBuffer) WriteString(s string) (n int, err error) { 420 return ob.Write([]byte(s)) 421 } 422 423 // String returns the contents of the unread portion of the buffer 424 // as a string. If the Buffer is a nil pointer, it returns "<nil>". 425 func (ob *outputBuffer) String() string { 426 ob.RLock() 427 defer ob.RUnlock() 428 return ob.buf.String() 429 }