executors/shell/shell.go (153 lines of code) (raw):

package shell import ( "errors" "fmt" "io" "os" "os/exec" "path/filepath" "runtime" "strings" "github.com/sirupsen/logrus" "gitlab.com/gitlab-org/gitlab-runner/common" "gitlab.com/gitlab-org/gitlab-runner/common/buildlogger" "gitlab.com/gitlab-org/gitlab-runner/executors" "gitlab.com/gitlab-org/gitlab-runner/helpers/featureflags" "gitlab.com/gitlab-org/gitlab-runner/helpers/process" ) var newProcessKillWaiter = process.NewOSKillWait var newCommander = process.NewOSCmd type executor struct { executors.AbstractExecutor } func (s *executor) Prepare(options common.ExecutorPrepareOptions) error { if options.User != "" { s.Shell().User = options.User } // expand environment variables to have current directory wd, err := os.Getwd() if err != nil { return fmt.Errorf("getwd: %w", err) } mapping := func(key string) string { switch key { case "PWD": return wd default: return "" } } s.DefaultBuildsDir = os.Expand(s.DefaultBuildsDir, mapping) s.DefaultCacheDir = os.Expand(s.DefaultCacheDir, mapping) // Pass control to executor err = s.AbstractExecutor.Prepare(options) if err != nil { return err } s.BuildLogger.Println("Using Shell (" + s.Shell().Shell + ") executor...") return nil } func (s *executor) Run(cmd common.ExecutorCommand) error { s.BuildLogger.Debugln("Using new shell command execution") stdout := s.BuildLogger.Stream(buildlogger.StreamWorkLevel, buildlogger.Stdout) defer stdout.Close() stderr := s.BuildLogger.Stream(buildlogger.StreamWorkLevel, buildlogger.Stderr) defer stderr.Close() cmdOpts := process.CommandOptions{ Env: os.Environ(), Stdout: stdout, Stderr: stderr, UseWindowsLegacyProcessStrategy: s.Build.IsFeatureFlagOn(featureflags.UseWindowsLegacyProcessStrategy), UseWindowsJobObject: s.Build.IsFeatureFlagOn(featureflags.UseWindowsJobObject), } args := s.BuildShell.Arguments stdin, args, cleanup, err := s.shellScriptArgs(cmd, args) if err != nil { return err } defer cleanup() cmdOpts.Stdin = stdin // Create execution command c := newCommander(s.BuildShell.Command, args, cmdOpts) // Start a process err = c.Start() if err != nil { return fmt.Errorf("failed to start process: %w", err) } // Wait for process to finish waitCh := make(chan error, 1) go func() { waitErr := c.Wait() var exitErr *exec.ExitError if errors.As(waitErr, &exitErr) { waitErr = &common.BuildError{Inner: waitErr, ExitCode: exitErr.ExitCode()} } waitCh <- waitErr }() // Support process abort select { case err = <-waitCh: return err case <-cmd.Context.Done(): logger := common.NewProcessLoggerAdapter(s.BuildLogger) return newProcessKillWaiter(logger, s.Config.GetGracefulKillTimeout(), s.Config.GetForceKillTimeout()). KillAndWait(c, waitCh) } } func (s *executor) shellScriptArgs(cmd common.ExecutorCommand, args []string) (io.Reader, []string, func(), error) { if !s.BuildShell.PassFile { return strings.NewReader(cmd.Script), args, func() {}, nil } scriptDir, err := os.MkdirTemp("", "build_script") if err != nil { return nil, nil, func() {}, fmt.Errorf("creating tmp build script dir: %w", err) } cleanup := func() { err := os.RemoveAll(scriptDir) if err != nil { s.BuildLogger.Warningln("Failed to remove build script directory", scriptDir, err) } } scriptFile := filepath.Join(scriptDir, "script."+s.BuildShell.Extension) err = os.WriteFile(scriptFile, []byte(cmd.Script), 0o700) if err != nil { return nil, nil, cleanup, fmt.Errorf("writing script file: %w", err) } return nil, append(args, scriptFile), cleanup, nil } func init() { // Look for self runnerCommand, err := os.Executable() if err != nil { logrus.Warningln(err) } RegisterExecutor("shell", runnerCommand) } func RegisterExecutor(executorName string, runnerCommandPath string) { options := executors.ExecutorOptions{ DefaultCustomBuildsDirEnabled: false, DefaultSafeDirectoryCheckout: false, DefaultBuildsDir: "$PWD/builds", DefaultCacheDir: "$PWD/cache", SharedBuildsDir: true, Shell: common.ShellScriptInfo{ Shell: common.GetDefaultShell(), Type: common.LoginShell, RunnerCommand: runnerCommandPath, }, ShowHostname: false, } creator := func() common.Executor { return &executor{ AbstractExecutor: executors.AbstractExecutor{ ExecutorOptions: options, }, } } featuresUpdater := func(features *common.FeaturesInfo) { features.Variables = true features.Shared = true if runtime.GOOS != "windows" { features.Session = true features.Terminal = true } } common.RegisterExecutorProvider(executorName, executors.DefaultExecutorProvider{ Creator: creator, FeaturesUpdater: featuresUpdater, DefaultShellName: options.Shell.Shell, }) }