in agent/executers/executers.go [255:390]
func ExecuteCommand(
context context.T,
cancelFlag task.CancelFlag,
workingDir string,
stdoutWriter io.Writer,
stderrWriter io.Writer,
executionTimeout int,
commandName string,
commandArguments []string,
envVars map[string]string,
) (exitCode int, err error) {
log := context.Log()
stdoutInterruptable, stopStdout := newWriter(stdoutWriter)
stderrInterruptable, stopStderr := newWriter(stderrWriter)
command := exec.Command(commandName, commandArguments...)
command.Dir = workingDir
exitCode = 0
// If we assign the writers directly, the command may never exit even though a command.Process.Wait() does due to https://github.com/golang/go/issues/13155
// However, if we run goroutines to copy from the StdoutPipe and StderrPipe we may lose the last write.
command.Stdout = stdoutInterruptable
command.Stderr = stderrInterruptable
/*
stdoutPipe, err := command.StdoutPipe()
if err != nil {
return 1, err
}
stderrPipe, err := command.StderrPipe()
if err != nil {
return 1, err
}
go io.Copy(stdoutInterruptable, stdoutPipe)
go io.Copy(stderrInterruptable, stderrPipe)
*/
// configure OS-specific process settings
prepareProcess(command)
// configure environment variables
prepareEnvironment(context, command, envVars)
log.Debugf("Running in directory %v, command: %v %v", workingDir, commandName, commandArguments)
quiesce()
if err = command.Start(); err != nil {
log.Error("error occurred starting the command", err)
exitCode = 1
return
}
signal := timeoutSignal{}
cancelled := make(chan bool, 1)
go func() {
cancelState := cancelFlag.Wait()
if cancelFlag.Canceled() {
cancelled <- true
log.Debug("Cancel flag set to cancelled")
}
log.Debugf("Cancel flag set to %v", cancelState)
}()
done := make(chan error, 1)
go func() {
done <- command.Wait()
}()
select {
case <-time.After(time.Duration(executionTimeout) * time.Second):
stopStdout <- true
stopStderr <- true
if err = killProcess(command.Process, &signal); err != nil {
exitCode = 1
log.Error(err)
} else {
// set appropriate exit code based on timeout
exitCode = appconfig.CommandStoppedPreemptivelyExitCode
err = &exec.ExitError{Stderr: []byte("Process timed out")}
log.Infof("The execution of command was timedout.")
}
case <-cancelled:
// task has been asked to cancel, kill process
log.Debug("Process cancelled. Attempting to stop process.")
stopStdout <- true
stopStderr <- true
if err = killProcess(command.Process, &signal); err != nil {
exitCode = 1
log.Error(err)
} else {
// set appropriate exit code based on cancel
exitCode = appconfig.CommandStoppedPreemptivelyExitCode
err = &exec.ExitError{Stderr: []byte("Cancelled process")}
log.Infof("The execution of command was cancelled.")
}
case err = <-done:
log.Debug("Process completed.")
if err != nil {
exitCode = 1
log.Debugf("command returned error %v", err)
if exiterr, ok := err.(*exec.ExitError); ok {
// The program has exited with an exit code != 0
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
exitCode = status.ExitStatus()
if signal.execInterruptedOnWindows {
log.Debug("command interrupted by cancel or timeout")
exitCode = -1
}
// First try to handle Cancel and Timeout scenarios
// SIGKILL will result in an exitcode of -1
if exitCode == -1 {
if cancelFlag.Canceled() {
// set appropriate exit code based on cancel or timeout
exitCode = appconfig.CommandStoppedPreemptivelyExitCode
log.Infof("The execution of command was cancelled.")
}
} else {
log.Infof("The execution of command returned Exit Status: %d", exitCode)
}
}
}
} else {
// check if cancellation or timeout failed to kill the process
// This will not occur as we do a SIGKILL, which is not recoverable.
if cancelFlag.Canceled() {
// This is when the cancellation failed and the command completed successfully
log.Errorf("the cancellation failed to stop the process.")
// do not return as the command could have been cancelled and also timedout
}
}
}
return
}