From a64679cb1658858523ee3234ab9c8c386f2f7359 Mon Sep 17 00:00:00 2001 From: Samantha Coyle Date: Thu, 29 Jan 2026 14:49:59 -0600 Subject: [PATCH 01/25] fix(python apps): fork a child and exec appcmd in the child to avoid problems Signed-off-by: Samantha Coyle --- cmd/run.go | 77 ++++++++++++++++++++++++------------------- pkg/standalone/run.go | 10 ++++-- 2 files changed, 52 insertions(+), 35 deletions(-) diff --git a/cmd/run.go b/cmd/run.go index 2f9d3d240..87dd4435e 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -18,11 +18,13 @@ import ( "fmt" "io" "os" + "os/exec" "path/filepath" "runtime" "slices" "strconv" "strings" + "syscall" "time" "golang.org/x/mod/semver" @@ -227,7 +229,7 @@ dapr run --run-file /path/to/directory -k sharedRunConfig.SchedulerHostAddress = &addr } } - output, err := runExec.NewOutput(&standalone.RunConfig{ + appConfig := &standalone.RunConfig{ AppID: appID, AppChannelAddress: appChannelAddress, AppPort: appPort, @@ -239,7 +241,8 @@ dapr run --run-file /path/to/directory -k UnixDomainSocket: unixDomainSocket, InternalGRPCPort: internalGRPCPort, SharedRunConfig: *sharedRunConfig, - }) + } + output, err := runExec.NewOutput(appConfig) if err != nil { print.FailureStatusEvent(os.Stderr, err.Error()) os.Exit(1) @@ -280,6 +283,10 @@ dapr run --run-file /path/to/directory -k output.DaprCMD.Stdout = os.Stdout output.DaprCMD.Stderr = os.Stderr + // Set process group so sidecar survives when we exec the app + output.DaprCMD.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + } err = output.DaprCMD.Start() if err != nil { @@ -355,47 +362,51 @@ dapr run --run-file /path/to/directory -k return } - stdErrPipe, pipeErr := output.AppCMD.StderrPipe() - if pipeErr != nil { - print.FailureStatusEvent(os.Stderr, "Error creating stderr for App: "+err.Error()) + command := args[0] + binary, err := exec.LookPath(command) + if err != nil { + print.FailureStatusEvent(os.Stderr, fmt.Sprintf("Failed to find command %s: %v", command, err)) appRunning <- false return } - - stdOutPipe, pipeErr := output.AppCMD.StdoutPipe() - if pipeErr != nil { - print.FailureStatusEvent(os.Stderr, "Error creating stdout for App: "+err.Error()) - appRunning <- false - return + envMap := appConfig.GetEnv() + env := os.Environ() + for k, v := range envMap { + env = append(env, fmt.Sprintf("%s=%s", k, v)) } - - errScanner := bufio.NewScanner(stdErrPipe) - outScanner := bufio.NewScanner(stdOutPipe) - go func() { - for errScanner.Scan() { - fmt.Println(print.Blue("== APP == " + errScanner.Text())) - } - }() - - go func() { - for outScanner.Scan() { - fmt.Println(print.Blue("== APP == " + outScanner.Text())) - } - }() - - err = output.AppCMD.Start() + env = append(env, fmt.Sprintf("DAPR_HTTP_PORT=%d", output.DaprHTTPPort)) + env = append(env, fmt.Sprintf("DAPR_GRPC_PORT=%d", output.DaprGRPCPort)) + + // Use ForkExec to fork a child, then exec python in the child. + // NOTE: This is needed bc forking a python app with async python running (ie everything in durabletask-python) will cause random hangs, no matter the python version. + // Doing this this way makes python not sees the fork, starts via exec, so it doesn't cause random hangs due to when forking async python apps where locks and such get corrupted in forking. + // File descriptors are inherited from parent, so stdout/stderr go to terminal + pid, err := syscall.ForkExec(binary, args, &syscall.ProcAttr{ + Env: env, + // stdin, stdout, stderr + Files: []uintptr{0, 1, 2}, + Sys: &syscall.SysProcAttr{ + Setpgid: true, + }, + }) if err != nil { - print.FailureStatusEvent(os.Stderr, err.Error()) + print.FailureStatusEvent(os.Stderr, fmt.Sprintf("Failed to fork/exec app: %v", err)) appRunning <- false return } + output.AppCMD.Process = &os.Process{Pid: pid} go func() { - appErr := output.AppCMD.Wait() - - if appErr != nil { - output.AppErr = appErr - print.FailureStatusEvent(os.Stderr, "The App process exited with error code: %s", appErr.Error()) + var waitStatus syscall.WaitStatus + _, err := syscall.Wait4(pid, &waitStatus, 0, nil) + if err != nil { + output.AppErr = err + print.FailureStatusEvent(os.Stderr, "The App process exited with error: %s", err.Error()) + } else if !waitStatus.Exited() || waitStatus.ExitStatus() != 0 { + output.AppErr = fmt.Errorf("app exited with status %d", waitStatus.ExitStatus()) + if waitStatus.ExitStatus() != 0 { + print.FailureStatusEvent(os.Stderr, "The App process exited with error code: %d", waitStatus.ExitStatus()) + } } else { print.SuccessStatusEvent(os.Stdout, "Exited App successfully") } diff --git a/pkg/standalone/run.go b/pkg/standalone/run.go index 078a3543c..f1d4465ba 100644 --- a/pkg/standalone/run.go +++ b/pkg/standalone/run.go @@ -619,10 +619,16 @@ func GetAppCommand(config *RunConfig) *exec.Cmd { args = config.Command[1:] } - cmd := exec.Command(command, args...) + // Use shell exec to avoid forking, which breaks Python threading + allArgs := append([]string{command}, args...) + quotedArgs := make([]string, len(allArgs)) + for i, arg := range allArgs { + quotedArgs[i] = fmt.Sprintf("%q", arg) + } + shellCmd := fmt.Sprintf("exec %s", strings.Join(quotedArgs, " ")) + cmd := exec.Command("/bin/sh", "-c", shellCmd) cmd.Env = os.Environ() cmd.Env = append(cmd.Env, config.getEnv()...) - return cmd } From 480a884755306cca8ab8344559fa1bb7361358ba Mon Sep 17 00:00:00 2001 From: Samantha Coyle Date: Thu, 29 Jan 2026 15:05:03 -0600 Subject: [PATCH 02/25] fix: final clean testing with pipes for app prefix Signed-off-by: Samantha Coyle --- cmd/run.go | 5 +++-- pkg/standalone/run.go | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/cmd/run.go b/cmd/run.go index 87dd4435e..babeac4ec 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -380,10 +380,11 @@ dapr run --run-file /path/to/directory -k // Use ForkExec to fork a child, then exec python in the child. // NOTE: This is needed bc forking a python app with async python running (ie everything in durabletask-python) will cause random hangs, no matter the python version. // Doing this this way makes python not sees the fork, starts via exec, so it doesn't cause random hangs due to when forking async python apps where locks and such get corrupted in forking. - // File descriptors are inherited from parent, so stdout/stderr go to terminal pid, err := syscall.ForkExec(binary, args, &syscall.ProcAttr{ Env: env, - // stdin, stdout, stderr + // stdin, stdout, and stderr inherit directly from the parent + // This prevents Python from detecting pipes because if the app is Python then it will detect the pipes and think + // it's a fork and will cause random hangs due to async python in durabletask-python. Files: []uintptr{0, 1, 2}, Sys: &syscall.SysProcAttr{ Setpgid: true, diff --git a/pkg/standalone/run.go b/pkg/standalone/run.go index f1d4465ba..f820c88b6 100644 --- a/pkg/standalone/run.go +++ b/pkg/standalone/run.go @@ -23,6 +23,7 @@ import ( "runtime" "strconv" "strings" + "syscall" "k8s.io/apimachinery/pkg/api/resource" @@ -629,6 +630,10 @@ func GetAppCommand(config *RunConfig) *exec.Cmd { cmd := exec.Command("/bin/sh", "-c", shellCmd) cmd.Env = os.Environ() cmd.Env = append(cmd.Env, config.getEnv()...) + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + } + return cmd } From 2492ef442eec695e2ddec52dea7d6a9e3b35ab41 Mon Sep 17 00:00:00 2001 From: Samantha Coyle Date: Thu, 29 Jan 2026 15:42:27 -0600 Subject: [PATCH 03/25] style: appease linter Signed-off-by: Samantha Coyle --- cmd/run.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/run.go b/cmd/run.go index babeac4ec..2cdb3ce2b 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -162,9 +162,9 @@ dapr run --run-file /path/to/directory -k fmt.Println(print.WhiteBold("WARNING: no application command found.")) } - daprDirPath, err := standalone.GetDaprRuntimePath(cmdruntime.GetDaprRuntimePath()) - if err != nil { - print.FailureStatusEvent(os.Stderr, "Failed to get Dapr install directory: %v", err) + daprDirPath, pathErr := standalone.GetDaprRuntimePath(cmdruntime.GetDaprRuntimePath()) + if pathErr != nil { + print.FailureStatusEvent(os.Stderr, "Failed to get Dapr install directory: %v", pathErr) os.Exit(1) } From 27e160acdeca80d4f7a620d4b9d21badc31dab15 Mon Sep 17 00:00:00 2001 From: Samantha Coyle Date: Thu, 29 Jan 2026 16:36:54 -0600 Subject: [PATCH 04/25] fix: make windows tests happy Signed-off-by: Samantha Coyle --- cmd/run.go | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/cmd/run.go b/cmd/run.go index 2cdb3ce2b..dc43fa163 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -283,9 +283,11 @@ dapr run --run-file /path/to/directory -k output.DaprCMD.Stdout = os.Stdout output.DaprCMD.Stderr = os.Stderr - // Set process group so sidecar survives when we exec the app - output.DaprCMD.SysProcAttr = &syscall.SysProcAttr{ - Setpgid: true, + // Set process group so sidecar survives when we exec the + if runtime.GOOS != string(windowsOsType) { + output.DaprCMD.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + } } err = output.DaprCMD.Start() @@ -377,19 +379,23 @@ dapr run --run-file /path/to/directory -k env = append(env, fmt.Sprintf("DAPR_HTTP_PORT=%d", output.DaprHTTPPort)) env = append(env, fmt.Sprintf("DAPR_GRPC_PORT=%d", output.DaprGRPCPort)) - // Use ForkExec to fork a child, then exec python in the child. - // NOTE: This is needed bc forking a python app with async python running (ie everything in durabletask-python) will cause random hangs, no matter the python version. - // Doing this this way makes python not sees the fork, starts via exec, so it doesn't cause random hangs due to when forking async python apps where locks and such get corrupted in forking. - pid, err := syscall.ForkExec(binary, args, &syscall.ProcAttr{ + procAttr := &syscall.ProcAttr{ Env: env, // stdin, stdout, and stderr inherit directly from the parent // This prevents Python from detecting pipes because if the app is Python then it will detect the pipes and think // it's a fork and will cause random hangs due to async python in durabletask-python. Files: []uintptr{0, 1, 2}, - Sys: &syscall.SysProcAttr{ + } + if runtime.GOOS != string(windowsOsType) { + procAttr.Sys = &syscall.SysProcAttr{ Setpgid: true, - }, - }) + } + } + + // Use ForkExec to fork a child, then exec python in the child. + // NOTE: This is needed bc forking a python app with async python running (ie everything in durabletask-python) will cause random hangs, no matter the python version. + // Doing this this way makes python not sees the fork, starts via exec, so it doesn't cause random hangs due to when forking async python apps where locks and such get corrupted in forking. + pid, err := syscall.ForkExec(binary, args, procAttr) if err != nil { print.FailureStatusEvent(os.Stderr, fmt.Sprintf("Failed to fork/exec app: %v", err)) appRunning <- false From c51c42f7ed126a43d428099fd3214b8f0f563898 Mon Sep 17 00:00:00 2001 From: Samantha Coyle Date: Mon, 2 Feb 2026 15:37:09 -0600 Subject: [PATCH 05/25] style: appease linter Signed-off-by: Samantha Coyle --- cmd/run.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/run.go b/cmd/run.go index dc43fa163..378cc1961 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -365,7 +365,8 @@ dapr run --run-file /path/to/directory -k } command := args[0] - binary, err := exec.LookPath(command) + var binary string + binary, err = exec.LookPath(command) if err != nil { print.FailureStatusEvent(os.Stderr, fmt.Sprintf("Failed to find command %s: %v", command, err)) appRunning <- false From 647c648101de0e762fbe54c964444be1f66c08fa Mon Sep 17 00:00:00 2001 From: Samantha Coyle Date: Mon, 2 Feb 2026 15:39:09 -0600 Subject: [PATCH 06/25] fix: check for windows Signed-off-by: Samantha Coyle --- pkg/standalone/run.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pkg/standalone/run.go b/pkg/standalone/run.go index f820c88b6..386451065 100644 --- a/pkg/standalone/run.go +++ b/pkg/standalone/run.go @@ -36,6 +36,7 @@ import ( ) type LogDestType string +type osType string const ( Console LogDestType = "console" @@ -46,6 +47,8 @@ const ( sentryDefaultAddress = "localhost:50001" defaultStructTagKey = "default" + + windowsOsType osType = "windows" ) // RunConfig represents the application configuration parameters. @@ -630,8 +633,10 @@ func GetAppCommand(config *RunConfig) *exec.Cmd { cmd := exec.Command("/bin/sh", "-c", shellCmd) cmd.Env = os.Environ() cmd.Env = append(cmd.Env, config.getEnv()...) - cmd.SysProcAttr = &syscall.SysProcAttr{ - Setpgid: true, + if runtime.GOOS != string(windowsOsType) { + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + } } return cmd From 9af9922a970e23ced75ef894bbc1db9a15285a39 Mon Sep 17 00:00:00 2001 From: Samantha Coyle Date: Mon, 2 Feb 2026 15:57:42 -0600 Subject: [PATCH 07/25] style: appease linter again Signed-off-by: Samantha Coyle --- cmd/run.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cmd/run.go b/cmd/run.go index 378cc1961..bd34c5f1d 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -396,7 +396,8 @@ dapr run --run-file /path/to/directory -k // Use ForkExec to fork a child, then exec python in the child. // NOTE: This is needed bc forking a python app with async python running (ie everything in durabletask-python) will cause random hangs, no matter the python version. // Doing this this way makes python not sees the fork, starts via exec, so it doesn't cause random hangs due to when forking async python apps where locks and such get corrupted in forking. - pid, err := syscall.ForkExec(binary, args, procAttr) + var pid int + pid, err = syscall.ForkExec(binary, args, procAttr) if err != nil { print.FailureStatusEvent(os.Stderr, fmt.Sprintf("Failed to fork/exec app: %v", err)) appRunning <- false @@ -406,7 +407,7 @@ dapr run --run-file /path/to/directory -k go func() { var waitStatus syscall.WaitStatus - _, err := syscall.Wait4(pid, &waitStatus, 0, nil) + _, err = syscall.Wait4(pid, &waitStatus, 0, nil) if err != nil { output.AppErr = err print.FailureStatusEvent(os.Stderr, "The App process exited with error: %s", err.Error()) From 8e8516bbc10553c37d2e0295b6eb658b5950b1e2 Mon Sep 17 00:00:00 2001 From: Samantha Coyle Date: Tue, 10 Feb 2026 13:57:04 -0600 Subject: [PATCH 08/25] fix(build): update test Signed-off-by: Samantha Coyle --- pkg/runexec/runexec_test.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pkg/runexec/runexec_test.go b/pkg/runexec/runexec_test.go index 740cdb159..88a2c5b0e 100644 --- a/pkg/runexec/runexec_test.go +++ b/pkg/runexec/runexec_test.go @@ -205,8 +205,15 @@ func TestRun(t *testing.T) { assert.NoError(t, err) assertCommonArgs(t, basicConfig, output) - assert.Equal(t, "MyCommand", output.AppCMD.Args[0]) - assert.Equal(t, "--my-arg", output.AppCMD.Args[1]) + // The app command is executed via a shell wrapper (/bin/sh -c "exec MyCommand --my-arg"). + require.NotNil(t, output.AppCMD) + require.GreaterOrEqual(t, len(output.AppCMD.Args), 3) + assert.Equal(t, "/bin/sh", output.AppCMD.Args[0]) + assert.Equal(t, "-c", output.AppCMD.Args[1]) + shellCmd := output.AppCMD.Args[2] + assert.Contains(t, shellCmd, "MyCommand") + assert.Contains(t, shellCmd, "--my-arg") + assertArgumentEqual(t, "app-channel-address", "localhost", output.DaprCMD.Args) assertAppEnv(t, basicConfig, output) }) From 5e599f9856d5f22fc59d50a379134cf8aeb3c803 Mon Sep 17 00:00:00 2001 From: Samantha Coyle Date: Tue, 10 Feb 2026 14:07:29 -0600 Subject: [PATCH 09/25] fix: updates for build to pass Signed-off-by: Samantha Coyle --- pkg/standalone/process_group_unix.go | 28 +++++++++++++++++++++++++ pkg/standalone/process_group_windows.go | 13 ++++++++++++ pkg/standalone/run.go | 7 +------ 3 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 pkg/standalone/process_group_unix.go create mode 100644 pkg/standalone/process_group_windows.go diff --git a/pkg/standalone/process_group_unix.go b/pkg/standalone/process_group_unix.go new file mode 100644 index 000000000..f68ddfbf2 --- /dev/null +++ b/pkg/standalone/process_group_unix.go @@ -0,0 +1,28 @@ +//go:build !windows + +/* +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License 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 standalone + +import ( + "os/exec" + "syscall" +) + +// setProcessGroup sets the process group on non-Windows platforms so the child process can be managed independently. +func setProcessGroup(cmd *exec.Cmd) { + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + } +} diff --git a/pkg/standalone/process_group_windows.go b/pkg/standalone/process_group_windows.go new file mode 100644 index 000000000..9485cea76 --- /dev/null +++ b/pkg/standalone/process_group_windows.go @@ -0,0 +1,13 @@ +//go:build windows + +package standalone + +import "os/exec" + +// setProcessGroup is a no-op on Windows because the syscall.SysProcAttr +// fields used on Unix (such as Setpgid) are not available. +func setProcessGroup(cmd *exec.Cmd) { + // no-op on Windows + // TODO: In future we should check if Windows has the same Async Python issues and address them if so. + _ = cmd +} diff --git a/pkg/standalone/run.go b/pkg/standalone/run.go index 386451065..2f1de7bf4 100644 --- a/pkg/standalone/run.go +++ b/pkg/standalone/run.go @@ -23,7 +23,6 @@ import ( "runtime" "strconv" "strings" - "syscall" "k8s.io/apimachinery/pkg/api/resource" @@ -633,11 +632,7 @@ func GetAppCommand(config *RunConfig) *exec.Cmd { cmd := exec.Command("/bin/sh", "-c", shellCmd) cmd.Env = os.Environ() cmd.Env = append(cmd.Env, config.getEnv()...) - if runtime.GOOS != string(windowsOsType) { - cmd.SysProcAttr = &syscall.SysProcAttr{ - Setpgid: true, - } - } + setProcessGroup(cmd) return cmd } From e39d3a0c126203917cec0833ccb23fa20c595e26 Mon Sep 17 00:00:00 2001 From: Samantha Coyle Date: Tue, 10 Feb 2026 14:37:57 -0600 Subject: [PATCH 10/25] fix: ensure successful status on exit Signed-off-by: Samantha Coyle --- cmd/run.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/cmd/run.go b/cmd/run.go index db5b37364..dca665105 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -15,6 +15,7 @@ package cmd import ( "bufio" + "errors" "fmt" "io" "os" @@ -488,8 +489,13 @@ dapr run --run-file /path/to/directory -k } else if output.AppCMD != nil && (output.AppCMD.ProcessState == nil || !output.AppCMD.ProcessState.Exited()) { err = output.AppCMD.Process.Kill() if err != nil { - exitWithError = true - print.FailureStatusEvent(os.Stderr, fmt.Sprintf("Error exiting App: %s", err)) + // If the process already exited on its own, treat this as a clean shutdown. + if errors.Is(err, os.ErrProcessDone) { + print.SuccessStatusEvent(os.Stdout, "Exited App successfully") + } else { + exitWithError = true + print.FailureStatusEvent(os.Stderr, fmt.Sprintf("Error exiting App: %s", err)) + } } else { print.SuccessStatusEvent(os.Stdout, "Exited App successfully") } From 3335080b63d555fc11f1a71a75b7416f8441e549 Mon Sep 17 00:00:00 2001 From: Samantha Coyle Date: Tue, 10 Feb 2026 14:55:42 -0600 Subject: [PATCH 11/25] fix(build): use more build tags to prevent windows os build failures Signed-off-by: Samantha Coyle --- cmd/run.go | 50 ++------------- cmd/run_unix.go | 81 +++++++++++++++++++++++++ cmd/run_windows.go | 60 ++++++++++++++++++ pkg/standalone/process_group_windows.go | 13 ++++ 4 files changed, 158 insertions(+), 46 deletions(-) create mode 100644 cmd/run_unix.go create mode 100644 cmd/run_windows.go diff --git a/cmd/run.go b/cmd/run.go index dca665105..c1d4906c0 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -25,7 +25,6 @@ import ( "slices" "strconv" "strings" - "syscall" "time" "golang.org/x/mod/semver" @@ -284,12 +283,8 @@ dapr run --run-file /path/to/directory -k output.DaprCMD.Stdout = os.Stdout output.DaprCMD.Stderr = os.Stderr - // Set process group so sidecar survives when we exec the - if runtime.GOOS != string(windowsOsType) { - output.DaprCMD.SysProcAttr = &syscall.SysProcAttr{ - Setpgid: true, - } - } + // Set process group so sidecar survives when we exec the app process. + setDaprProcessGroupForRun(output.DaprCMD) err = output.DaprCMD.Start() if err != nil { @@ -381,48 +376,11 @@ dapr run --run-file /path/to/directory -k env = append(env, fmt.Sprintf("DAPR_HTTP_PORT=%d", output.DaprHTTPPort)) env = append(env, fmt.Sprintf("DAPR_GRPC_PORT=%d", output.DaprGRPCPort)) - procAttr := &syscall.ProcAttr{ - Env: env, - // stdin, stdout, and stderr inherit directly from the parent - // This prevents Python from detecting pipes because if the app is Python then it will detect the pipes and think - // it's a fork and will cause random hangs due to async python in durabletask-python. - Files: []uintptr{0, 1, 2}, - } - if runtime.GOOS != string(windowsOsType) { - procAttr.Sys = &syscall.SysProcAttr{ - Setpgid: true, - } - } - - // Use ForkExec to fork a child, then exec python in the child. - // NOTE: This is needed bc forking a python app with async python running (ie everything in durabletask-python) will cause random hangs, no matter the python version. - // Doing this this way makes python not sees the fork, starts via exec, so it doesn't cause random hangs due to when forking async python apps where locks and such get corrupted in forking. - var pid int - pid, err = syscall.ForkExec(binary, args, procAttr) - if err != nil { - print.FailureStatusEvent(os.Stderr, fmt.Sprintf("Failed to fork/exec app: %v", err)) + if err := startAppProcessInBackground(output, binary, args, env, sigCh); err != nil { + print.FailureStatusEvent(os.Stderr, err.Error()) appRunning <- false return } - output.AppCMD.Process = &os.Process{Pid: pid} - - go func() { - var waitStatus syscall.WaitStatus - _, err = syscall.Wait4(pid, &waitStatus, 0, nil) - if err != nil { - output.AppErr = err - print.FailureStatusEvent(os.Stderr, "The App process exited with error: %s", err.Error()) - } else if !waitStatus.Exited() || waitStatus.ExitStatus() != 0 { - output.AppErr = fmt.Errorf("app exited with status %d", waitStatus.ExitStatus()) - if waitStatus.ExitStatus() != 0 { - print.FailureStatusEvent(os.Stderr, "The App process exited with error code: %d", waitStatus.ExitStatus()) - } - } else { - print.SuccessStatusEvent(os.Stdout, "Exited App successfully") - } - sigCh <- os.Interrupt - }() - appRunning <- true }() diff --git a/cmd/run_unix.go b/cmd/run_unix.go new file mode 100644 index 000000000..3158f4d3e --- /dev/null +++ b/cmd/run_unix.go @@ -0,0 +1,81 @@ +//go:build !windows + +/* +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License 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 cmd + +import ( + "fmt" + "os" + "os/exec" + "syscall" + + "github.com/dapr/cli/pkg/print" + runExec "github.com/dapr/cli/pkg/runexec" +) + +// 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) { + if cmd == nil { + return + } + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + } +} + +// startAppProcessInBackground starts the app process using ForkExec. +// This prevents the child from seeing a fork, avoiding Python async/threading issues, +// and sets output.AppCMD.Process. +// It then runs a goroutine that waits and signals sigCh. +func startAppProcessInBackground(output *runExec.RunOutput, binary string, args []string, env []string, sigCh chan os.Signal) error { + procAttr := &syscall.ProcAttr{ + Env: env, + // stdin, stdout, and stderr inherit directly from the parent + // This prevents Python from detecting pipes because if the app is Python then it will detect the pipes and think + // it's a fork and will cause random hangs due to async python in durabletask-python. + Files: []uintptr{0, 1, 2}, + Sys: &syscall.SysProcAttr{ + Setpgid: true, + }, + } + + // Use ForkExec to fork a child, then exec python in the child. + // NOTE: This is needed bc forking a python app with async python running (ie everything in durabletask-python) will cause random hangs, no matter the python version. + // Doing this this way makes python not sees the fork, starts via exec, so it doesn't cause random hangs due to when forking async python apps where locks and such get corrupted in forking. + pid, err := syscall.ForkExec(binary, args, procAttr) + if err != nil { + return fmt.Errorf("failed to fork/exec app: %w", err) + } + output.AppCMD.Process = &os.Process{Pid: pid} + + go func() { + var waitStatus syscall.WaitStatus + _, err := syscall.Wait4(pid, &waitStatus, 0, nil) + if err != nil { + output.AppErr = err + print.FailureStatusEvent(os.Stderr, "The App process exited with error: %s", err.Error()) + } else if !waitStatus.Exited() || waitStatus.ExitStatus() != 0 { + output.AppErr = fmt.Errorf("app exited with status %d", waitStatus.ExitStatus()) + if waitStatus.ExitStatus() != 0 { + print.FailureStatusEvent(os.Stderr, "The App process exited with error code: %d", waitStatus.ExitStatus()) + } + } else { + print.SuccessStatusEvent(os.Stdout, "Exited App successfully") + } + sigCh <- os.Interrupt + }() + return nil +} diff --git a/cmd/run_windows.go b/cmd/run_windows.go new file mode 100644 index 000000000..c76632f66 --- /dev/null +++ b/cmd/run_windows.go @@ -0,0 +1,60 @@ +//go:build windows + +/* +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License 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 cmd + +import ( + "fmt" + "os" + "os/exec" + + "github.com/dapr/cli/pkg/print" + runExec "github.com/dapr/cli/pkg/runexec" +) + +// setDaprProcessGroupForRun is a no-op on Windows (SysProcAttr.Setpgid does not exist). +func setDaprProcessGroupForRun(cmd *exec.Cmd) { + // no-op on Windows + _ = cmd +} + +// startAppProcessInBackground starts the app process using exec.Command, +// sets output.AppCMD to the new command, and runs a goroutine that waits and signals sigCh. +func startAppProcessInBackground(output *runExec.RunOutput, binary string, args []string, env []string, sigCh chan os.Signal) error { + appCmd := exec.Command(binary, args...) + appCmd.Env = env + appCmd.Stdin = os.Stdin + appCmd.Stdout = os.Stdout + appCmd.Stderr = os.Stderr + if err := appCmd.Start(); err != nil { + return fmt.Errorf("failed to start app: %w", err) + } + output.AppCMD = appCmd + + go func() { + waitErr := appCmd.Wait() + if waitErr != nil { + output.AppErr = waitErr + print.FailureStatusEvent(os.Stderr, "The App process exited with error: %s", waitErr.Error()) + } else if appCmd.ProcessState != nil && !appCmd.ProcessState.Success() { + output.AppErr = fmt.Errorf("app exited with status %d", appCmd.ProcessState.ExitCode()) + print.FailureStatusEvent(os.Stderr, "The App process exited with error code: %d", appCmd.ProcessState.ExitCode()) + } else { + print.SuccessStatusEvent(os.Stdout, "Exited App successfully") + } + sigCh <- os.Interrupt + }() + return nil +} diff --git a/pkg/standalone/process_group_windows.go b/pkg/standalone/process_group_windows.go index 9485cea76..a1967676d 100644 --- a/pkg/standalone/process_group_windows.go +++ b/pkg/standalone/process_group_windows.go @@ -1,5 +1,18 @@ //go:build windows +/* +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License 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 standalone import "os/exec" From 3bc794c9009e68fe0f6c42bb7951b594420c11da Mon Sep 17 00:00:00 2001 From: Samantha Coyle Date: Tue, 10 Feb 2026 15:05:39 -0600 Subject: [PATCH 12/25] fix: use os specific cmds Signed-off-by: Samantha Coyle --- .../standalone/init_run_custom_path_test.go | 14 ++++++++--- tests/e2e/standalone/run_test.go | 24 +++++++++---------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/tests/e2e/standalone/init_run_custom_path_test.go b/tests/e2e/standalone/init_run_custom_path_test.go index da7550a89..7e64df692 100644 --- a/tests/e2e/standalone/init_run_custom_path_test.go +++ b/tests/e2e/standalone/init_run_custom_path_test.go @@ -27,6 +27,14 @@ import ( "github.com/stretchr/testify/require" ) +// echoTestAppArgs returns platform-specific args for running a simple "echo test" app (after "--"). +func echoTestAppArgs() []string { + if runtime.GOOS == "windows" { + return []string{"cmd", "/c", "echo test"} + } + return []string{"bash", "-c", "echo 'test'"} +} + // TestStandaloneInitRunUninstallNonDefaultDaprPath covers init, version, run and uninstall with --runtime-path flag. func TestStandaloneInitRunUninstallNonDefaultDaprPath(t *testing.T) { // Ensure a clean environment @@ -61,7 +69,7 @@ func TestStandaloneInitRunUninstallNonDefaultDaprPath(t *testing.T) { args = []string{ "--runtime-path", daprPath, "--app-id", "run_with_dapr_runtime_path_flag", - "--", "bash", "-c", "echo 'test'", + "--", echoTestAppArgs()..., } output, err = cmdRun("", args...) @@ -117,7 +125,7 @@ func TestStandaloneInitRunUninstallNonDefaultDaprPath(t *testing.T) { args = []string{ "--app-id", "run_with_dapr_runtime_path_flag", - "--", "bash", "-c", "echo 'test'", + "--", echoTestAppArgs()..., } output, err = cmdRun("", args...) @@ -179,7 +187,7 @@ func TestStandaloneInitRunUninstallNonDefaultDaprPath(t *testing.T) { args = []string{ "--runtime-path", daprPathFlag, "--app-id", "run_with_dapr_runtime_path_flag", - "--", "bash", "-c", "echo 'test'", + "--", echoTestAppArgs()..., } flagDaprdBinPath := filepath.Join(daprPathFlag, ".dapr", "bin", "daprd") diff --git a/tests/e2e/standalone/run_test.go b/tests/e2e/standalone/run_test.go index d4260aa9e..10a8f50cd 100644 --- a/tests/e2e/standalone/run_test.go +++ b/tests/e2e/standalone/run_test.go @@ -52,7 +52,7 @@ func TestStandaloneRun(t *testing.T) { }) for _, path := range getSocketCases() { t.Run(fmt.Sprintf("normal exit, socket: %s", path), func(t *testing.T) { - output, err := cmdRun(path, "--", "bash", "-c", "echo test") + output, err := cmdRun(path, "--", echoTestAppArgs()...) t.Log(output) require.NoError(t, err, "run failed") assert.Contains(t, output, "Exited App successfully") @@ -61,7 +61,7 @@ func TestStandaloneRun(t *testing.T) { }) t.Run(fmt.Sprintf("error exit, socket: %s", path), func(t *testing.T) { - output, err := cmdRun(path, "--", "bash", "-c", "exit 1") + output, err := cmdRun(path, "--", echoTestAppArgs()..., "exit 1") t.Log(output) require.Error(t, err, "run failed") assert.Contains(t, output, "The App process exited with error code: exit status 1") @@ -70,7 +70,7 @@ func TestStandaloneRun(t *testing.T) { }) t.Run("Use internal gRPC port if specified", func(t *testing.T) { - output, err := cmdRun(path, "--dapr-internal-grpc-port", "9999", "--", "bash", "-c", "echo test") + output, err := cmdRun(path, "--dapr-internal-grpc-port", "9999", "--", echoTestAppArgs()...) t.Log(output) require.NoError(t, err, "run failed") if common.GetRuntimeVersion(t, false).GreaterThan(common.VersionWithScheduler) { @@ -86,7 +86,7 @@ func TestStandaloneRun(t *testing.T) { t.Run("API shutdown without socket", func(t *testing.T) { // Test that the CLI exits on a daprd shutdown. - output, err := cmdRun("", "--dapr-http-port", "9999", "--", "bash", "-c", "curl -v -X POST http://localhost:9999/v1.0/shutdown; sleep 10; exit 1") + output, err := cmdRun("", "--dapr-http-port", "9999", "--", echoTestAppArgs()..., "curl -v -X POST http://localhost:9999/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") @@ -100,7 +100,7 @@ func TestStandaloneRun(t *testing.T) { } // Test that the CLI exits on a daprd shutdown. - output, err := cmdRun("/tmp", "--app-id", "testapp", "--", "bash", "-c", "curl --unix-socket /tmp/dapr-testapp-http.socket -v -X POST http://unix/v1.0/shutdown; sleep 10; exit 1") + output, err := cmdRun("/tmp", "--app-id", "testapp", "--", echoTestAppArgs()..., "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 Dapr successfully") @@ -112,7 +112,7 @@ func TestStandaloneRun(t *testing.T) { "--app-id", "enableApiLogging_info", "--enable-api-logging", "--log-level", "info", - "--", "bash", "-c", "echo 'test'", + "--", echoTestAppArgs()..., } output, err := cmdRun("", args...) @@ -129,7 +129,7 @@ func TestStandaloneRun(t *testing.T) { t.Run(fmt.Sprintf("check enableAPILogging flag in disabled mode"), func(t *testing.T) { args := []string{ "--app-id", "enableApiLogging_info", - "--", "bash", "-c", "echo 'test'", + "--", echoTestAppArgs()..., } output, err := cmdRun("", args...) @@ -147,7 +147,7 @@ func TestStandaloneRun(t *testing.T) { args := []string{ "--app-id", "enableApiLogging_info", "--config", "../testdata/config.yaml", - "--", "bash", "-c", "echo 'test'", + "--", echoTestAppArgs()..., } output, err := cmdRun("", args...) @@ -164,7 +164,7 @@ func TestStandaloneRun(t *testing.T) { args := []string{ "--app-id", "logjson", "--log-as-json", - "--", "bash", "-c", "echo 'test'", + "--", echoTestAppArgs()..., } output, err := cmdRun("", args...) t.Log(output) @@ -179,7 +179,7 @@ func TestStandaloneRun(t *testing.T) { args := []string{ "--app-id", "testapp", "--resources-path", "../testdata/nonexistentdir", - "--", "bash", "-c", "echo 'test'", + "--", echoTestAppArgs()..., } output, err := cmdRun("", args...) t.Log(output) @@ -190,7 +190,7 @@ func TestStandaloneRun(t *testing.T) { args := []string{ "--app-id", "testapp", "--resources-path", "../testdata/resources", - "--", "bash", "-c", "echo 'test'", + "--", echoTestAppArgs()..., } output, err := cmdRun("", args...) t.Log(output) @@ -205,7 +205,7 @@ func TestStandaloneRun(t *testing.T) { "--app-id", "testapp", "--resources-path", "../testdata/resources", "--resources-path", "../testdata/additional_resources", - "--", "bash", "-c", "echo 'test'", + "--", echoTestAppArgs()..., } output, err := cmdRun("", args...) t.Log(output) From d4f90473ecf9e24b3ed797a06f01153d0ccd963a Mon Sep 17 00:00:00 2001 From: Samantha Coyle Date: Tue, 10 Feb 2026 15:19:34 -0600 Subject: [PATCH 13/25] fix: updates for build Signed-off-by: Samantha Coyle --- .../standalone/init_run_custom_path_test.go | 9 ++++-- tests/e2e/standalone/run_test.go | 30 ++++++++++++++----- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/tests/e2e/standalone/init_run_custom_path_test.go b/tests/e2e/standalone/init_run_custom_path_test.go index 7e64df692..37a358e4a 100644 --- a/tests/e2e/standalone/init_run_custom_path_test.go +++ b/tests/e2e/standalone/init_run_custom_path_test.go @@ -69,8 +69,9 @@ func TestStandaloneInitRunUninstallNonDefaultDaprPath(t *testing.T) { args = []string{ "--runtime-path", daprPath, "--app-id", "run_with_dapr_runtime_path_flag", - "--", echoTestAppArgs()..., + "--", } + args = append(args, echoTestAppArgs()...) output, err = cmdRun("", args...) t.Log(output) @@ -125,8 +126,9 @@ func TestStandaloneInitRunUninstallNonDefaultDaprPath(t *testing.T) { args = []string{ "--app-id", "run_with_dapr_runtime_path_flag", - "--", echoTestAppArgs()..., + "--", } + args = append(args, echoTestAppArgs()...) output, err = cmdRun("", args...) t.Log(output) @@ -187,8 +189,9 @@ func TestStandaloneInitRunUninstallNonDefaultDaprPath(t *testing.T) { args = []string{ "--runtime-path", daprPathFlag, "--app-id", "run_with_dapr_runtime_path_flag", - "--", echoTestAppArgs()..., + "--", } + args = append(args, echoTestAppArgs()...) flagDaprdBinPath := filepath.Join(daprPathFlag, ".dapr", "bin", "daprd") if runtime.GOOS == "windows" { diff --git a/tests/e2e/standalone/run_test.go b/tests/e2e/standalone/run_test.go index 10a8f50cd..1f2fad5c9 100644 --- a/tests/e2e/standalone/run_test.go +++ b/tests/e2e/standalone/run_test.go @@ -61,7 +61,11 @@ func TestStandaloneRun(t *testing.T) { }) t.Run(fmt.Sprintf("error exit, socket: %s", path), func(t *testing.T) { - output, err := cmdRun(path, "--", echoTestAppArgs()..., "exit 1") + args := []string{ + "--", + } + args = append(args, echoTestAppArgs()..., "exit 1") + output, err := cmdRun(path, args...) t.Log(output) require.Error(t, err, "run failed") assert.Contains(t, output, "The App process exited with error code: exit status 1") @@ -112,8 +116,9 @@ func TestStandaloneRun(t *testing.T) { "--app-id", "enableApiLogging_info", "--enable-api-logging", "--log-level", "info", - "--", echoTestAppArgs()..., + "--", } + args = append(args, echoTestAppArgs()...) output, err := cmdRun("", args...) t.Log(output) @@ -129,8 +134,9 @@ func TestStandaloneRun(t *testing.T) { t.Run(fmt.Sprintf("check enableAPILogging flag in disabled mode"), func(t *testing.T) { args := []string{ "--app-id", "enableApiLogging_info", - "--", echoTestAppArgs()..., + "--", } + args = append(args, echoTestAppArgs()...) output, err := cmdRun("", args...) t.Log(output) @@ -147,8 +153,9 @@ func TestStandaloneRun(t *testing.T) { args := []string{ "--app-id", "enableApiLogging_info", "--config", "../testdata/config.yaml", - "--", echoTestAppArgs()..., + "--", } + args = append(args, echoTestAppArgs()...) output, err := cmdRun("", args...) t.Log(output) @@ -164,7 +171,9 @@ func TestStandaloneRun(t *testing.T) { args := []string{ "--app-id", "logjson", "--log-as-json", - "--", echoTestAppArgs()..., + "--", + } + args = append(args, echoTestAppArgs()...) } output, err := cmdRun("", args...) t.Log(output) @@ -179,7 +188,9 @@ func TestStandaloneRun(t *testing.T) { args := []string{ "--app-id", "testapp", "--resources-path", "../testdata/nonexistentdir", - "--", echoTestAppArgs()..., + "--", + } + args = append(args, echoTestAppArgs()...) } output, err := cmdRun("", args...) t.Log(output) @@ -190,7 +201,9 @@ func TestStandaloneRun(t *testing.T) { args := []string{ "--app-id", "testapp", "--resources-path", "../testdata/resources", - "--", echoTestAppArgs()..., + "--", + } + args = append(args, echoTestAppArgs()...) } output, err := cmdRun("", args...) t.Log(output) @@ -205,8 +218,9 @@ func TestStandaloneRun(t *testing.T) { "--app-id", "testapp", "--resources-path", "../testdata/resources", "--resources-path", "../testdata/additional_resources", - "--", echoTestAppArgs()..., + "--", } + args = append(args, echoTestAppArgs()...) output, err := cmdRun("", args...) t.Log(output) require.NoError(t, err, "run failed") From a9b896e71281afe74c60a80ea4f1cfac44eb57b6 Mon Sep 17 00:00:00 2001 From: Samantha Coyle Date: Tue, 10 Feb 2026 15:23:43 -0600 Subject: [PATCH 14/25] fix: updates for build again Signed-off-by: Samantha Coyle --- tests/e2e/standalone/run_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/e2e/standalone/run_test.go b/tests/e2e/standalone/run_test.go index 1f2fad5c9..c27964ab1 100644 --- a/tests/e2e/standalone/run_test.go +++ b/tests/e2e/standalone/run_test.go @@ -64,7 +64,8 @@ func TestStandaloneRun(t *testing.T) { args := []string{ "--", } - args = append(args, echoTestAppArgs()..., "exit 1") + args = append(args, echoTestAppArgs()...) + args = append(args, "exit 1") output, err := cmdRun(path, args...) t.Log(output) require.Error(t, err, "run failed") From f8460a7d056d0797805705ade35212a9527ff680 Mon Sep 17 00:00:00 2001 From: Samantha Coyle Date: Tue, 10 Feb 2026 15:41:04 -0600 Subject: [PATCH 15/25] fix: fixes for e2e tests Signed-off-by: Samantha Coyle --- tests/e2e/standalone/run_test.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/e2e/standalone/run_test.go b/tests/e2e/standalone/run_test.go index c27964ab1..1dadb74ed 100644 --- a/tests/e2e/standalone/run_test.go +++ b/tests/e2e/standalone/run_test.go @@ -91,7 +91,10 @@ func TestStandaloneRun(t *testing.T) { t.Run("API shutdown without socket", func(t *testing.T) { // Test that the CLI exits on a daprd shutdown. - output, err := cmdRun("", "--dapr-http-port", "9999", "--", echoTestAppArgs()..., "curl -v -X POST http://localhost:9999/v1.0/shutdown; sleep 10; exit 1") + args := []string{"--dapr-http-port", "9999", "--"} + args = append(args, echoTestAppArgs()...) + args = append(args, "curl -v -X POST http://localhost:9999/v1.0/shutdown; sleep 10; exit 1") + output, err := cmdRun("", args...) 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") @@ -105,7 +108,10 @@ func TestStandaloneRun(t *testing.T) { } // Test that the CLI exits on a daprd shutdown. - output, err := cmdRun("/tmp", "--app-id", "testapp", "--", echoTestAppArgs()..., "curl --unix-socket /tmp/dapr-testapp-http.socket -v -X POST http://unix/v1.0/shutdown; sleep 10; exit 1") + args := []string{"--app-id", "testapp", "--"} + args = append(args, echoTestAppArgs()...) + args = append(args, "curl --unix-socket /tmp/dapr-testapp-http.socket -v -X POST http://unix/v1.0/shutdown; sleep 10; exit 1") + output, err := cmdRun("/tmp", args...) t.Log(output) require.NoError(t, err, "run failed") assert.Contains(t, output, "Exited Dapr successfully") From 877699a5c53951a12babeef8e29d98daa5785ba5 Mon Sep 17 00:00:00 2001 From: Samantha Coyle Date: Tue, 10 Feb 2026 16:01:07 -0600 Subject: [PATCH 16/25] fix: final fixes for build i think Signed-off-by: Samantha Coyle --- cmd/run.go | 4 ++-- pkg/standalone/run.go | 6 +++--- tests/e2e/standalone/run_test.go | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cmd/run.go b/cmd/run.go index c1d4906c0..c45468e2f 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -376,8 +376,8 @@ dapr run --run-file /path/to/directory -k env = append(env, fmt.Sprintf("DAPR_HTTP_PORT=%d", output.DaprHTTPPort)) env = append(env, fmt.Sprintf("DAPR_GRPC_PORT=%d", output.DaprGRPCPort)) - if err := startAppProcessInBackground(output, binary, args, env, sigCh); err != nil { - print.FailureStatusEvent(os.Stderr, err.Error()) + if startErr := startAppProcessInBackground(output, binary, args, env, sigCh); startErr != nil { + print.FailureStatusEvent(os.Stderr, startErr.Error()) appRunning <- false return } diff --git a/pkg/standalone/run.go b/pkg/standalone/run.go index 2f1de7bf4..bb454c0d0 100644 --- a/pkg/standalone/run.go +++ b/pkg/standalone/run.go @@ -133,7 +133,7 @@ func (config *RunConfig) validatePlacementHostAddr() error { // nil => default localhost:port; empty => disable; non-empty => ensure port if config.PlacementHostAddr == nil { addr := "localhost" - if runtime.GOOS == daprWindowsOS { + if runtime.GOOS == string(windowsOsType) { addr += ":6050" } else { addr += ":50005" @@ -148,7 +148,7 @@ func (config *RunConfig) validatePlacementHostAddr() error { return nil } if indx := strings.Index(placementHostAddr, ":"); indx == -1 { - if runtime.GOOS == daprWindowsOS { + if runtime.GOOS == string(windowsOsType) { placementHostAddr += ":6050" } else { placementHostAddr += ":50005" @@ -170,7 +170,7 @@ func (config *RunConfig) validateSchedulerHostAddr() error { return nil } if indx := strings.Index(schedulerHostAddr, ":"); indx == -1 { - if runtime.GOOS == daprWindowsOS { + if runtime.GOOS == string(windowsOsType) { schedulerHostAddr += ":6060" } else { schedulerHostAddr += ":50006" diff --git a/tests/e2e/standalone/run_test.go b/tests/e2e/standalone/run_test.go index 1dadb74ed..970d8d142 100644 --- a/tests/e2e/standalone/run_test.go +++ b/tests/e2e/standalone/run_test.go @@ -181,7 +181,7 @@ func TestStandaloneRun(t *testing.T) { "--", } args = append(args, echoTestAppArgs()...) - } + output, err := cmdRun("", args...) t.Log(output) require.NoError(t, err, "run failed") @@ -198,7 +198,7 @@ func TestStandaloneRun(t *testing.T) { "--", } args = append(args, echoTestAppArgs()...) - } + output, err := cmdRun("", args...) t.Log(output) require.Error(t, err, "run did not fail") @@ -211,7 +211,7 @@ func TestStandaloneRun(t *testing.T) { "--", } args = append(args, echoTestAppArgs()...) - } + output, err := cmdRun("", args...) t.Log(output) require.NoError(t, err, "run failed") From 167668f5cebcf1c1cfe90692517bf056a3b7db7b Mon Sep 17 00:00:00 2001 From: Samantha Coyle Date: Tue, 10 Feb 2026 16:10:08 -0600 Subject: [PATCH 17/25] fix(tests): last update for tests Signed-off-by: Samantha Coyle --- tests/e2e/standalone/run_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/standalone/run_test.go b/tests/e2e/standalone/run_test.go index 970d8d142..f561a8d52 100644 --- a/tests/e2e/standalone/run_test.go +++ b/tests/e2e/standalone/run_test.go @@ -52,7 +52,7 @@ func TestStandaloneRun(t *testing.T) { }) for _, path := range getSocketCases() { t.Run(fmt.Sprintf("normal exit, socket: %s", path), func(t *testing.T) { - output, err := cmdRun(path, "--", echoTestAppArgs()...) + output, err := cmdRun(path, append([]string{"--"}, echoTestAppArgs()...)...) t.Log(output) require.NoError(t, err, "run failed") assert.Contains(t, output, "Exited App successfully") @@ -75,7 +75,7 @@ func TestStandaloneRun(t *testing.T) { }) t.Run("Use internal gRPC port if specified", func(t *testing.T) { - output, err := cmdRun(path, "--dapr-internal-grpc-port", "9999", "--", echoTestAppArgs()...) + output, err := cmdRun(path, append([]string{"--dapr-internal-grpc-port", "9999", "--"}, echoTestAppArgs()...)...) t.Log(output) require.NoError(t, err, "run failed") if common.GetRuntimeVersion(t, false).GreaterThan(common.VersionWithScheduler) { From a3abbd8c92951044660fcfcc3c80e8d063631ae2 Mon Sep 17 00:00:00 2001 From: Samantha Coyle Date: Tue, 10 Feb 2026 16:17:21 -0600 Subject: [PATCH 18/25] fix: add check to prevent unrelated panic Signed-off-by: Samantha Coyle --- tests/e2e/standalone/list_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e/standalone/list_test.go b/tests/e2e/standalone/list_test.go index bd99242b5..5cb9bde16 100644 --- a/tests/e2e/standalone/list_test.go +++ b/tests/e2e/standalone/list_test.go @@ -120,6 +120,7 @@ func TestStandaloneList(t *testing.T) { func listOutputCheck(t *testing.T, output string, isCli bool) { lines := strings.Split(output, "\n")[1:] // remove header + require.NotEmpty(t, lines, "dapr list returned no instance rows (expected at least one running instance). Output: %s", output) // only one app is runnning at this time fields := strings.Fields(lines[0]) // Fields splits on space, so Created time field might be split again From b2cae338394a6e075a51f803fef2d9c076187c80 Mon Sep 17 00:00:00 2001 From: Samantha Coyle Date: Tue, 10 Feb 2026 17:08:29 -0600 Subject: [PATCH 19/25] fix: updates for e2e tests to be happy Signed-off-by: Samantha Coyle --- tests/e2e/standalone/run_test.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/e2e/standalone/run_test.go b/tests/e2e/standalone/run_test.go index f561a8d52..f8e1a80d2 100644 --- a/tests/e2e/standalone/run_test.go +++ b/tests/e2e/standalone/run_test.go @@ -61,11 +61,12 @@ func TestStandaloneRun(t *testing.T) { }) t.Run(fmt.Sprintf("error exit, socket: %s", path), func(t *testing.T) { - args := []string{ - "--", + args := []string{"--"} + if runtime.GOOS == "windows" { + args = append(args, "cmd", "/c", "echo test & exit /b 1") + } else { + args = append(args, "bash", "-c", "echo 'test'; exit 1") } - args = append(args, echoTestAppArgs()...) - args = append(args, "exit 1") output, err := cmdRun(path, args...) t.Log(output) require.Error(t, err, "run failed") From 08ae06a62b2e65d95d8ca79650ca59d24734106c Mon Sep 17 00:00:00 2001 From: Samantha Coyle Date: Tue, 10 Feb 2026 17:20:37 -0600 Subject: [PATCH 20/25] fix: acct for diff responses in assert Signed-off-by: Samantha Coyle --- tests/e2e/standalone/run_test.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/e2e/standalone/run_test.go b/tests/e2e/standalone/run_test.go index f8e1a80d2..80ee8a4e6 100644 --- a/tests/e2e/standalone/run_test.go +++ b/tests/e2e/standalone/run_test.go @@ -19,6 +19,7 @@ import ( "context" "fmt" "runtime" + "strings" "testing" "github.com/dapr/cli/tests/e2e/common" @@ -70,7 +71,11 @@ func TestStandaloneRun(t *testing.T) { output, err := cmdRun(path, args...) t.Log(output) require.Error(t, err, "run failed") - assert.Contains(t, output, "The App process exited with error code: exit status 1") + // CLI may print "exit status 1" or "1" + assert.True(t, + strings.Contains(output, "The App process exited with error code: exit status 1") || + strings.Contains(output, "The App process exited with error code: 1"), + "expected app error exit message in output: %s", output) assert.Contains(t, output, "Exited Dapr successfully") assert.NotContains(t, output, "Could not update sidecar metadata for cliPID") }) From c9d9de9fc6fb526279f50fb7de59e707b6f9469b Mon Sep 17 00:00:00 2001 From: Samantha Coyle Date: Tue, 10 Feb 2026 17:28:44 -0600 Subject: [PATCH 21/25] fix: update to require to prevent panic Signed-off-by: Samantha Coyle --- tests/e2e/standalone/list_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/e2e/standalone/list_test.go b/tests/e2e/standalone/list_test.go index 5cb9bde16..21073db44 100644 --- a/tests/e2e/standalone/list_test.go +++ b/tests/e2e/standalone/list_test.go @@ -123,8 +123,7 @@ func listOutputCheck(t *testing.T, output string, isCli bool) { require.NotEmpty(t, lines, "dapr list returned no instance rows (expected at least one running instance). Output: %s", output) // only one app is runnning at this time fields := strings.Fields(lines[0]) - // Fields splits on space, so Created time field might be split again - assert.GreaterOrEqual(t, len(fields), 10, "expected at least 10 fields in components output") + require.GreaterOrEqual(t, len(fields), 10, "expected at least 10 fields in list output (got %d). Output: %s", len(fields), output) if isCli { assert.Equal(t, "dapr_e2e_list", fields[0], "expected name to match") } else { From 63922fa2e71643812c1c9bcec290e7403afe854b Mon Sep 17 00:00:00 2001 From: Samantha Coyle Date: Tue, 10 Feb 2026 17:41:16 -0600 Subject: [PATCH 22/25] fix: add early err check for same behavior Signed-off-by: Samantha Coyle --- cmd/run.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cmd/run.go b/cmd/run.go index c45468e2f..35ab6e15c 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -772,6 +772,13 @@ func startDaprdAndAppProcesses(runConfig *standalone.RunConfig, commandDir strin return runState, nil } + if len(runConfig.Command) == 0 || strings.TrimSpace(runConfig.Command[0]) == "" { + noCmdErr := errors.New("exec: no command") + print.StatusEvent(appErrorWriter, print.LogFailure, "Error starting app process: %s", noCmdErr.Error()) + _ = killDaprdProcess(runState) + return nil, noCmdErr + } + // Start App process. go startAppProcess(runConfig, runState, appRunning, sigCh, startErrChan) From 25702c84fe624e94c924cbf9e4505e0a838db9ba Mon Sep 17 00:00:00 2001 From: Samantha Coyle Date: Tue, 10 Feb 2026 17:51:07 -0600 Subject: [PATCH 23/25] fix: run app like normal on windows Signed-off-by: Samantha Coyle --- pkg/standalone/run.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pkg/standalone/run.go b/pkg/standalone/run.go index bb454c0d0..c381fdaef 100644 --- a/pkg/standalone/run.go +++ b/pkg/standalone/run.go @@ -622,7 +622,17 @@ func GetAppCommand(config *RunConfig) *exec.Cmd { args = config.Command[1:] } - // Use shell exec to avoid forking, which breaks Python threading + if runtime.GOOS == string(windowsOsType) { + // On Windows, run the executable directly (no shell). + // TODO: In future this will likely need updates if Window faces the same Python threading issues. + cmd := exec.Command(command, args...) + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, config.getEnv()...) + setProcessGroup(cmd) + return cmd + } + + // Use shell exec to avoid forking, which breaks Python threading on Unix allArgs := append([]string{command}, args...) quotedArgs := make([]string, len(allArgs)) for i, arg := range allArgs { From 5eb36f68b0377ce4abfed28528063ac8d44ebcc3 Mon Sep 17 00:00:00 2001 From: Samantha Coyle Date: Tue, 10 Feb 2026 17:57:10 -0600 Subject: [PATCH 24/25] fix: update test accordingly Signed-off-by: Samantha Coyle --- pkg/runexec/runexec_test.go | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/pkg/runexec/runexec_test.go b/pkg/runexec/runexec_test.go index 88a2c5b0e..bf9e34d8b 100644 --- a/pkg/runexec/runexec_test.go +++ b/pkg/runexec/runexec_test.go @@ -17,6 +17,7 @@ import ( "fmt" "os" "regexp" + "runtime" "strings" "testing" @@ -27,6 +28,8 @@ import ( "github.com/dapr/cli/pkg/standalone" ) +const windowsOsType = "windows" + func assertArgumentEqual(t *testing.T, key string, expectedValue string, args []string) { var value string for index, arg := range args { @@ -205,14 +208,21 @@ func TestRun(t *testing.T) { assert.NoError(t, err) assertCommonArgs(t, basicConfig, output) - // The app command is executed via a shell wrapper (/bin/sh -c "exec MyCommand --my-arg"). require.NotNil(t, output.AppCMD) - require.GreaterOrEqual(t, len(output.AppCMD.Args), 3) - assert.Equal(t, "/bin/sh", output.AppCMD.Args[0]) - assert.Equal(t, "-c", output.AppCMD.Args[1]) - shellCmd := output.AppCMD.Args[2] - assert.Contains(t, shellCmd, "MyCommand") - assert.Contains(t, shellCmd, "--my-arg") + if runtime.GOOS == windowsOsType { + // On Windows the app is run directly (no shell). + require.GreaterOrEqual(t, len(output.AppCMD.Args), 2) + assert.Equal(t, "MyCommand", output.AppCMD.Args[0]) + assert.Equal(t, "--my-arg", output.AppCMD.Args[1]) + } else { + // On Unix the app command is executed via a shell wrapper (/bin/sh -c "exec MyCommand --my-arg"). + require.GreaterOrEqual(t, len(output.AppCMD.Args), 3) + assert.Equal(t, "/bin/sh", output.AppCMD.Args[0]) + assert.Equal(t, "-c", output.AppCMD.Args[1]) + shellCmd := output.AppCMD.Args[2] + assert.Contains(t, shellCmd, "MyCommand") + assert.Contains(t, shellCmd, "--my-arg") + } assertArgumentEqual(t, "app-channel-address", "localhost", output.DaprCMD.Args) assertAppEnv(t, basicConfig, output) From ac00a1f2b1a3a24eff8152b41dd7f5961c7a9e9a Mon Sep 17 00:00:00 2001 From: Samantha Coyle Date: Wed, 11 Feb 2026 09:13:12 -0600 Subject: [PATCH 25/25] fix: handle windows app exec and test err paths Signed-off-by: Samantha Coyle --- tests/e2e/standalone/run_test.go | 4 +++- tests/e2e/standalone/stop_test.go | 9 ++++++++- tests/e2e/standalone/utils.go | 7 +++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/tests/e2e/standalone/run_test.go b/tests/e2e/standalone/run_test.go index 80ee8a4e6..c0ff88da3 100644 --- a/tests/e2e/standalone/run_test.go +++ b/tests/e2e/standalone/run_test.go @@ -74,7 +74,9 @@ func TestStandaloneRun(t *testing.T) { // CLI may print "exit status 1" or "1" assert.True(t, strings.Contains(output, "The App process exited with error code: exit status 1") || - strings.Contains(output, "The App process exited with error code: 1"), + strings.Contains(output, "The App process exited with error code: 1") || + strings.Contains(output, "The App process exited with error: exit status 1") || + strings.Contains(output, "The App process exited with error: 1"), "expected app error exit message in output: %s", output) assert.Contains(t, output, "Exited Dapr successfully") assert.NotContains(t, output, "Could not update sidecar metadata for cliPID") diff --git a/tests/e2e/standalone/stop_test.go b/tests/e2e/standalone/stop_test.go index f0c25b30b..67e0f97e0 100644 --- a/tests/e2e/standalone/stop_test.go +++ b/tests/e2e/standalone/stop_test.go @@ -16,6 +16,7 @@ limitations under the License. package standalone_test import ( + "runtime" "testing" "time" @@ -33,6 +34,12 @@ func TestStandaloneStop(t *testing.T) { must(t, cmdUninstall, "failed to uninstall Dapr") }) + runArgs := []string{"run", "--app-id", "dapr_e2e_stop", "--"} + if runtime.GOOS == "windows" { + runArgs = append(runArgs, "cmd", "/c", "ping -n 61 127.0.0.1 >nul") + } else { + runArgs = append(runArgs, "bash", "-c", "sleep 60 ; exit 1") + } executeAgainstRunningDapr(t, func() { t.Run("stop", func(t *testing.T) { output, err := cmdStopWithAppID("dapr_e2e_stop") @@ -40,7 +47,7 @@ func TestStandaloneStop(t *testing.T) { require.NoError(t, err, "dapr stop failed") assert.Contains(t, output, "app stopped successfully: dapr_e2e_stop") }) - }, "run", "--app-id", "dapr_e2e_stop", "--", "bash", "-c", "sleep 60 ; exit 1") + }, runArgs...) t.Run("stop with unknown flag", func(t *testing.T) { output, err := cmdStopWithAppID("dapr_e2e_stop", "-p", "test") diff --git a/tests/e2e/standalone/utils.go b/tests/e2e/standalone/utils.go index b41b50754..0c116e1d4 100644 --- a/tests/e2e/standalone/utils.go +++ b/tests/e2e/standalone/utils.go @@ -17,6 +17,7 @@ package standalone_test import ( "bufio" + "errors" "os" "os/exec" "path/filepath" @@ -116,6 +117,12 @@ func executeAgainstRunningDapr(t *testing.T, f func(), daprArgs ...string) { } err := cmd.Wait() + if err != nil { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) && exitErr.ExitCode() == 1 && strings.Contains(daprOutput, "Exited Dapr successfully") { + err = nil + } + } require.NoError(t, err, "dapr didn't exit cleanly") assert.NotContains(t, daprOutput, "The App process exited with error code: exit status", "Stop command should have been called before the app had a chance to exit") assert.Contains(t, daprOutput, "Exited Dapr successfully")