diff --git a/cmd/run.go b/cmd/run.go index 1c6cf4c8c..35ab6e15c 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -15,9 +15,11 @@ package cmd import ( "bufio" + "errors" "fmt" "io" "os" + "os/exec" "path/filepath" "runtime" "slices" @@ -160,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) } @@ -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,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 app process. + setDaprProcessGroupForRun(output.DaprCMD) err = output.DaprCMD.Start() if err != nil { @@ -355,53 +360,27 @@ 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] + 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 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)) } + env = append(env, fmt.Sprintf("DAPR_HTTP_PORT=%d", output.DaprHTTPPort)) + env = append(env, fmt.Sprintf("DAPR_GRPC_PORT=%d", output.DaprGRPCPort)) - 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() - if 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 } - - 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()) - } else { - print.SuccessStatusEvent(os.Stdout, "Exited App successfully") - } - sigCh <- os.Interrupt - }() - appRunning <- true }() @@ -468,8 +447,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") } @@ -788,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) 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/runexec/runexec_test.go b/pkg/runexec/runexec_test.go index 740cdb159..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,8 +208,22 @@ 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]) + require.NotNil(t, output.AppCMD) + 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) }) 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..a1967676d --- /dev/null +++ b/pkg/standalone/process_group_windows.go @@ -0,0 +1,26 @@ +//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" + +// 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 078a3543c..c381fdaef 100644 --- a/pkg/standalone/run.go +++ b/pkg/standalone/run.go @@ -35,6 +35,7 @@ import ( ) type LogDestType string +type osType string const ( Console LogDestType = "console" @@ -45,6 +46,8 @@ const ( sentryDefaultAddress = "localhost:50001" defaultStructTagKey = "default" + + windowsOsType osType = "windows" ) // RunConfig represents the application configuration parameters. @@ -130,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" @@ -145,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" @@ -167,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" @@ -619,9 +622,27 @@ func GetAppCommand(config *RunConfig) *exec.Cmd { args = config.Command[1:] } - cmd := exec.Command(command, args...) + 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 { + 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()...) + setProcessGroup(cmd) return cmd } diff --git a/tests/e2e/standalone/init_run_custom_path_test.go b/tests/e2e/standalone/init_run_custom_path_test.go index da7550a89..37a358e4a 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,8 +69,9 @@ func TestStandaloneInitRunUninstallNonDefaultDaprPath(t *testing.T) { args = []string{ "--runtime-path", daprPath, "--app-id", "run_with_dapr_runtime_path_flag", - "--", "bash", "-c", "echo 'test'", + "--", } + args = append(args, echoTestAppArgs()...) output, err = cmdRun("", args...) t.Log(output) @@ -117,8 +126,9 @@ func TestStandaloneInitRunUninstallNonDefaultDaprPath(t *testing.T) { args = []string{ "--app-id", "run_with_dapr_runtime_path_flag", - "--", "bash", "-c", "echo 'test'", + "--", } + args = append(args, echoTestAppArgs()...) output, err = cmdRun("", args...) t.Log(output) @@ -179,8 +189,9 @@ func TestStandaloneInitRunUninstallNonDefaultDaprPath(t *testing.T) { args = []string{ "--runtime-path", daprPathFlag, "--app-id", "run_with_dapr_runtime_path_flag", - "--", "bash", "-c", "echo 'test'", + "--", } + args = append(args, echoTestAppArgs()...) flagDaprdBinPath := filepath.Join(daprPathFlag, ".dapr", "bin", "daprd") if runtime.GOOS == "windows" { diff --git a/tests/e2e/standalone/list_test.go b/tests/e2e/standalone/list_test.go index bd99242b5..21073db44 100644 --- a/tests/e2e/standalone/list_test.go +++ b/tests/e2e/standalone/list_test.go @@ -120,10 +120,10 @@ 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 - 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 { diff --git a/tests/e2e/standalone/run_test.go b/tests/e2e/standalone/run_test.go index d4260aa9e..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" @@ -52,7 +53,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, append([]string{"--"}, echoTestAppArgs()...)...) t.Log(output) require.NoError(t, err, "run failed") assert.Contains(t, output, "Exited App successfully") @@ -61,16 +62,26 @@ 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") + 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") + } + 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") }) 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, 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) { @@ -86,7 +97,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", "--", "bash", "-c", "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") @@ -100,7 +114,10 @@ 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") + 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") @@ -112,8 +129,9 @@ func TestStandaloneRun(t *testing.T) { "--app-id", "enableApiLogging_info", "--enable-api-logging", "--log-level", "info", - "--", "bash", "-c", "echo 'test'", + "--", } + args = append(args, echoTestAppArgs()...) output, err := cmdRun("", args...) t.Log(output) @@ -129,8 +147,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", - "--", "bash", "-c", "echo 'test'", + "--", } + args = append(args, echoTestAppArgs()...) output, err := cmdRun("", args...) t.Log(output) @@ -147,8 +166,9 @@ func TestStandaloneRun(t *testing.T) { args := []string{ "--app-id", "enableApiLogging_info", "--config", "../testdata/config.yaml", - "--", "bash", "-c", "echo 'test'", + "--", } + args = append(args, echoTestAppArgs()...) output, err := cmdRun("", args...) t.Log(output) @@ -164,8 +184,10 @@ func TestStandaloneRun(t *testing.T) { args := []string{ "--app-id", "logjson", "--log-as-json", - "--", "bash", "-c", "echo 'test'", + "--", } + args = append(args, echoTestAppArgs()...) + output, err := cmdRun("", args...) t.Log(output) require.NoError(t, err, "run failed") @@ -179,8 +201,10 @@ func TestStandaloneRun(t *testing.T) { args := []string{ "--app-id", "testapp", "--resources-path", "../testdata/nonexistentdir", - "--", "bash", "-c", "echo 'test'", + "--", } + args = append(args, echoTestAppArgs()...) + output, err := cmdRun("", args...) t.Log(output) require.Error(t, err, "run did not fail") @@ -190,8 +214,10 @@ func TestStandaloneRun(t *testing.T) { args := []string{ "--app-id", "testapp", "--resources-path", "../testdata/resources", - "--", "bash", "-c", "echo 'test'", + "--", } + args = append(args, echoTestAppArgs()...) + output, err := cmdRun("", args...) t.Log(output) require.NoError(t, err, "run failed") @@ -205,8 +231,9 @@ func TestStandaloneRun(t *testing.T) { "--app-id", "testapp", "--resources-path", "../testdata/resources", "--resources-path", "../testdata/additional_resources", - "--", "bash", "-c", "echo 'test'", + "--", } + args = append(args, echoTestAppArgs()...) output, err := cmdRun("", args...) t.Log(output) require.NoError(t, err, "run failed")