diff --git a/cmd/stop.go b/cmd/stop.go index fe2141092..1dde0f0e3 100644 --- a/cmd/stop.go +++ b/cmd/stop.go @@ -17,6 +17,7 @@ import ( "fmt" "os" "path/filepath" + "time" "github.com/spf13/cobra" @@ -26,8 +27,10 @@ import ( ) var ( - stopAppID string - stopK8s bool + stopAppID string + stopK8s bool + stopWait bool + stopTimeout int ) var StopCmd = &cobra.Command{ @@ -48,9 +51,16 @@ dapr stop --run-file dapr.yaml -k # Stop and delete Kubernetes deployment of multiple apps by providing a directory path containing the run config file(dapr.yaml) dapr stop --run-file /path/to/directory -k + +# Stop and wait for Dapr application to exit +dapr stop --app-id --wait --timeout 20 `, Run: func(cmd *cobra.Command, args []string) { var err error + var timeout time.Duration + if stopWait { + timeout = time.Duration(int(time.Second) * stopTimeout) + } if len(runFilePath) > 0 { runFilePath, err = getRunFilePath(runFilePath) if err != nil { @@ -58,7 +68,7 @@ dapr stop --run-file /path/to/directory -k os.Exit(1) } if !stopK8s { - err = executeStopWithRunFile(runFilePath) + err = executeStopWithRunFile(runFilePath, timeout) if err != nil { print.FailureStatusEvent(os.Stderr, "Failed to stop Dapr and app processes: %s", err) } else { @@ -85,7 +95,7 @@ dapr stop --run-file /path/to/directory -k } cliPIDToNoOfApps := standalone.GetCLIPIDCountMap(apps) for _, appID := range args { - err = standalone.Stop(appID, cliPIDToNoOfApps, apps) + err = standalone.Stop(appID, cliPIDToNoOfApps, apps, timeout) if err != nil { print.FailureStatusEvent(os.Stderr, "failed to stop app id %s: %s", appID, err) } else { @@ -99,14 +109,16 @@ func init() { StopCmd.Flags().StringVarP(&stopAppID, "app-id", "a", "", "The application id to be stopped") StopCmd.Flags().StringVarP(&runFilePath, "run-file", "f", "", "Path to the run template file for the list of apps to stop") StopCmd.Flags().BoolVarP(&stopK8s, "kubernetes", "k", false, "Stop deployments in Kunernetes based on multi-app run file") + StopCmd.Flags().BoolVarP(&stopWait, "wait", "w", false, "Wait for apps to stop") + StopCmd.Flags().IntVarP(&stopTimeout, "timeout", "t", 15, "Wait timeout in seconds") StopCmd.Flags().BoolP("help", "h", false, "Print this help message") RootCmd.AddCommand(StopCmd) } -func executeStopWithRunFile(runFilePath string) error { +func executeStopWithRunFile(runFilePath string, timeout time.Duration) error { absFilePath, err := filepath.Abs(runFilePath) if err != nil { return fmt.Errorf("failed to get absolute file path for %s: %w", runFilePath, err) } - return standalone.StopAppsWithRunFile(absFilePath) + return standalone.StopAppsWithRunFile(absFilePath, timeout) } diff --git a/pkg/standalone/stop.go b/pkg/standalone/stop.go index 78a75a968..968f4f490 100644 --- a/pkg/standalone/stop.go +++ b/pkg/standalone/stop.go @@ -17,36 +17,42 @@ limitations under the License. package standalone import ( + "errors" "fmt" + "os" "syscall" + "time" "github.com/dapr/cli/utils" ) // Stop terminates the application process. -func Stop(appID string, cliPIDToNoOfApps map[int]int, apps []ListOutput) error { +func Stop(appID string, cliPIDToNoOfApps map[int]int, apps []ListOutput, timeout time.Duration) error { for _, a := range apps { if a.AppID == appID { - var pid string + var pid int // Kill the Daprd process if Daprd was started without CLI, otherwise // kill the CLI process which also kills the associated Daprd process. if a.CliPID == 0 || cliPIDToNoOfApps[a.CliPID] > 1 { - pid = fmt.Sprintf("%v", a.DaprdPID) //nolint: perfsprint + pid = a.DaprdPID //nolint: perfsprint cliPIDToNoOfApps[a.CliPID]-- } else { - pid = fmt.Sprintf("%v", a.CliPID) //nolint: perfsprint + pid = a.CliPID //nolint: perfsprint } - _, err := utils.RunCmdAndWait("kill", pid) + _, err := utils.RunCmdAndWait("kill", fmt.Sprintf("%v", pid)) //nolint:perfsprint + if err != nil { + return err + } - return err + return waitForProccessToExit(pid, timeout) } } return fmt.Errorf("couldn't find app id %s", appID) } // StopAppsWithRunFile terminates the daprd and application processes with the given run file. -func StopAppsWithRunFile(runTemplatePath string) error { +func StopAppsWithRunFile(runTemplatePath string, timeout time.Duration) error { apps, err := List() if err != nil { return err @@ -58,12 +64,47 @@ func StopAppsWithRunFile(runTemplatePath string) error { if err != nil { // Fall back to cliPID if pgid is not available. _, err = utils.RunCmdAndWait("kill", fmt.Sprintf("%v", a.CliPID)) //nolint:perfsprint - return err + if err != nil { + return err + } + return waitForProccessToExit(a.CliPID, timeout) } // Kill the whole process group. err = syscall.Kill(-pgid, syscall.SIGINT) - return err + if err != nil { + return err + } + return waitForProccessToExit(-pgid, timeout) } } return fmt.Errorf("couldn't find apps with run file %q", runTemplatePath) } + +func waitForProccessToExit(pid int, timeout time.Duration) error { + if timeout == 0 { + return nil + } + + proc, err := os.FindProcess(pid) + if err != nil { + return nil //nolint:nilerr + } + + ticker := time.NewTicker(time.Second) + timer := time.NewTimer(timeout) + defer timer.Stop() + + for { + select { + case <-ticker.C: + if err := proc.Signal(syscall.Signal(0)); err != nil && !errors.Is(err, os.ErrProcessDone) { + return err + } else if err != nil { + return nil //nolint:nilerr + } + case <-timer.C: + proc.Signal(syscall.SIGKILL) + return nil + } + } +}