Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ dapr run --run-file /path/to/directory -k
exitWithError = true
print.FailureStatusEvent(os.Stderr, fmt.Sprintf("Error exiting App: %s", output.AppErr))
} else if output.AppCMD != nil && output.AppCMD.Process != nil && (output.AppCMD.ProcessState == nil || !output.AppCMD.ProcessState.Exited()) {
err = output.AppCMD.Process.Kill()
err = killProcessGroup(output.AppCMD.Process)
if err != nil {
// If the process already exited on its own, treat this as a clean shutdown.
if errors.Is(err, os.ErrProcessDone) {
Expand Down Expand Up @@ -1007,7 +1007,7 @@ func killAppProcess(runE *runExec.RunExec) error {
// Process already exited, no need to kill it.
return nil
}
err := runE.AppCMD.Command.Process.Kill()
err := killProcessGroup(runE.AppCMD.Command.Process)
if err != nil {
// If the process already exited on its own
if errors.Is(err, os.ErrProcessDone) {
Expand Down
62 changes: 62 additions & 0 deletions cmd/run_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,77 @@ limitations under the License.
package cmd

import (
"errors"
"fmt"
"os"
"os/exec"
"syscall"
"time"

"github.com/dapr/cli/pkg/print"
runExec "github.com/dapr/cli/pkg/runexec"
)

// killProcessGroup kills the entire process group of the given process so that
// grandchild processes (e.g. the compiled binary spawned by `go run`) are also
// terminated. It sends SIGTERM first; if the process group is still alive after
// a 5-second grace period, it sends SIGKILL.
func killProcessGroup(process *os.Process) error {
var (
pgid int
err error
)

pgid, err = syscall.Getpgid(process.Pid)
if err != nil {
if errors.Is(err, syscall.ESRCH) {
// The group leader may have already exited (e.g. when using `go run`),
// but other processes in the same process group can still be alive.
// Since the app is started with Setpgid=true, the PGID equals the leader
// PID, so fall back to using process.Pid as the PGID.
pgid = process.Pid
} else {
// Can't determine pgid for some other reason — fall back to single-process kill.
killErr := process.Kill()
if errors.Is(killErr, os.ErrProcessDone) {
return nil
}
return killErr
}
}

err = syscall.Kill(-pgid, syscall.SIGTERM)
if err != nil {
if err == syscall.ESRCH {
return nil // process group already gone
}
return fmt.Errorf("failed to send SIGTERM to process group %d: %w", pgid, err)
}

const gracePeriod = 5 * time.Second
deadline := time.Now().Add(gracePeriod)
for time.Now().Before(deadline) {
err = syscall.Kill(-pgid, 0)
if err == nil {
time.Sleep(100 * time.Millisecond)
continue
}
if errors.Is(err, syscall.ESRCH) {
return nil // process group gone
}
return fmt.Errorf("failed to check status of process group %d: %w", pgid, err)
}
// Grace period elapsed — force kill.
err = syscall.Kill(-pgid, syscall.SIGKILL)
if err == syscall.ESRCH {
return nil
}
if err != nil {
return fmt.Errorf("failed to send SIGKILL to process group %d: %w", pgid, err)
}
return nil
}

// setDaprProcessGroupForRun sets the process group on the daprd command so the
// sidecar can be managed independently (e.g. when the app is started via exec).
func setDaprProcessGroupForRun(cmd *exec.Cmd) {
Expand Down
20 changes: 20 additions & 0 deletions cmd/run_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,30 @@ import (
"fmt"
"os"
"os/exec"
"strconv"

"github.com/kolesnikovae/go-winjob"

"github.com/dapr/cli/pkg/print"
runExec "github.com/dapr/cli/pkg/runexec"
"github.com/dapr/cli/utils"
)

// killProcessGroup on Windows terminates the entire process tree by terminating the
// job object associated with the current CLI process (keyed by os.Getpid()). The
// process argument is only used for the fallback path when no job object can be
// opened (e.g. the process was never attached to one).
func killProcessGroup(process *os.Process) error {
jobName := utils.GetJobObjectNameFromPID(strconv.Itoa(os.Getpid()))
jbobj, err := winjob.Open(jobName)
if err != nil {
// No job object found — fall back to single-process kill.
return process.Kill()
}
defer jbobj.Close()
return jbobj.TerminateWithExitCode(0)
}

// setDaprProcessGroupForRun is a no-op on Windows (SysProcAttr.Setpgid does not exist).
func setDaprProcessGroupForRun(cmd *exec.Cmd) {
// no-op on Windows
Expand All @@ -48,6 +67,7 @@ func startAppProcessInBackground(output *runExec.RunOutput, binary string, args
if err := output.AppCMD.Start(); err != nil {
return fmt.Errorf("failed to start app: %w", err)
}
utils.AttachJobObjectToProcess(strconv.Itoa(os.Getpid()), output.AppCMD.Process)

go func() {
waitErr := output.AppCMD.Wait()
Expand Down
Loading