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,
})
}