// Execute runs multiple sets of commands and returns their outputs. // res.Output will contain a slice of RunCommandPluginOutput. func (p *Plugin) Execute(context context.T, config contracts.Configuration, cancelFlag task.CancelFlag) (res contracts.PluginResult) { log := context.Log() log.Info("RunCommand started with configuration ", config) util := new(updateutil.Utility) manager := new(updateManager) res.StartDateTime = time.Now() defer func() { res.EndDateTime = time.Now() }() //loading Properties as list since aws:updateSsmAgent uses properties as list var properties []interface{} if properties, res = pluginutil.LoadParametersAsList(log, config.Properties); res.Code != 0 { return res } out := make([]UpdatePluginOutput, len(properties)) for i, prop := range properties { // check if a reboot has been requested if rebooter.RebootRequested() { log.Info("A plugin has requested a reboot.") break } if cancelFlag.ShutDown() { out[i] = UpdatePluginOutput{} out[i].Errors = []string{"Execution canceled due to ShutDown"} break } else if cancelFlag.Canceled() { out[i] = UpdatePluginOutput{} out[i].Errors = []string{"Execution canceled"} break } out[i] = updateAgent(p, config, log, manager, util, prop, cancelFlag, config.OutputS3BucketName, config.OutputS3KeyPrefix, res.StartDateTime) res.Code = out[i].ExitCode res.Status = out[i].Status res.Output = fmt.Sprintf("%v", out[i].String()) } return }
// Execute runs multiple sets of commands and returns their outputs. // res.Output will contain a slice of RunCommandPluginOutput. func (p *Plugin) Execute(context context.T, config contracts.Configuration, cancelFlag task.CancelFlag) (res contracts.PluginResult) { log := context.Log() log.Infof("%v started with configuration %v", Name(), config) res.StartDateTime = time.Now() defer func() { res.EndDateTime = time.Now() }() //loading Properties as list since aws:runPowershellScript & aws:runShellScript uses properties as list var properties []interface{} if properties, res = pluginutil.LoadParametersAsList(log, config.Properties); res.Code != 0 { pluginutil.PersistPluginInformationToCurrent(log, Name(), config, res) return res } out := make([]contracts.PluginOutput, len(properties)) for i, prop := range properties { // check if a reboot has been requested if rebooter.RebootRequested() { log.Info("A plugin has requested a reboot.") return } if cancelFlag.ShutDown() { out[i] = contracts.PluginOutput{Errors: []string{"Execution canceled due to ShutDown"}} out[i].ExitCode = 1 out[i].Status = contracts.ResultStatusFailed break } if cancelFlag.Canceled() { out[i] = contracts.PluginOutput{Errors: []string{"Execution canceled"}} out[i].ExitCode = 1 out[i].Status = contracts.ResultStatusCancelled break } out[i] = p.runCommandsRawInput(log, prop, config.OrchestrationDirectory, cancelFlag, config.OutputS3BucketName, config.OutputS3KeyPrefix) } // TODO: instance here we have to do more result processing, where individual sub properties results are merged smartly into plugin response. // Currently assuming we have only one work. if len(properties) > 0 { res.Code = out[0].ExitCode res.Status = out[0].Status res.Output = out[0].String() } pluginutil.PersistPluginInformationToCurrent(log, Name(), config, res) return res }
// killProcessOnCancel waits for a cancel request. // If a cancel request is received, this method kills the underlying // process of the command. This will unblock the command.Wait() call. // If the task completed successfully this method returns with no action. func killProcessOnCancel(log log.T, command *exec.Cmd, cancelFlag task.CancelFlag) { cancelFlag.Wait() if cancelFlag.Canceled() { log.Debug("Process cancelled. Attempting to stop process.") // task has been asked to cancel, kill process if err := killProcess(command.Process); err != nil { log.Error(err) return } log.Debug("Process stopped successfully.") } }
// setMsiExecStatus sets the exit status and output to be returned to the user based on exit code func setMsiExecStatus(log log.T, pluginInput ApplicationPluginInput, cancelFlag task.CancelFlag, out *ApplicationPluginOutput) { out.Stdout = pluginInput.Source out.Stderr = "" out.Status = contracts.ResultStatusFailed isUnKnownError := false switch out.ExitCode { case appconfig.SuccessExitCode: out.Status = contracts.ResultStatusSuccess case ErrorUnknownProduct: if pluginInput.Action == UNINSTALL { // Uninstall will skip, if product is not currently installed. // This is needed to support idempotent behavior. out.Status = contracts.ResultStatusSuccess } case ErrorSuccessRebootInitiated: fallthrough case appconfig.RebootExitCode: out.Status = contracts.ResultStatusSuccessAndReboot case pluginutil.CommandStoppedPreemptivelyExitCode: if cancelFlag.ShutDown() { out.Status = contracts.ResultStatusFailed } if cancelFlag.Canceled() { out.Status = contracts.ResultStatusCancelled } out.Status = contracts.ResultStatusTimedOut default: isUnKnownError = true } if isUnKnownError { // Note: Sample Stderr: // Action:{Installed}; Status:{Failed}; // ErrorCode:{1620}; ErrorMsg:{ERROR_INSTALL_PACKAGE_INVALID}; // Description:{This installation package could not be opened. Contact the application vendor to verify that this is a valid Windows Installer package.}; // Source:{https:///} // Construct stderr in above format using StandardMsiErrorCodes out.Stderr = fmt.Sprintf("Action:{%v}; Status:{%v}; ErrorCode:{%v}; %v Source:{%v};", pluginInput.Action, out.Status, out.ExitCode, getExitCodeDescription(out.ExitCode), pluginInput.Source) } // Logging msiexec.Result log.Debug("logging stdouts & errors after setting final status for msiexec") log.Debugf("resultCode: %v", out.ExitCode) log.Debugf("stdout: %v", out.Stdout) log.Debugf("stderr: %v", out.Stderr) }
// GetStatus returns a ResultStatus variable based on the received exitCode func GetStatus(exitCode int, cancelFlag task.CancelFlag) contracts.ResultStatus { switch exitCode { case appconfig.SuccessExitCode: return contracts.ResultStatusSuccess case CommandStoppedPreemptivelyExitCode: if cancelFlag.ShutDown() { return contracts.ResultStatusFailed } if cancelFlag.Canceled() { return contracts.ResultStatusCancelled } return contracts.ResultStatusTimedOut default: return contracts.ResultStatusFailed } }
func testCommandInvokerCancel(t *testing.T, invoke CommandInvoker, cancelFlag task.CancelFlag, testCase TestCase) { go func() { time.Sleep(100 * time.Millisecond) cancelFlag.Set(task.Canceled) }() start := time.Now() stdout, stderr, exitCode, errs := invoke(testCase.Commands) duration := time.Since(start) // test that the job returned before the normal time assert.True(t, duration.Seconds() <= cancelWaitTimeoutSeconds, "The command took too long to kill (%v)!", duration) // test that we receive kill exception assert.Equal(t, len(errs), 1) assert.IsType(t, &exec.ExitError{}, errs[0]) assertReaderEquals(t, testCase.ExpectedStdout, stdout) assertReaderEquals(t, testCase.ExpectedStderr, stderr) assert.Equal(t, exitCode, testCase.ExpectedExitCode) }
// Execute runs multiple sets of commands and returns their outputs. // res.Output will contain a slice of PluginOutput. func (p *Plugin) Execute(context context.T, config contracts.Configuration, cancelFlag task.CancelFlag) (res contracts.PluginResult) { log := context.Log() log.Infof("%v started with configuration %v", Name(), config) res.StartDateTime = time.Now() defer func() { res.EndDateTime = time.Now() }() //loading Properties as list since aws:applications uses properties as list var properties []interface{} if properties, res = pluginutil.LoadParametersAsList(log, config.Properties); res.Code != 0 { pluginutil.PersistPluginInformationToCurrent(log, Name(), config, res) return res } msiFailureCount := 0 atleastOneRequestedReboot := false finalStdOut := "" finalStdErr := "" out := make([]ApplicationPluginOutput, len(properties)) for i, prop := range properties { // check if a reboot has been requested if rebooter.RebootRequested() { log.Infof("Stopping execution of %v plugin due to an external reboot request.", Name()) return } if cancelFlag.ShutDown() { res.Code = 1 res.Status = contracts.ResultStatusFailed pluginutil.PersistPluginInformationToCurrent(log, Name(), config, res) return } if cancelFlag.Canceled() { res.Code = 1 res.Status = contracts.ResultStatusCancelled pluginutil.PersistPluginInformationToCurrent(log, Name(), config, res) return } out[i] = p.runCommandsRawInput(log, prop, config.OrchestrationDirectory, cancelFlag, config.OutputS3BucketName, config.OutputS3KeyPrefix) if out[i].Status == contracts.ResultStatusFailed { msiFailureCount++ if out[i].Stdout != "" { finalStdOut = fmt.Sprintf("%v\n%v", finalStdOut, out[i].Stdout) } if out[i].Stderr != "" { finalStdErr = fmt.Sprintf("%v\n%v", finalStdErr, out[i].Stderr) } } if out[i].Status == contracts.ResultStatusSuccessAndReboot { atleastOneRequestedReboot = true res.Code = out[i].ExitCode } } if atleastOneRequestedReboot { res.Status = contracts.ResultStatusSuccessAndReboot } else { res.Status = contracts.ResultStatusSuccess res.Code = 0 } if msiFailureCount > 0 { finalStdOut = fmt.Sprintf("Number of Failures: %v\n%v", msiFailureCount, finalStdOut) res.Status = contracts.ResultStatusFailed res.Code = 1 } finalOut := contracts.PluginOutput{ Stdout: finalStdOut, Stderr: finalStdErr, } res.Output = finalOut.String() pluginutil.PersistPluginInformationToCurrent(log, Name(), config, res) return res }
// RunCommand runs the given commands using the given working directory. // Standard output and standard error are sent to the given writers. func RunCommand(log log.T, cancelFlag task.CancelFlag, workingDir string, stdoutWriter io.Writer, stderrWriter io.Writer, executionTimeout int, commandName string, commandArguments []string, ) (exitCode int, err error) { command := exec.Command(commandName, commandArguments...) command.Dir = workingDir command.Stdout = stdoutWriter command.Stderr = stderrWriter exitCode = 0 // configure OS-specific process settings prepareProcess(command) log.Debug() log.Debugf("Running in directory %v, command: %v %v.", workingDir, commandName, commandArguments) log.Debug() if err = command.Start(); err != nil { log.Error("error occurred starting the command", err) exitCode = 1 return } go killProcessOnCancel(log, command, cancelFlag) timer := time.NewTimer(time.Duration(executionTimeout) * time.Second) go killProcessOnTimeout(log, command, timer) err = command.Wait() timedOut := !timer.Stop() // returns false if called previously - indicates timedOut. if err != nil { exitCode = 1 log.Debugf("command failed to run %v", err) if exiterr, ok := err.(*exec.ExitError); ok { // The program has exited with an exit code != 0 if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { exitCode = status.ExitStatus() // First try to handle Cancel and Timeout scenarios // SIGKILL will result in an exitcode of -1 if exitCode == -1 { if cancelFlag.Canceled() { // set appropriate exit code based on cancel or timeout exitCode = pluginutil.CommandStoppedPreemptivelyExitCode log.Infof("The execution of command was cancelled.") } else if timedOut { // set appropriate exit code based on cancel or timeout exitCode = pluginutil.CommandStoppedPreemptivelyExitCode log.Infof("The execution of command was timedout.") } } else { log.Infof("The execution of command returned Exit Status: %d", exitCode) } } } } else { // check if cancellation or timeout failed to kill the process // This will not occur as we do a SIGKILL, which is not recoverable. if cancelFlag.Canceled() { // This is when the cancellation failed and the command completed successfully log.Errorf("the cancellation failed to stop the process.") // do not return as the command could have been cancelled and also timedout } if timedOut { // This is when the timeout failed and the command completed successfully log.Errorf("the timeout failed to stop the process.") } } log.Debug("Done waiting!") return }