From 6738eefe2b854a5d3e1918524279769ef3010b3c Mon Sep 17 00:00:00 2001 From: Pravin Pushkar Date: Wed, 30 Aug 2023 11:06:40 +0530 Subject: [PATCH] Multiapp run and stop implementation for windows (#1315) * windows impl for multiapp run Signed-off-by: Pravin Pushkar * Uncommenting run on windows Signed-off-by: Pravin Pushkar * fix static checks Signed-off-by: Pravin Pushkar * Kill children and grand children forcefully Signed-off-by: Pravin Pushkar * ommiting tests for wiondows Signed-off-by: Pravin Pushkar * rename method Signed-off-by: Pravin Pushkar * shut down all processes Signed-off-by: Pravin Pushkar * Use job handle and named evens together to kill the processes Signed-off-by: Pravin Pushkar * lint fix Signed-off-by: Pravin Pushkar * revert wait to kill Signed-off-by: Pravin Pushkar * Adding E2E for windows template file run Signed-off-by: Pravin Pushkar * rename job name Signed-off-by: Pravin Pushkar * review comments Signed-off-by: Pravin Pushkar * build failure Signed-off-by: Pravin Pushkar --------- Signed-off-by: Pravin Pushkar Co-authored-by: Mukundan Sundararajan <65565396+mukundansundar@users.noreply.github.com> --- cmd/run.go | 6 +- cmd/stop.go | 5 - go.mod | 1 + go.sum | 8 + pkg/standalone/stop_windows.go | 52 +++++-- pkg/syscall/syscall.go | 6 + pkg/syscall/syscall_windows.go | 47 +++++- tests/e2e/standalone/run_template_test.go | 10 +- .../standalone/stop_with_run_template_test.go | 7 +- .../standalone/windows_run_template_test.go | 144 ++++++++++++++++++ .../app_output_to_only_console.yaml | 14 ++ utils/utils.go | 15 ++ 12 files changed, 280 insertions(+), 35 deletions(-) create mode 100644 tests/e2e/standalone/windows_run_template_test.go create mode 100644 tests/e2e/testdata/run-template-files/app_output_to_only_console.yaml diff --git a/cmd/run.go b/cmd/run.go index 8674061bb..bf89f68a2 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -112,10 +112,6 @@ dapr run --run-file /path/to/directory }, Run: func(cmd *cobra.Command, args []string) { if len(runFilePath) > 0 { - if runtime.GOOS == string(windowsOsType) { - print.FailureStatusEvent(os.Stderr, "The run command with run file is not supported on Windows") - os.Exit(1) - } runConfigFilePath, err := getRunFilePath(runFilePath) if err != nil { print.FailureStatusEvent(os.Stderr, "Failed to get run file path: %v", err) @@ -562,6 +558,8 @@ func executeRun(runTemplateName, runFilePath string, apps []runfileconfig.App) ( if runState.AppCMD.Command.Process != nil { putAppProcessIDInMeta(runState) + // Attach a windows job object to the app process. + utils.AttachJobObjectToProcess(strconv.Itoa(os.Getpid()), runState.AppCMD.Command.Process) } } diff --git a/cmd/stop.go b/cmd/stop.go index bdfe8be48..3540bac90 100644 --- a/cmd/stop.go +++ b/cmd/stop.go @@ -17,7 +17,6 @@ import ( "fmt" "os" "path/filepath" - "runtime" "github.com/spf13/cobra" @@ -43,10 +42,6 @@ dapr stop --run-file /path/to/directory Run: func(cmd *cobra.Command, args []string) { var err error if len(runFilePath) > 0 { - if runtime.GOOS == string(windowsOsType) { - print.FailureStatusEvent(os.Stderr, "Stop command with run file is not supported on Windows") - os.Exit(1) - } runFilePath, err = getRunFilePath(runFilePath) if err != nil { print.FailureStatusEvent(os.Stderr, "Failed to get run file path: %v", err) diff --git a/go.mod b/go.mod index a903106b9..97548b5f0 100644 --- a/go.mod +++ b/go.mod @@ -130,6 +130,7 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.16.3 // indirect + github.com/kolesnikovae/go-winjob v1.0.0 github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/lestrrat-go/blackmagic v1.0.1 // indirect diff --git a/go.sum b/go.sum index bf91ca344..86a97a55e 100644 --- a/go.sum +++ b/go.sum @@ -119,6 +119,7 @@ github.com/Microsoft/hcsshim v0.9.6/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfy github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU= github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/Pallinder/sillyname-go v0.0.0-20130730142914-97aeae9e6ba1 h1:ReSY7H5Nf08bSzShfWAUTCthIsK08iNitWGX5YFQGXE= github.com/Pallinder/sillyname-go v0.0.0-20130730142914-97aeae9e6ba1/go.mod h1:cTmXjiBQMtbZnpc/yLode6SPqKmzeL7xJlD+9R9zxoc= @@ -417,6 +418,7 @@ github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0= +github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -715,6 +717,7 @@ github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= @@ -739,6 +742,8 @@ github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYs github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY= github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/kolesnikovae/go-winjob v1.0.0 h1:OKEtCHB3sYNAiqNwGDhf08Y6luM7C8mP+42rp1N6SeE= +github.com/kolesnikovae/go-winjob v1.0.0/go.mod h1:k0joOLP3/NBrRmDQjPV2+oN1TPmEWt6arTNtFjVeQuM= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -1049,6 +1054,7 @@ github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= @@ -1069,6 +1075,7 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1 github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg= github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -1139,6 +1146,7 @@ github.com/tklauser/numcpus v0.4.0 h1:E53Dm1HjH1/R2/aoCtXtPgzmElmn51aOkhCFSuZq// github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= diff --git a/pkg/standalone/stop_windows.go b/pkg/standalone/stop_windows.go index b12bc5c58..b5d40ce36 100644 --- a/pkg/standalone/stop_windows.go +++ b/pkg/standalone/stop_windows.go @@ -14,10 +14,13 @@ limitations under the License. package standalone import ( - "errors" "fmt" + "strconv" "syscall" + "time" + "github.com/dapr/cli/utils" + "github.com/kolesnikovae/go-winjob" "golang.org/x/sys/windows" ) @@ -25,20 +28,47 @@ import ( func Stop(appID string, cliPIDToNoOfApps map[int]int, apps []ListOutput) error { for _, a := range apps { if a.AppID == appID { - eventName, _ := syscall.UTF16FromString(fmt.Sprintf("dapr_cli_%v", a.CliPID)) - eventHandle, err := windows.OpenEvent(windows.EVENT_MODIFY_STATE, false, &eventName[0]) - if err != nil { - return err - } - - err = windows.SetEvent(eventHandle) - return err + return setStopEvent(a.CliPID) } } return fmt.Errorf("couldn't find app id %s", appID) } // StopAppsWithRunFile terminates the daprd and application processes with the given run file. -func StopAppsWithRunFile(runFilePath string) error { - return errors.New("stopping apps with run template file is not supported on windows") +func StopAppsWithRunFile(runTemplatePath string) error { + apps, err := List() + if err != nil { + return err + } + for _, a := range apps { + if a.RunTemplatePath == runTemplatePath { + return disposeJobHandle(a.CliPID) + } + } + return fmt.Errorf("couldn't find apps with run file %q", runTemplatePath) +} + +func disposeJobHandle(cliPID int) error { + jobObjectName := utils.GetJobObjectNameFromPID(strconv.Itoa(cliPID)) + jbobj, err := winjob.Open(jobObjectName) + if err != nil { + return fmt.Errorf("error opening job object: %w", err) + } + err = jbobj.TerminateWithExitCode(0) + if err != nil { + return fmt.Errorf("error terminating job object: %w", err) + } + time.Sleep(5 * time.Second) + return setStopEvent(cliPID) +} + +func setStopEvent(cliPID int) error { + eventName, _ := syscall.UTF16FromString(fmt.Sprintf("dapr_cli_%v", cliPID)) + eventHandle, err := windows.OpenEvent(windows.EVENT_MODIFY_STATE, false, &eventName[0]) + if err != nil { + return err + } + + err = windows.SetEvent(eventHandle) + return err } diff --git a/pkg/syscall/syscall.go b/pkg/syscall/syscall.go index bde03be4e..197d6dd33 100644 --- a/pkg/syscall/syscall.go +++ b/pkg/syscall/syscall.go @@ -40,3 +40,9 @@ func CreateProcessGroupID() { print.WarningStatusEvent(os.Stdout, "Failed to create process group id: %s", err.Error()) } } + +// AttachJobObjectToProcess attaches the process to a job object. +func AttachJobObjectToProcess(jobName string, proc *os.Process) { + // This is a no-op on Linux/Mac. + // Instead, we use process group ID to kill all the processes. +} diff --git a/pkg/syscall/syscall_windows.go b/pkg/syscall/syscall_windows.go index 89c3c7d88..90d93ae71 100644 --- a/pkg/syscall/syscall_windows.go +++ b/pkg/syscall/syscall_windows.go @@ -1,3 +1,6 @@ +//go:build windows +// +build windows + /* Copyright 2021 The Dapr Authors Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,12 +21,17 @@ import ( "os" "os/signal" "syscall" + "unsafe" "golang.org/x/sys/windows" "github.com/dapr/cli/pkg/print" + "github.com/kolesnikovae/go-winjob" + "github.com/kolesnikovae/go-winjob/jobapi" ) +var jbObj *winjob.JobObject + func SetupShutdownNotify(sigCh chan os.Signal) { signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT) @@ -43,6 +51,41 @@ func SetupShutdownNotify(sigCh chan os.Signal) { // CreateProcessGroupID creates a process group ID for the current process. func CreateProcessGroupID() { - // No-op on Windows - print.WarningStatusEvent(os.Stdout, "Creating process group id is not implemented on Windows") + // This is a no-op on windows. + // Process group ID is not used for killing all the processes on windows. + // Instead, we use combination of named event and job object to kill all the processes. +} + +// AttachJobObjectToProcess attaches the process to a job object. +// It creates the job object if it doesn't exist. +func AttachJobObjectToProcess(jobName string, proc *os.Process) { + if jbObj != nil { + err := jbObj.Assign(proc) + if err != nil { + print.WarningStatusEvent(os.Stdout, "failed to assign process to job object: %s", err.Error()) + } + return + } + jbObj, err := winjob.Create(jobName) + if err != nil { + print.WarningStatusEvent(os.Stdout, "failed to create job object: %s", err.Error()) + return + } + // Below lines control the relation between Job object and processes attached to it. + // By passing JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE flag, it will make sure that when + // job object is closed all the processed must also be exited. + info := windows.JOBOBJECT_EXTENDED_LIMIT_INFORMATION{ + BasicLimitInformation: windows.JOBOBJECT_BASIC_LIMIT_INFORMATION{ + LimitFlags: windows.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, + }, + } + err = jobapi.SetInformationJobObject(jbObj.Handle, jobapi.JobObjectExtendedLimitInformation, unsafe.Pointer(&info), uint32(unsafe.Sizeof(info))) + if err != nil { + print.WarningStatusEvent(os.Stdout, "failed to set job object info: %s", err.Error()) + return + } + err = jbObj.Assign(proc) + if err != nil { + print.WarningStatusEvent(os.Stdout, "failed to assign process to job object: %s", err.Error()) + } } diff --git a/tests/e2e/standalone/run_template_test.go b/tests/e2e/standalone/run_template_test.go index a831527db..2e77c63aa 100644 --- a/tests/e2e/standalone/run_template_test.go +++ b/tests/e2e/standalone/run_template_test.go @@ -1,4 +1,5 @@ -//go:build e2e || template +//go:build !windows && (e2e || template) +// +build !windows // +build e2e template /* @@ -24,7 +25,6 @@ import ( "io/ioutil" "os" "path/filepath" - "runtime" "strings" "testing" "time" @@ -43,9 +43,6 @@ type AppTestOutput struct { } func TestRunWithTemplateFile(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("Skipping test on Windows") - } ensureDaprInstallation(t) t.Cleanup(func() { // remove dapr installation after all tests in this function. @@ -373,9 +370,6 @@ func TestRunWithTemplateFile(t *testing.T) { } func TestRunTemplateFileWithoutDaprInit(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("Skipping test on Windows") - } // remove any dapr installation before this test. must(t, cmdUninstall, "failed to uninstall Dapr") t.Run("valid template file without dapr init", func(t *testing.T) { diff --git a/tests/e2e/standalone/stop_with_run_template_test.go b/tests/e2e/standalone/stop_with_run_template_test.go index 3a7d2c9d1..ae4adaf14 100644 --- a/tests/e2e/standalone/stop_with_run_template_test.go +++ b/tests/e2e/standalone/stop_with_run_template_test.go @@ -1,4 +1,5 @@ -//go:build e2e || template +//go:build !windows && (e2e || template) +// +build !windows // +build e2e template /* @@ -22,7 +23,6 @@ import ( "encoding/json" "fmt" "os" - "runtime" "testing" "time" @@ -31,9 +31,6 @@ import ( ) func TestStopAppsStartedWithRunTemplate(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("Skipping test on windows") - } ensureDaprInstallation(t) t.Cleanup(func() { // remove dapr installation after all tests in this function. diff --git a/tests/e2e/standalone/windows_run_template_test.go b/tests/e2e/standalone/windows_run_template_test.go new file mode 100644 index 000000000..44c71bc03 --- /dev/null +++ b/tests/e2e/standalone/windows_run_template_test.go @@ -0,0 +1,144 @@ +//go:build windows && (e2e || template) +// +build windows +// +build e2e template + +/* +Copyright 2023 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_test + +import ( + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type AppTestOutput struct { + appID string + appLogContents []string + daprdLogContent []string + baseLogDirPath string + appLogDoesNotExist bool + daprdLogFileDoesNotExist bool +} + +func TestRunWithTemplateFile(t *testing.T) { + ensureDaprInstallation(t) + t.Cleanup(func() { + // remove dapr installation after all tests in this function. + must(t, cmdUninstall, "failed to uninstall Dapr") + }) + // These tests are dependent on run template files in ../testdata/run-template-files folder. + t.Run("valid template file", func(t *testing.T) { + runFilePath := "../testdata/run-template-files/dapr.yaml" + go startAppsWithValidRunTemplate(t, runFilePath) + time.Sleep(10 * time.Second) + output, err := cmdStopWithRunTemplate(runFilePath) + assert.NoError(t, err, "failed to stop apps started with run template") + assert.Contains(t, output, "Dapr and app processes stopped successfully") + time.Sleep(5 * time.Second) + }) + + t.Run("valid template file with App output written to only file", func(t *testing.T) { + runFilePath := "../testdata/run-template-files/app_output_to_file_and_console.yaml" + go startAppsWithAppLogDestFile(t, runFilePath) + time.Sleep(10 * time.Second) + output, err := cmdStopWithRunTemplate(runFilePath) + assert.NoError(t, err, "failed to stop apps started with run template") + assert.Contains(t, output, "Dapr and app processes stopped successfully") + time.Sleep(5 * time.Second) + }) + + t.Run("valid template file with App output written to only console", func(t *testing.T) { + runFilePath := "../testdata/run-template-files/app_output_to_only_console.yaml" + go startAppsWithAppLogDestConsole(t, runFilePath) + time.Sleep(10 * time.Second) + output, err := cmdStopWithRunTemplate(runFilePath) + assert.NoError(t, err, "failed to stop apps started with run template") + assert.Contains(t, output, "Dapr and app processes stopped successfully") + time.Sleep(5 * time.Second) + }) +} + +func startAppsWithValidRunTemplate(t *testing.T, file string) { + args := []string{ + "-f", file, + } + output, err := cmdRun("", args...) + t.Logf(output) + require.NoError(t, err, "run failed") + lines := strings.Split(output, "\n") + assert.GreaterOrEqual(t, len(lines), 7, "expected at least 7 lines in output of starting two apps") + assert.Contains(t, lines[0], "This is a preview feature and subject to change in future releases.") + assert.Contains(t, lines[1], "Validating config and starting app \"processor\"") + assert.Contains(t, lines[2], "Started Dapr with app id \"processor\". HTTP Port: 3510.") + assert.Contains(t, lines[3], "Writing log files to directory") + assert.Contains(t, lines[3], "tests\\apps\\processor\\.dapr\\logs") + assert.Contains(t, lines[4], "Validating config and starting app \"emit-metrics\"") + assert.Contains(t, lines[5], "Started Dapr with app id \"emit-metrics\". HTTP Port: 3511.") + assert.Contains(t, lines[6], "Writing log files to directory") + assert.Contains(t, lines[6], "tests\\apps\\emit-metrics\\.dapr\\logs") +} + +func startAppsWithAppLogDestFile(t *testing.T, file string) { + args := []string{ + "-f", file, + } + output, err := cmdRun("", args...) + t.Logf(output) + require.NoError(t, err, "run failed") + + // App logs for processor app should not be printed to console and only written to file. + assert.NotContains(t, output, "== APP - processor") + + // Daprd logs for processor app should only be printed to console and not written to file. + assert.Contains(t, output, "msg=\"All outstanding components processed\" app_id=processor") + + // App logs for emit-metrics app should be printed to console and written to file. + assert.Contains(t, output, "== APP - emit-metrics") + + // Daprd logs for emit-metrics app should only be written to file. + assert.NotContains(t, output, "msg=\"All outstanding components processed\" app_id=emit-metrics") + + assert.Contains(t, output, "Received signal to stop Dapr and app processes. Shutting down Dapr and app processes.") + +} + +func startAppsWithAppLogDestConsole(t *testing.T, file string) { + args := []string{ + "-f", file, + } + output, err := cmdRun("", args...) + t.Logf(output) + require.NoError(t, err, "run failed") + + // App logs for processor app should be printed to console. + assert.Contains(t, output, "== APP - processor") + + // Daprd logs for processor app should only be written to file. + assert.NotContains(t, output, "msg=\"All outstanding components processed\" app_id=processor") + + // App logs for emit-metrics app should be printed to console. + assert.Contains(t, output, "== APP - emit-metrics") + + // Daprd logs for emit-metrics app should only be written to file. + assert.NotContains(t, output, "msg=\"All outstanding components processed\" app_id=emit-metrics") + + assert.Contains(t, output, "Received signal to stop Dapr and app processes. Shutting down Dapr and app processes.") + +} diff --git a/tests/e2e/testdata/run-template-files/app_output_to_only_console.yaml b/tests/e2e/testdata/run-template-files/app_output_to_only_console.yaml new file mode 100644 index 000000000..17ddc86a2 --- /dev/null +++ b/tests/e2e/testdata/run-template-files/app_output_to_only_console.yaml @@ -0,0 +1,14 @@ +version: 1 +apps: + - appDirPath: ../../../apps/processor/ + appPort: 9081 + daprHTTPPort: 3510 + command: ["go","run", "app.go"] + appLogDestination: console + - appID: emit-metrics + appDirPath: ../../../apps/emit-metrics/ + daprHTTPPort: 3511 + env: + DAPR_HOST_ADD: localhost + command: ["go","run", "app.go"] + appLogDestination: console diff --git a/utils/utils.go b/utils/utils.go index fd8aae628..977e57866 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -29,6 +29,7 @@ import ( "time" "github.com/dapr/cli/pkg/print" + daprsyscall "github.com/dapr/cli/pkg/syscall" "github.com/docker/docker/client" "github.com/gocarina/gocsv" @@ -51,6 +52,9 @@ const ( // DefaultAppChannelAddress is the default local network address that user application listen on. DefaultAppChannelAddress = "127.0.0.1" + + // windowsDaprAppProcJobName is the name of the Windows job object that is used to manage the Daprized app's processes on windows. + windowsDaprAppProcJobName = "dapr-app-process-job" ) // IsValidContainerRuntime checks if the input is a valid container runtime. @@ -408,3 +412,14 @@ func FindFileInDir(dirPath, fileName string) (string, error) { func SanitizeDir(destDir string) string { return strings.ReplaceAll(destDir, "'", "''") } + +// Attach Job object to App Process. +func AttachJobObjectToProcess(pid string, proc *os.Process) { + // Attach a job object to the app process. + daprsyscall.AttachJobObjectToProcess(GetJobObjectNameFromPID(pid), proc) +} + +// GetJobObjectNameFromPID returns the name of the Windows job object that is used to manage the Daprized app's processes on windows. +func GetJobObjectNameFromPID(pid string) string { + return pid + "-" + windowsDaprAppProcJobName +}