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 }