//go:build windows // +build windows // Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"). You may not // use this file except in compliance with the License. A copy of the // License is located at // // http://aws.amazon.com/apache2.0/ // // or in the "license" file accompanying this file. This file is distributed // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, // either express or implied. See the License for the specific language governing // permissions and limitations under the License. // // Package rundaemon implements rundaemon plugin and its configuration package rundaemon import ( "os" "os/exec" "runtime/debug" "strconv" "strings" "sync" "time" "github.com/aws/amazon-ssm-agent/agent/context" "github.com/aws/amazon-ssm-agent/agent/framework/processor/executer/iohandler" "github.com/aws/amazon-ssm-agent/agent/longrunning/jobobject" "github.com/aws/amazon-ssm-agent/agent/task" ) // Created Executor function interfaces to allow for better testability var DaemonCmdExecutor = RunDaemon var BlockWhileDaemonRunningExecutor = BlockWhileDaemonRunning var StopDaemonExecutor = StopDaemon var IsDaemonRunningExecutor = IsDaemonRunning var StartDaemonHelperExecutor = StartDaemonHelper // RequestedDaemonStateType represents whether the user has explicitly requested to start/stop the daemon type RequestedDaemonStateType uint const ( RequestedDisabled RequestedDaemonStateType = iota RequestedEnabled ) // CurrentDaemonStateType represents whether the daemon is currently running or not. type CurrentDaemonStateType uint const ( CurrentStopped CurrentDaemonStateType = iota CurrentRunning ) // Plugin is the type for the configureDaemon plugin. type Plugin struct { iohandler.PluginConfig // Context is the agent context for config, identity and logger Context context.T // ExeLocation is the directory for a particular daemon package ExeLocation string // Name is name of the daemon Name string // CommandLine is command line to launch the daemon (On Windows, ame of executable or a powershell script) CommandLine string Process *os.Process //ProcessStateLock lock is used to Protect access to daemon state updates ProcessStateLock sync.Mutex // RequestedDaemonState represents whether the user has explicitly requested to start/stop the daemon RequestedDaemonState RequestedDaemonStateType // 1 = Start. 0 = Stop // CurrentDaemonState represents whether the daemon is currently running or not. CurrentDaemonState CurrentDaemonStateType // 1 = Running, 0 = Stopped } // MinWaitBetweenRetries 60seconds // Successive Daemon Restarts should be atleast 60sec apart const MinWaitBetweenRetries = 60 * time.Second // MaxThresholdToResetRetryCounter 5 hours // If the daemon process exits happens after this specified threshold, then // the Retrycount is set back to 0. const MaxTimeThresholdToResetRetryCounter = 18000 * time.Second // MaxRetryCountDuringFailures const MaxRetryCountDuringFailures = 10 // BlockWhileDaemonRunning checks if the process with the given process id is still running // The function will block and the context swapped out while the underlying process is still running. func BlockWhileDaemonRunning(context context.T, pid int) error { log := context.Log() process, err := os.FindProcess(pid) if err != nil { log.Infof("Daemon Not Running. Pid %v : %s", pid, err.Error()) return err } log.Infof("Waiting for the process to die") // Control blocks here until this process stops running (gets killed for example) _, err = process.Wait() return err } // IsRunning returns if the said plugin is running or not, to the long running plugin manager. // We always return false here since the lifecycle of the underlying daemon is anyways being controlled here func (p *Plugin) IsRunning() bool { return false } // This function sets the flag to indicate that daemon stop has been requested via the StopPlugin call. func (p *Plugin) stopRequested() bool { p.ProcessStateLock.Lock() defer p.ProcessStateLock.Unlock() return p.RequestedDaemonState == RequestedDisabled } // This function sets the daemon current state to being stopped. func (p *Plugin) SetDaemonStateStopped() { p.ProcessStateLock.Lock() defer p.ProcessStateLock.Unlock() p.CurrentDaemonState = CurrentStopped } // Function RunDaemon invokes exec.Cmd.Start with appropriate arguments. func RunDaemon(daemonInvoke *exec.Cmd) (err error) { err = daemonInvoke.Start() return err } // Function checks if the daemon is currently running or not. func IsDaemonRunning(p *Plugin) bool { p.ProcessStateLock.Lock() defer p.ProcessStateLock.Unlock() return p.CurrentDaemonState == CurrentRunning } // Starts a given executable or a specified powershell script and enables daemon functionality func StartDaemonHelper(p *Plugin, configuration string) (err error) { log := p.Context.Log() if IsDaemonRunningExecutor(p) { log.Infof("Daemon already running: %v", configuration) return } p.ProcessStateLock.Lock() defer p.ProcessStateLock.Unlock() log.Infof("Attempting to Start Daemon") //TODO Currently pathnames with spaces do not seem to work correctly with the below // usage of exec.command. Given that ConfigurePackage defaults to a directory name which // doesnt have spaces (C:/ProgramData/Amazon/SSM/....), the issue is not currently exposed. // Needs to be fixed regardless. commandArguments := append(strings.Split(configuration, " ")) log.Infof("Running command: %v.", commandArguments) daemonInvoke := exec.Command(commandArguments[0], commandArguments[1:]...) daemonInvoke.Dir = p.ExeLocation err = DaemonCmdExecutor(daemonInvoke) if err != nil { log.Errorf("Error starting Daemon: %s", err.Error()) return err } p.Process = daemonInvoke.Process // Attach daemon process to the SSM agent job object err = jobobject.AttachProcessToJobObject(uint32(daemonInvoke.Process.Pid)) if err != nil { log.Errorf("Error attaching job object to Daemon: %s", err.Error()) } else { log.Debugf("Successfully attached job object to Daemon") } p.CurrentDaemonState = CurrentRunning return } // Starts a given executable or a specified powershell script and enables daemon functionality func StartDaemon(p *Plugin, configuration string) (err error) { log := p.Context.Log() defer func() { if r := recover(); r != nil { log.Errorf("Start daemon panic: \n%v", r) log.Errorf("Stacktrace:\n%s", debug.Stack()) } }() RetryCount := 0 // Bail out if an explicit Stop daemon is requested by the user if p.stopRequested() { log.Infof("Daemon requested to be stopped: %v", configuration) return } // Below loop initiates daemon startup and then goes to sleep once the daemon // starts running. Once the daemon exits, it will attempt to retry launching // daemon unless the user has explicitly requested a stop. for { start := time.Now() log := p.Context.Log() if p.Process != nil { err = BlockWhileDaemonRunningExecutor(p.Context, p.Process.Pid) p.SetDaemonStateStopped() if err != nil { log.Infof("Encountered error: process may not have exited cleanly. Pid %v : %s", p.Process.Pid, err.Error()) } } // Bail out if an explicit Stop daemon is requested by the user if p.stopRequested() { log.Infof("Daemon requested to be stopped: %v", configuration) return } // Invoke the helper function to start daermo err = StartDaemonHelperExecutor(p, configuration) if err == nil { log.Infof("Started Daemon...") } // Setting the time between successive daemon restarts to be atleast 60 seconds end := time.Now() elapsedSecs := end.Sub(start) MinWaitSecs := MinWaitBetweenRetries MaxTimeToResetRetryCount := MaxTimeThresholdToResetRetryCounter if elapsedSecs < MinWaitSecs { RetryCount++ if RetryCount > MaxRetryCountDuringFailures { log.Infof("Daemon %v process exited for %v times within the minimum threshold time window from its startup. Bailing out.", p.Name, RetryCount) return } log.Infof("Waiting %v seconds to start %s again", MinWaitSecs-elapsedSecs, p.Name) time.Sleep(MinWaitSecs - elapsedSecs) } else if elapsedSecs > MaxTimeToResetRetryCount { log.Infof("Setting retrycount for %s daemon back to 0", p.Name) RetryCount = 0 } } } func (p *Plugin) Start(configuration string, orchestrationDir string, cancelFlag task.CancelFlag, out iohandler.IOHandler) error { log := p.Context.Log() log.Infof(" Package location - %s", p.ExeLocation) log.Infof(" Command/Script/Executable to be run - %s", configuration) // Start a new daemon handling goroutine if the goroutine is currently not running p.ProcessStateLock.Lock() if p.CurrentDaemonState == CurrentStopped { log.Infof(" Invoking goroutine to manage daemon lifecycle") // Set the User Requested state to ENABLED p.RequestedDaemonState = RequestedEnabled p.ProcessStateLock.Unlock() go StartDaemon(p, configuration) } else { p.ProcessStateLock.Unlock() } return nil } func StopDaemon(p *Plugin) { log := p.Context.Log() p.ProcessStateLock.Lock() defer p.ProcessStateLock.Unlock() if p.Process != nil { log.Infof("Process id of daemon -> %v", p.Process.Pid) if p.CurrentDaemonState == CurrentRunning { err := exec.Command("taskkill", "/F", "/T", "/PID", strconv.Itoa(p.Process.Pid)).Run() if err != nil { log.Infof("Encountered error while trying to stop the child processes %v : %s", p.Process.Pid, err.Error()) } else { log.Infof("Successfully stopped the children of process %v", p.Process.Pid) } if err = p.Process.Kill(); err != nil { log.Infof("Encountered error while trying to kill the process %v : %s", p.Process.Pid, err.Error()) } else { log.Infof("Successfully stopped the process %v", p.Process.Pid) } p.RequestedDaemonState = RequestedDisabled p.CurrentDaemonState = CurrentStopped p.Process = nil } } } func (p *Plugin) Stop(cancelFlag task.CancelFlag) error { log := p.Context.Log() log.Infof("Stopping Daemon") StopDaemonExecutor(p) return nil }