github.com/secure-build/gitlab-runner@v12.5.0+incompatible/executors/custom/command/command.go (about)

     1  package command
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"os/exec"
    10  	"strconv"
    11  	"syscall"
    12  	"time"
    13  
    14  	"github.com/sirupsen/logrus"
    15  
    16  	"gitlab.com/gitlab-org/gitlab-runner/common"
    17  	"gitlab.com/gitlab-org/gitlab-runner/executors/custom/api"
    18  	"gitlab.com/gitlab-org/gitlab-runner/executors/custom/process"
    19  )
    20  
    21  const (
    22  	BuildFailureExitCode  = 1
    23  	SystemFailureExitCode = 2
    24  )
    25  
    26  type CreateOptions struct {
    27  	Dir string
    28  	Env []string
    29  
    30  	Stdout io.Writer
    31  	Stderr io.Writer
    32  
    33  	Logger common.BuildLogger
    34  
    35  	GracefulKillTimeout time.Duration
    36  	ForceKillTimeout    time.Duration
    37  }
    38  
    39  type Command interface {
    40  	Run() error
    41  }
    42  
    43  type command struct {
    44  	context context.Context
    45  	cmd     commander
    46  
    47  	waitCh chan error
    48  
    49  	logger common.BuildLogger
    50  
    51  	gracefulKillTimeout time.Duration
    52  	forceKillTimeout    time.Duration
    53  }
    54  
    55  func New(ctx context.Context, executable string, args []string, options CreateOptions) Command {
    56  	defaultVariables := map[string]string{
    57  		"TMPDIR": options.Dir,
    58  		api.BuildFailureExitCodeVariable:  strconv.Itoa(BuildFailureExitCode),
    59  		api.SystemFailureExitCodeVariable: strconv.Itoa(SystemFailureExitCode),
    60  	}
    61  
    62  	env := os.Environ()
    63  	for key, value := range defaultVariables {
    64  		env = append(env, fmt.Sprintf("%s=%s", key, value))
    65  	}
    66  	options.Env = append(env, options.Env...)
    67  
    68  	return &command{
    69  		context:             ctx,
    70  		cmd:                 newCmd(executable, args, options),
    71  		waitCh:              make(chan error),
    72  		logger:              options.Logger,
    73  		gracefulKillTimeout: options.GracefulKillTimeout,
    74  		forceKillTimeout:    options.ForceKillTimeout,
    75  	}
    76  }
    77  
    78  func (c *command) Run() error {
    79  	err := c.cmd.Start()
    80  	if err != nil {
    81  		return fmt.Errorf("failed to start command: %v", err)
    82  	}
    83  
    84  	go c.waitForCommand()
    85  
    86  	select {
    87  	case err = <-c.waitCh:
    88  		return err
    89  
    90  	case <-c.context.Done():
    91  		return c.killAndWait()
    92  	}
    93  }
    94  
    95  var getExitStatus = func(err *exec.ExitError) int {
    96  	// TODO: simplify when we will update to Go 1.12. ExitStatus()
    97  	//       is available there directly from err.Sys().
    98  	return err.Sys().(syscall.WaitStatus).ExitStatus()
    99  }
   100  
   101  func (c *command) waitForCommand() {
   102  	err := c.cmd.Wait()
   103  
   104  	eerr, ok := err.(*exec.ExitError)
   105  	if ok {
   106  		exitCode := getExitStatus(eerr)
   107  		if exitCode == BuildFailureExitCode {
   108  			err = &common.BuildError{Inner: eerr}
   109  		} else if exitCode != SystemFailureExitCode {
   110  			err = &ErrUnknownFailure{Inner: eerr, ExitCode: exitCode}
   111  		}
   112  	}
   113  
   114  	c.waitCh <- err
   115  }
   116  
   117  var newProcessKiller = process.NewKiller
   118  
   119  func (c *command) killAndWait() error {
   120  	if c.cmd.Process() == nil {
   121  		return errors.New("process not started yet")
   122  	}
   123  
   124  	logger := c.logger.WithFields(logrus.Fields{
   125  		"PID": c.cmd.Process().Pid,
   126  	})
   127  
   128  	processKiller := newProcessKiller(logger, c.cmd.Process())
   129  	processKiller.Terminate()
   130  
   131  	select {
   132  	case err := <-c.waitCh:
   133  		return err
   134  
   135  	case <-time.After(c.gracefulKillTimeout):
   136  		processKiller.ForceKill()
   137  
   138  		select {
   139  		case err := <-c.waitCh:
   140  			return err
   141  
   142  		case <-time.After(c.forceKillTimeout):
   143  			return errors.New("failed to kill process, likely process is dormant")
   144  		}
   145  	}
   146  }