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  }