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 }