github.com/justinjmoses/evergreen@v0.0.0-20170530173719-1d50e381ff0d/plugin/builtin/shell/sh_plugin.go (about)

     1  package shell
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path/filepath"
     7  
     8  	"github.com/evergreen-ci/evergreen/command"
     9  	"github.com/evergreen-ci/evergreen/model"
    10  	"github.com/evergreen-ci/evergreen/plugin"
    11  	"github.com/mitchellh/mapstructure"
    12  	"github.com/mongodb/grip/slogger"
    13  	"github.com/pkg/errors"
    14  )
    15  
    16  func init() {
    17  	plugin.Publish(&ShellPlugin{})
    18  }
    19  
    20  const (
    21  	ShellPluginName = "shell"
    22  	ShellExecCmd    = "exec"
    23  	CleanupCmd      = "cleanup"
    24  	TrackCmd        = "track"
    25  )
    26  
    27  // ShellPlugin runs arbitrary shell code on the agent's machine.
    28  type ShellPlugin struct{}
    29  
    30  // Name returns the name of the plugin. Required to fulfill
    31  // the Plugin interface.
    32  func (sp *ShellPlugin) Name() string {
    33  	return ShellPluginName
    34  }
    35  
    36  // NewCommand returns the requested command, or returns an error
    37  // if a non-existing command is requested.
    38  func (sp *ShellPlugin) NewCommand(cmdName string) (plugin.Command, error) {
    39  	if cmdName == TrackCmd {
    40  		return &TrackCommand{}, nil
    41  	} else if cmdName == CleanupCmd {
    42  		return &CleanupCommand{}, nil
    43  	} else if cmdName == ShellExecCmd {
    44  		return &ShellExecCommand{}, nil
    45  	}
    46  	return nil, errors.Errorf("no such command: %v", cmdName)
    47  }
    48  
    49  type TrackCommand struct{}
    50  
    51  func (cc *TrackCommand) Name() string {
    52  	return TrackCmd
    53  }
    54  
    55  func (cc *TrackCommand) Plugin() string {
    56  	return ShellPluginName
    57  }
    58  
    59  func (cc *TrackCommand) ParseParams(params map[string]interface{}) error {
    60  	return nil
    61  }
    62  
    63  // Execute starts the shell with its given parameters.
    64  func (cc *TrackCommand) Execute(pluginLogger plugin.Logger,
    65  	pluginCom plugin.PluginCommunicator, conf *model.TaskConfig, stop chan bool) error {
    66  	pluginLogger.LogExecution(slogger.WARN,
    67  		"WARNING: shell.track is deprecated. Process tracking is now enabled by default.")
    68  	return nil
    69  }
    70  
    71  type CleanupCommand struct{}
    72  
    73  func (cc *CleanupCommand) Name() string {
    74  	return CleanupCmd
    75  }
    76  
    77  func (cc *CleanupCommand) Plugin() string {
    78  	return ShellPluginName
    79  }
    80  
    81  // ParseParams reads in the command's parameters.
    82  func (cc *CleanupCommand) ParseParams(params map[string]interface{}) error {
    83  	return nil
    84  }
    85  
    86  // Execute starts the shell with its given parameters.
    87  func (cc *CleanupCommand) Execute(pluginLogger plugin.Logger,
    88  	pluginCom plugin.PluginCommunicator, conf *model.TaskConfig, stop chan bool) error {
    89  	pluginLogger.LogExecution(slogger.WARN,
    90  		"WARNING: shell.cleanup is deprecated. Process cleanup is now enabled by default.")
    91  	return nil
    92  }
    93  
    94  // ShellExecCommand is responsible for running the shell code.
    95  type ShellExecCommand struct {
    96  	// Script is the shell code to be run on the agent machine.
    97  	Script string `mapstructure:"script" plugin:"expand"`
    98  
    99  	// Silent, if set to true, prevents shell code/output from being
   100  	// logged to the agent's task logs. This can be used to avoid
   101  	// exposing sensitive expansion parameters and keys.
   102  	Silent bool `mapstructure:"silent"`
   103  
   104  	// Shell describes the shell to execute the script contents
   105  	// with. Defaults to "sh", but users can customize to
   106  	// explicitly specify another shell.
   107  	Shell string `mapstructure:"shell"`
   108  
   109  	// Background, if set to true, prevents shell code/output from
   110  	// waiting for the script to complete and immediately returns
   111  	// to the caller
   112  	Background bool `mapstructure:"background"`
   113  
   114  	// WorkingDir is the working directory to start the shell in.
   115  	WorkingDir string `mapstructure:"working_dir"`
   116  
   117  	// SystemLog if set will write the shell command's output to the system logs, instead of the
   118  	// task logs. This can be used to collect diagnostic data in the background of a running task.
   119  	SystemLog bool `mapstructure:"system_log"`
   120  
   121  	// ContinueOnError determines whether or not a failed return code
   122  	// should cause the task to be marked as failed. Setting this to true
   123  	// allows following commands to execute even if this shell command fails.
   124  	ContinueOnError bool `mapstructure:"continue_on_err"`
   125  }
   126  
   127  func (_ *ShellExecCommand) Name() string {
   128  	return ShellExecCmd
   129  }
   130  
   131  func (_ *ShellExecCommand) Plugin() string {
   132  	return ShellPluginName
   133  }
   134  
   135  // ParseParams reads in the command's parameters.
   136  func (sec *ShellExecCommand) ParseParams(params map[string]interface{}) error {
   137  	err := mapstructure.Decode(params, sec)
   138  	if err != nil {
   139  		return errors.Wrapf(err, "error decoding %v params", sec.Name())
   140  	}
   141  	return nil
   142  }
   143  
   144  // Execute starts the shell with its given parameters.
   145  func (sec *ShellExecCommand) Execute(pluginLogger plugin.Logger,
   146  	pluginCom plugin.PluginCommunicator,
   147  	conf *model.TaskConfig,
   148  	stop chan bool) error {
   149  	pluginLogger.LogExecution(slogger.DEBUG, "Preparing script...")
   150  
   151  	logWriterInfo := pluginLogger.GetTaskLogWriter(slogger.INFO)
   152  	logWriterErr := pluginLogger.GetTaskLogWriter(slogger.ERROR)
   153  	if sec.SystemLog {
   154  		logWriterInfo = pluginLogger.GetSystemLogWriter(slogger.INFO)
   155  		logWriterErr = pluginLogger.GetSystemLogWriter(slogger.ERROR)
   156  	}
   157  
   158  	localCmd := &command.LocalCommand{
   159  		CmdString:  sec.Script,
   160  		Stdout:     logWriterInfo,
   161  		Stderr:     logWriterErr,
   162  		ScriptMode: true,
   163  	}
   164  
   165  	if sec.WorkingDir != "" {
   166  		localCmd.WorkingDirectory = filepath.Join(conf.WorkDir, sec.WorkingDir)
   167  	} else {
   168  		localCmd.WorkingDirectory = conf.WorkDir
   169  	}
   170  
   171  	if sec.Shell != "" {
   172  		localCmd.Shell = sec.Shell
   173  	}
   174  
   175  	err := localCmd.PrepToRun(conf.Expansions)
   176  	if err != nil {
   177  		return errors.Wrap(err, "Failed to apply expansions")
   178  	}
   179  	if sec.Silent {
   180  		pluginLogger.LogExecution(slogger.INFO, "Executing script with %s (source hidden)...",
   181  			localCmd.Shell)
   182  	} else {
   183  		pluginLogger.LogExecution(slogger.INFO, "Executing script with %s: %v",
   184  			localCmd.Shell, localCmd.CmdString)
   185  	}
   186  
   187  	doneStatus := make(chan error)
   188  	go func() {
   189  		var err error
   190  		env := os.Environ()
   191  		env = append(env, fmt.Sprintf("EVR_TASK_ID=%v", conf.Task.Id))
   192  		env = append(env, fmt.Sprintf("EVR_AGENT_PID=%v", os.Getpid()))
   193  		localCmd.Environment = env
   194  		err = localCmd.Start()
   195  		if err == nil {
   196  			pluginLogger.LogSystem(slogger.DEBUG, "spawned shell process with pid %v", localCmd.Cmd.Process.Pid)
   197  
   198  			// Call the platform's process-tracking function. On some OSes this will be a noop,
   199  			// on others this may need to do some additional work to track the process so that
   200  			// it can be cleaned up later.
   201  			trackProcess(conf.Task.Id, localCmd.Cmd.Process.Pid, pluginLogger)
   202  
   203  			if !sec.Background {
   204  				err = localCmd.Cmd.Wait()
   205  			}
   206  		} else {
   207  			pluginLogger.LogSystem(slogger.DEBUG, "error spawning shell process: %v", err)
   208  		}
   209  		doneStatus <- err
   210  	}()
   211  
   212  	defer pluginLogger.Flush()
   213  	select {
   214  	case err = <-doneStatus:
   215  		if err != nil {
   216  			if sec.ContinueOnError {
   217  				pluginLogger.LogExecution(slogger.INFO, "(ignoring) Script finished with error: %v", err)
   218  				return nil
   219  			} else {
   220  				pluginLogger.LogExecution(slogger.INFO, "Script finished with error: %v", err)
   221  				return err
   222  			}
   223  		} else {
   224  			pluginLogger.LogExecution(slogger.INFO, "Script execution complete.")
   225  		}
   226  	case <-stop:
   227  		pluginLogger.LogExecution(slogger.INFO, "Got kill signal")
   228  
   229  		// need to check command has started
   230  		if localCmd.Cmd != nil {
   231  			pluginLogger.LogExecution(slogger.INFO, "Stopping process: %v", localCmd.Cmd.Process.Pid)
   232  
   233  			// try and stop the process
   234  			if err := localCmd.Stop(); err != nil {
   235  				pluginLogger.LogExecution(slogger.ERROR, "Error occurred stopping process: %v", err)
   236  			}
   237  		}
   238  
   239  		return errors.New("Shell command interrupted.")
   240  	}
   241  
   242  	return nil
   243  }
   244  
   245  // envHasMarkers returns a bool indicating if both marker vars are found in an environment var list
   246  func envHasMarkers(env []string, pidMarker, taskMarker string) bool {
   247  	hasPidMarker := false
   248  	hasTaskMarker := false
   249  	for _, envVar := range env {
   250  		if envVar == pidMarker {
   251  			hasPidMarker = true
   252  		}
   253  		if envVar == taskMarker {
   254  			hasTaskMarker = true
   255  		}
   256  	}
   257  	return hasPidMarker && hasTaskMarker
   258  }
   259  
   260  // KillSpawnedProcs cleans up any tasks that were spawned by the given task.
   261  func KillSpawnedProcs(taskId string, pluginLogger plugin.Logger) error {
   262  	// Clean up all shell processes spawned during the execution of this task by this agent,
   263  	// by calling the platform-specific "cleanup" function
   264  	return cleanup(taskId, pluginLogger)
   265  }