Skip to content

Commit

Permalink
Send the running app and dapr an interrupt signal instead of killing …
Browse files Browse the repository at this point in the history
…them immediately. This allows the processes to clean up appropriately.
  • Loading branch information
pkedy committed Oct 28, 2021
1 parent 4d75238 commit b43ffa1
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 23 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ cli
# CLI's auto-generated components directory
**/components

test_output.json
test_output.json
28 changes: 28 additions & 0 deletions cmd/interrupt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// +build !windows

package cmd

import (
"os"
"os/exec"
"syscall"
)

// interruptProcess sends an Interrupt signal to the process.
func interruptProcess(proc *os.Process) error {
return proc.Signal(os.Interrupt)
}

func suppressCtrlC(err error) error {
if exitErr, ok := err.(*exec.ExitError); ok {
// The SIGINT signal is sent when the user at the
// controlling terminal presses the interrupt character,
// which by default is ^C (Control-C)
// https://golang.org/pkg/os/signal/#hdr-Types_of_signals
if exitErr.ExitCode() == int(syscall.SIGINT) {
return nil
}
}

return err
}
26 changes: 26 additions & 0 deletions cmd/interrupt_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package cmd

import (
"os"
"os/exec"

"golang.org/x/sys/windows"
)

// interruptProcess sends a CTRL-BREAK to the process.
func interruptProcess(proc *os.Process) error {
return windows.GenerateConsoleCtrlEvent(windows.CTRL_BREAK_EVENT, uint32(proc.Pid))
}

func suppressCtrlC(err error) error {
if exitErr, ok := err.(*exec.ExitError); ok {
// windows.STATUS_CONTROL_C_EXIT means
// The application terminated as a result of a CTRL+C.
// http://errorco.de/win32/ntstatus-h/status_control_c_exit/0xc000013a/
if exitErr.ExitCode() == int(windows.STATUS_CONTROL_C_EXIT) {
return nil
}
}

return err
}
66 changes: 47 additions & 19 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"bufio"
"fmt"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
Expand Down Expand Up @@ -104,11 +105,11 @@ var RunCmd = &cobra.Command{
return
}

sigCh := make(chan os.Signal, 1)
sigCh := make(chan os.Signal, 5)
setupShutdownNotify(sigCh)

daprRunning := make(chan bool, 1)
appRunning := make(chan bool, 1)
daprRunning := make(chan bool, 2)
appRunning := make(chan bool, 2)

go func() {
var startInfo string
Expand Down Expand Up @@ -137,7 +138,7 @@ var RunCmd = &cobra.Command{
}

go func() {
daprdErr := output.DaprCMD.Wait()
daprdErr := suppressCtrlC(output.DaprCMD.Wait())

if daprdErr != nil {
print.FailureStatusEvent(os.Stderr, "The daprd process exited with error code: %s", daprdErr.Error())
Expand All @@ -146,6 +147,7 @@ var RunCmd = &cobra.Command{
print.SuccessStatusEvent(os.Stdout, "Exited Dapr successfully")
}
sigCh <- os.Interrupt
daprRunning <- false
}()

if appPort <= 0 {
Expand Down Expand Up @@ -240,14 +242,15 @@ var RunCmd = &cobra.Command{
}

go func() {
appErr := output.AppCMD.Wait()
appErr := suppressCtrlC(output.AppCMD.Wait())

if appErr != nil {
print.FailureStatusEvent(os.Stderr, "The App process exited with error code: %s", appErr.Error())
} else {
print.SuccessStatusEvent(os.Stdout, "Exited App successfully")
}
sigCh <- os.Interrupt
appRunning <- false
}()

appRunning <- true
Expand Down Expand Up @@ -287,22 +290,22 @@ var RunCmd = &cobra.Command{
<-sigCh
print.InfoStatusEvent(os.Stdout, "\nterminated signal received: shutting down")

if output.DaprCMD.ProcessState == nil || !output.DaprCMD.ProcessState.Exited() {
err = output.DaprCMD.Process.Kill()
if err != nil {
print.FailureStatusEvent(os.Stderr, fmt.Sprintf("Error exiting Dapr: %s", err))
} else {
print.SuccessStatusEvent(os.Stdout, "Exited Dapr successfully")
}
interruptCmd(output.AppCMD, "App")

// Wait for the app to terminate for up to 5 seconds.
select {
case <-appRunning:
case <-time.After(5 * time.Second):
killCmd(output.AppCMD)
}

if output.AppCMD != nil && (output.AppCMD.ProcessState == nil || !output.AppCMD.ProcessState.Exited()) {
err = output.AppCMD.Process.Kill()
if err != nil {
print.FailureStatusEvent(os.Stderr, fmt.Sprintf("Error exiting App: %s", err))
} else {
print.SuccessStatusEvent(os.Stdout, "Exited App successfully")
}
interruptCmd(output.DaprCMD, "Dapr")

// Wait for daprd to terminate for up to 10 seconds.
select {
case <-daprRunning:
case <-time.After(10 * time.Second):
killCmd(output.DaprCMD)
}

if unixDomainSocket != "" {
Expand All @@ -313,6 +316,31 @@ var RunCmd = &cobra.Command{
},
}

func interruptCmd(cmd *exec.Cmd, name string) {
if cmd == nil || cmd.ProcessState == nil || cmd.ProcessState.Exited() {
return
}

if err := interruptProcess(cmd.Process); err != nil {
print.FailureStatusEvent(os.Stdout, fmt.Sprintf("Interrupt signal failed for %s. Attempting to kill process: %v", name, err))
if err = cmd.Process.Kill(); err != nil {
print.FailureStatusEvent(os.Stderr, fmt.Sprintf("Error exiting %s: %s", name, err))

return
}
}

print.SuccessStatusEvent(os.Stdout, fmt.Sprintf("Exited %s successfully", name))
}

func killCmd(cmd *exec.Cmd) {
if cmd == nil || cmd.ProcessState == nil || cmd.ProcessState.Exited() {
return
}

cmd.Process.Kill()
}

func init() {
RunCmd.Flags().IntVarP(&appPort, "app-port", "p", -1, "The port your application is listening on")
RunCmd.Flags().StringVarP(&appID, "app-id", "a", "", "The id for your application, used for service discovery")
Expand Down
17 changes: 17 additions & 0 deletions pkg/standalone/processgroup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//go:build !windows
// +build !windows

// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation and Dapr Contributors.
// Licensed under the MIT License.
// ------------------------------------------------------------

package standalone

import (
"os/exec"
)

func setCmdSysProcAttr(cmd *exec.Cmd) {
// Do nothing
}
19 changes: 19 additions & 0 deletions pkg/standalone/processgroup_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation and Dapr Contributors.
// Licensed under the MIT License.
// ------------------------------------------------------------

package standalone

import (
"os/exec"
"syscall"
)

func setCmdSysProcAttr(cmd *exec.Cmd) {
if cmd.SysProcAttr == nil {
cmd.SysProcAttr = &syscall.SysProcAttr{}
}

cmd.SysProcAttr.CreationFlags = syscall.CREATE_NEW_PROCESS_GROUP
}
2 changes: 2 additions & 0 deletions pkg/standalone/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ func getDaprCommand(appID string, daprHTTPPort int, daprGRPCPort int, appPort in
}

cmd := exec.Command(daprCMD, args...)
setCmdSysProcAttr(cmd)
return cmd, daprHTTPPort, daprGRPCPort, metricsPort, nil
}

Expand Down Expand Up @@ -184,6 +185,7 @@ func getAppCommand(httpPort, grpcPort, metricsPort int, command string, args []s
fmt.Sprintf("DAPR_HTTP_PORT=%v", httpPort),
fmt.Sprintf("DAPR_GRPC_PORT=%v", grpcPort),
fmt.Sprintf("DAPR_METRICS_PORT=%v", metricsPort))
setCmdSysProcAttr(cmd)

return cmd, nil
}
Expand Down
6 changes: 3 additions & 3 deletions tests/e2e/standalone/standalone_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,9 @@ func testRun(t *testing.T) {
output, err := spawn.Command(daprPath, "run", "--app-id", "testapp", "--unix-domain-socket", "/tmp", "--", "bash", "-c", "curl --unix-socket /tmp/dapr-testapp-http.socket -v -X POST http://unix/v1.0/shutdown; sleep 10; exit 1")
t.Log(output)
require.NoError(t, err, "run failed")
assert.Contains(t, output, "Exited App successfully", "App should be shutdown before it has a chance to return non-zero")
// It would be ideal to check that the spawned app existed with a non-zero
// exit code, however when sending a SIGINT/CTRL-C to curl, it does not.
// In the future we could find a program that returns 0 in this scenario.
assert.Contains(t, output, "Exited Dapr successfully")
})
}
Expand Down Expand Up @@ -586,9 +588,7 @@ func testInvoke(t *testing.T) {
require.NoError(t, err, "dapr stop failed")
assert.Contains(t, output, "app stopped successfully: invoke_e2e")
}, "run", "--app-id", "invoke_e2e", "--app-port", "9987", "--unix-domain-socket", path)

}

}

func listOutputCheck(t *testing.T, output string) {
Expand Down

0 comments on commit b43ffa1

Please sign in to comment.