From ebfddf94c1452812d2f0c00aa8495b6210340d68 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sat, 2 May 2026 17:54:49 -0500 Subject: [PATCH 01/13] Implementing additional functionality for `dapr init` when running in elevated prompts on Windows to address port-busy errors for Dapr scheduler when WSL is installed on the system. If not elevated and the error is encountered on Windows, the output suggests launching as an elevated process and retrying (and why). Signed-off-by: Whit Waldo --- pkg/standalone/standalone.go | 63 ++++++++++++++++ pkg/standalone/wsl_nowindows.go | 34 +++++++++ pkg/standalone/wsl_nowindows_test.go | 34 +++++++++ pkg/standalone/wsl_test.go | 109 +++++++++++++++++++++++++++ pkg/standalone/wsl_windows.go | 69 +++++++++++++++++ pkg/standalone/wsl_windows_test.go | 50 ++++++++++++ 6 files changed, 359 insertions(+) create mode 100644 pkg/standalone/wsl_nowindows.go create mode 100644 pkg/standalone/wsl_nowindows_test.go create mode 100644 pkg/standalone/wsl_test.go create mode 100644 pkg/standalone/wsl_windows.go create mode 100644 pkg/standalone/wsl_windows_test.go diff --git a/pkg/standalone/standalone.go b/pkg/standalone/standalone.go index 2a1e81ca2..c0bed7ae9 100644 --- a/pkg/standalone/standalone.go +++ b/pkg/standalone/standalone.go @@ -706,7 +706,52 @@ func runSchedulerService(wg *sync.WaitGroup, errorChan chan<- error, info initIn args = append(args, "--etcd-client-listen-address=0.0.0.0") } + // On non-elevated Windows with WSL2 installed, verify the scheduler ports + // are free before attempting the container start. WSL2 commonly holds + // :2379 (etcd) and the only reliable fix requires an elevated terminal. + if runtime.GOOS == daprWindowsOS && !isWindowsElevated() && isWSLAvailable() { + if portErr := checkSchedulerPorts(osPort); portErr != nil { + errorChan <- fmt.Errorf( + "failed to start scheduler service: %s\n\n"+ + "WSL2 is occupying a port the scheduler requires.\n"+ + "To resolve this, re-run 'dapr init' in an elevated (Administrator)\n"+ + "terminal (e.g. right-click → \"Run as administrator\"). When running\n"+ + "elevated, the CLI will automatically stop and restart WSL and\n"+ + "Windows networking services as part of the installation process.", + portErr) + return + } + } + + // On elevated Windows, shut down WSL2 and stop WinNAT so Docker can + // re-acquire the scheduler's port bindings (especially etcd :2379) that + // WSL2 may be holding. + if runtime.GOOS == daprWindowsOS && isWindowsElevated() { + if isWSLAvailable() { + print.InfoStatusEvent(os.Stdout, "Temporarily shutting down WSL to free ports for scheduler installation...") + if wslErr := shutdownWSL(); wslErr != nil { + print.WarningStatusEvent(os.Stdout, "Failed to shut down WSL: %s. Continuing...", wslErr) + } + } + print.InfoStatusEvent(os.Stdout, "Temporarily stopping Windows NAT service to free scheduler ports...") + if stopErr := stopWinNAT(); stopErr != nil { + print.WarningStatusEvent(os.Stdout, "Failed to stop Windows NAT service: %s. Continuing...", stopErr) + } + } + _, err = utils.RunCmdAndWait(runtimeCmd, args...) + + // Restore WinNAT and restart WSL regardless of whether the scheduler container started successfully. + if runtime.GOOS == daprWindowsOS && isWindowsElevated() { + if startErr := startWinNAT(); startErr != nil { + print.WarningStatusEvent(os.Stdout, "Failed to restart Windows NAT service: %s", startErr) + } + if isWSLAvailable() { + print.InfoStatusEvent(os.Stdout, "Restarting WSL...") + startWSLBackground() + } + } + if err != nil { runError := isContainerRunError(err) if !runError { @@ -719,6 +764,24 @@ func runSchedulerService(wg *sync.WaitGroup, errorChan chan<- error, info initIn errorChan <- nil } +// checkSchedulerPorts verifies that all ports required by the scheduler +// service are available. grpcPort is the platform-specific gRPC port +// (50006 on Linux/Mac, 6060 on Windows). +func checkSchedulerPorts(grpcPort int) error { + return checkPorts(grpcPort, schedulerEtcdPort, schedulerHealthPort, schedulerMetricPort) +} + +// checkPorts returns an error for the first port in the list that is not +// available, including the port number in the message. +func checkPorts(ports ...int) error { + for _, p := range ports { + if err := utils.CheckIfPortAvailable(p); err != nil { + return fmt.Errorf("port %d is not available: %w", p, err) + } + } + return nil +} + func schedulerOverrideHostPort(info initInfo) bool { if info.runtimeVersion == "edge" || info.runtimeVersion == "dev" { return true diff --git a/pkg/standalone/wsl_nowindows.go b/pkg/standalone/wsl_nowindows.go new file mode 100644 index 000000000..2065e1d1e --- /dev/null +++ b/pkg/standalone/wsl_nowindows.go @@ -0,0 +1,34 @@ +//go:build !windows + +/* +Copyright 2021 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 + +// isWindowsElevated always returns false on non-Windows platforms. +func isWindowsElevated() bool { return false } + +// isWSLAvailable always returns false on non-Windows platforms. +func isWSLAvailable() bool { return false } + +// shutdownWSL is a no-op on non-Windows platforms. +func shutdownWSL() error { return nil } + +// stopWinNAT is a no-op on non-Windows platforms. +func stopWinNAT() error { return nil } + +// startWinNAT is a no-op on non-Windows platforms. +func startWinNAT() error { return nil } + +// startWSLBackground is a no-op on non-Windows platforms. +func startWSLBackground() {} diff --git a/pkg/standalone/wsl_nowindows_test.go b/pkg/standalone/wsl_nowindows_test.go new file mode 100644 index 000000000..86d5c6341 --- /dev/null +++ b/pkg/standalone/wsl_nowindows_test.go @@ -0,0 +1,34 @@ +//go:build !windows + +/* +Copyright 2021 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 ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestNoopStubs verifies that every non-Windows stub returns the correct +// zero/no-op value and does not panic. This guards against accidental breakage +// of the cross-platform build contract. +func TestNoopStubs(t *testing.T) { + assert.False(t, isWindowsElevated(), "isWindowsElevated must always be false on non-Windows") + assert.False(t, isWSLAvailable(), "isWSLAvailable must always be false on non-Windows") + assert.NoError(t, shutdownWSL()) + assert.NoError(t, stopWinNAT()) + assert.NoError(t, startWinNAT()) + startWSLBackground() // must not panic +} diff --git a/pkg/standalone/wsl_test.go b/pkg/standalone/wsl_test.go new file mode 100644 index 000000000..af094d151 --- /dev/null +++ b/pkg/standalone/wsl_test.go @@ -0,0 +1,109 @@ +/* +Copyright 2021 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 ( + "fmt" + "net" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestCheckPorts exercises the core port-availability helper used by the +// Windows WSL2 port-conflict detection path. +func TestCheckPorts(t *testing.T) { + t.Run("returns nil when given no ports", func(t *testing.T) { + assert.NoError(t, checkPorts()) + }) + + t.Run("returns nil when all ports are free", func(t *testing.T) { + p1 := freePort(t) + p2 := freePort(t) + assert.NoError(t, checkPorts(p1, p2)) + }) + + t.Run("returns error containing port number when port is in use", func(t *testing.T) { + ln := holdPort(t) + defer ln.Close() + port := ln.Addr().(*net.TCPAddr).Port + + err := checkPorts(port) + require.Error(t, err) + assert.Contains(t, err.Error(), fmt.Sprintf("port %d", port)) + }) + + t.Run("returns error for first occupied port in the list", func(t *testing.T) { + free := freePort(t) + + ln := holdPort(t) + defer ln.Close() + busy := ln.Addr().(*net.TCPAddr).Port + + // free comes first — we still expect failure once the busy port is reached. + err := checkPorts(free, busy) + require.Error(t, err) + assert.Contains(t, err.Error(), fmt.Sprintf("port %d", busy)) + }) + + t.Run("succeeds for free port preceding an occupied one when first fails fast", func(t *testing.T) { + ln := holdPort(t) + defer ln.Close() + busy := ln.Addr().(*net.TCPAddr).Port + + free := freePort(t) + + // busy comes first — error must name it, not the free port. + err := checkPorts(busy, free) + require.Error(t, err) + assert.Contains(t, err.Error(), fmt.Sprintf("port %d", busy)) + assert.NotContains(t, err.Error(), fmt.Sprintf("port %d", free)) + }) +} + +// TestCheckSchedulerPorts_PortInUse verifies that checkSchedulerPorts surfaces +// an error (with the port number) when the gRPC port it is given is already +// bound. This is the scenario triggered by WSL2 holding scheduler ports. +func TestCheckSchedulerPorts_PortInUse(t *testing.T) { + ln := holdPort(t) + defer ln.Close() + busyPort := ln.Addr().(*net.TCPAddr).Port + + err := checkSchedulerPorts(busyPort) + require.Error(t, err) + assert.Contains(t, err.Error(), fmt.Sprintf("port %d", busyPort)) +} + +// holdPort binds an OS-assigned port and returns the listener. The caller is +// responsible for closing it. Using ":0" matches the binding style of +// utils.CheckIfPortAvailable so the conflict is detected reliably. +func holdPort(t *testing.T) net.Listener { + t.Helper() + ln, err := net.Listen("tcp", ":0") + require.NoError(t, err) + return ln +} + +// freePort returns an available port number by briefly binding then releasing +// it. There is a theoretical race between Close and the subsequent +// checkPorts call, but in practice this window is negligible for unit tests. +func freePort(t *testing.T) int { + t.Helper() + ln, err := net.Listen("tcp", ":0") + require.NoError(t, err) + port := ln.Addr().(*net.TCPAddr).Port + ln.Close() + return port +} diff --git a/pkg/standalone/wsl_windows.go b/pkg/standalone/wsl_windows.go new file mode 100644 index 000000000..10c06de60 --- /dev/null +++ b/pkg/standalone/wsl_windows.go @@ -0,0 +1,69 @@ +//go:build windows + +/* +Copyright 2021 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" + + "golang.org/x/sys/windows" + + "github.com/dapr/cli/utils" +) + +// isWindowsElevated returns true if the current process is running with +// elevated (Administrator) privileges. +func isWindowsElevated() bool { + return windows.GetCurrentProcessToken().IsElevated() +} + +// isWSLAvailable returns true if the wsl executable is available in PATH. +func isWSLAvailable() bool { + _, err := exec.LookPath("wsl") + return err == nil +} + +// shutdownWSL runs `wsl --shutdown` to terminate the WSL2 VM and free any +// ports it holds. +func shutdownWSL() error { + _, err := utils.RunCmdAndWait("wsl", "--shutdown") + return err +} + +// stopWinNAT stops the Windows NAT driver service (WinNat) so that Docker +// can re-acquire port bindings that WinNAT was caching. +func stopWinNAT() error { + _, err := utils.RunCmdAndWait("net", "stop", "winnat") + return err +} + +// startWinNAT starts the Windows NAT driver service after the scheduler +// container has been created. +func startWinNAT() error { + _, err := utils.RunCmdAndWait("net", "start", "winnat") + return err +} + +// startWSLBackground starts WSL in the background to re-initialize WSL2 +// networking after a wsl --shutdown. We run a no-op command so the session +// exits immediately once WSL services are up, then wait in a goroutine to +// clean up the process handle. +func startWSLBackground() { + cmd := exec.Command("wsl", "--exec", "echo") + if err := cmd.Start(); err != nil { + return + } + go func() { _ = cmd.Wait() }() +} diff --git a/pkg/standalone/wsl_windows_test.go b/pkg/standalone/wsl_windows_test.go new file mode 100644 index 000000000..1c0de6cec --- /dev/null +++ b/pkg/standalone/wsl_windows_test.go @@ -0,0 +1,50 @@ +//go:build windows + +/* +Copyright 2021 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" + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestIsWindowsElevated_Callable verifies the function completes without +// panicking. The actual return value depends on whether the test process is +// running as Administrator, so we only log it rather than assert a fixed value. +func TestIsWindowsElevated_Callable(t *testing.T) { + elevated := isWindowsElevated() + t.Logf("isWindowsElevated() = %v (test process running as Administrator: %v)", elevated, elevated) +} + +// TestIsWSLAvailable_MatchesLookPath verifies that isWSLAvailable reports the +// same result as exec.LookPath("wsl"), confirming it accurately reflects +// whether wsl.exe is on the PATH. +func TestIsWSLAvailable_MatchesLookPath(t *testing.T) { + _, err := exec.LookPath("wsl") + expected := err == nil + assert.Equal(t, expected, isWSLAvailable(), + "isWSLAvailable() should return true iff wsl.exe is on PATH") +} + +// TestStartWSLBackground_DoesNotBlock verifies that startWSLBackground returns +// promptly regardless of whether WSL is installed. When WSL is absent the +// internal cmd.Start() fails silently; when present, wsl --exec echo exits +// immediately and the cleanup goroutine reaps it. +func TestStartWSLBackground_DoesNotBlock(t *testing.T) { + // This must complete without hanging; no assertion on side-effects. + startWSLBackground() +} From 3600df1d597d15ab925fd4abfce2a4a96089dd7e Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sat, 2 May 2026 18:05:58 -0500 Subject: [PATCH 02/13] Fixing lint error Signed-off-by: Whit Waldo --- pkg/standalone/standalone.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/standalone/standalone.go b/pkg/standalone/standalone.go index c0bed7ae9..fb28bb6ba 100644 --- a/pkg/standalone/standalone.go +++ b/pkg/standalone/standalone.go @@ -717,7 +717,7 @@ func runSchedulerService(wg *sync.WaitGroup, errorChan chan<- error, info initIn "To resolve this, re-run 'dapr init' in an elevated (Administrator)\n"+ "terminal (e.g. right-click → \"Run as administrator\"). When running\n"+ "elevated, the CLI will automatically stop and restart WSL and\n"+ - "Windows networking services as part of the installation process.", + "Windows networking services as part of the installation process", portErr) return } From a524ef6cfdc67c6a7b040605f72537a982032214 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 5 May 2026 21:24:11 -0500 Subject: [PATCH 03/13] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Whit Waldo --- pkg/standalone/standalone.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/standalone/standalone.go b/pkg/standalone/standalone.go index fb28bb6ba..a2c0d6231 100644 --- a/pkg/standalone/standalone.go +++ b/pkg/standalone/standalone.go @@ -707,9 +707,10 @@ func runSchedulerService(wg *sync.WaitGroup, errorChan chan<- error, info initIn } // On non-elevated Windows with WSL2 installed, verify the scheduler ports - // are free before attempting the container start. WSL2 commonly holds - // :2379 (etcd) and the only reliable fix requires an elevated terminal. - if runtime.GOOS == daprWindowsOS && !isWindowsElevated() && isWSLAvailable() { + // are free before attempting the container start, but only when the + // scheduler is publishing host ports. WSL2 commonly holds :2379 (etcd) + // and the only reliable fix requires an elevated terminal. + if info.dockerNetwork == "" && runtime.GOOS == daprWindowsOS && !isWindowsElevated() && isWSLAvailable() { if portErr := checkSchedulerPorts(osPort); portErr != nil { errorChan <- fmt.Errorf( "failed to start scheduler service: %s\n\n"+ From e9321b5f6cdf3694c338950283038a3a74576f34 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 5 May 2026 22:09:04 -0500 Subject: [PATCH 04/13] Addressing more accurate error message after failing to start scheduler service Signed-off-by: Whit Waldo --- pkg/standalone/standalone.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/standalone/standalone.go b/pkg/standalone/standalone.go index a2c0d6231..ef693bfdb 100644 --- a/pkg/standalone/standalone.go +++ b/pkg/standalone/standalone.go @@ -714,7 +714,7 @@ func runSchedulerService(wg *sync.WaitGroup, errorChan chan<- error, info initIn if portErr := checkSchedulerPorts(osPort); portErr != nil { errorChan <- fmt.Errorf( "failed to start scheduler service: %s\n\n"+ - "WSL2 is occupying a port the scheduler requires.\n"+ + "A required port is already in use (often due to WSL2).\n"+ "To resolve this, re-run 'dapr init' in an elevated (Administrator)\n"+ "terminal (e.g. right-click → \"Run as administrator\"). When running\n"+ "elevated, the CLI will automatically stop and restart WSL and\n"+ From 35a703049da4f6889011683594bc3ba7f5bb8177 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 5 May 2026 22:11:04 -0500 Subject: [PATCH 05/13] Add conditional flag to track and restart WinNAT only if it was stopped Signed-off-by: Whit Waldo --- pkg/standalone/standalone.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pkg/standalone/standalone.go b/pkg/standalone/standalone.go index ef693bfdb..610a7980f 100644 --- a/pkg/standalone/standalone.go +++ b/pkg/standalone/standalone.go @@ -727,6 +727,7 @@ func runSchedulerService(wg *sync.WaitGroup, errorChan chan<- error, info initIn // On elevated Windows, shut down WSL2 and stop WinNAT so Docker can // re-acquire the scheduler's port bindings (especially etcd :2379) that // WSL2 may be holding. + winNATStopped := false if runtime.GOOS == daprWindowsOS && isWindowsElevated() { if isWSLAvailable() { print.InfoStatusEvent(os.Stdout, "Temporarily shutting down WSL to free ports for scheduler installation...") @@ -737,6 +738,8 @@ func runSchedulerService(wg *sync.WaitGroup, errorChan chan<- error, info initIn print.InfoStatusEvent(os.Stdout, "Temporarily stopping Windows NAT service to free scheduler ports...") if stopErr := stopWinNAT(); stopErr != nil { print.WarningStatusEvent(os.Stdout, "Failed to stop Windows NAT service: %s. Continuing...", stopErr) + } else { + winNATStopped = true } } @@ -744,8 +747,10 @@ func runSchedulerService(wg *sync.WaitGroup, errorChan chan<- error, info initIn // Restore WinNAT and restart WSL regardless of whether the scheduler container started successfully. if runtime.GOOS == daprWindowsOS && isWindowsElevated() { - if startErr := startWinNAT(); startErr != nil { - print.WarningStatusEvent(os.Stdout, "Failed to restart Windows NAT service: %s", startErr) + if winNATStopped { + if startErr := startWinNAT(); startErr != nil { + print.WarningStatusEvent(os.Stdout, "Failed to restart Windows NAT service: %s", startErr) + } } if isWSLAvailable() { print.InfoStatusEvent(os.Stdout, "Restarting WSL...") From d0edaf008bd4a1ff2c5e989f574d93717ed45ca1 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 5 May 2026 22:12:54 -0500 Subject: [PATCH 06/13] More accurately represents the test per review Signed-off-by: Whit Waldo --- pkg/standalone/wsl_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/standalone/wsl_test.go b/pkg/standalone/wsl_test.go index af094d151..88f56ceef 100644 --- a/pkg/standalone/wsl_test.go +++ b/pkg/standalone/wsl_test.go @@ -58,7 +58,7 @@ func TestCheckPorts(t *testing.T) { assert.Contains(t, err.Error(), fmt.Sprintf("port %d", busy)) }) - t.Run("succeeds for free port preceding an occupied one when first fails fast", func(t *testing.T) { + t.Run("errors when the first port is occupied, ignoring a free port that follows", func(t *testing.T) { ln := holdPort(t) defer ln.Close() busy := ln.Addr().(*net.TCPAddr).Port From 84a8e70bba765e2ee214c989ebc316b4707d9e0d Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 5 May 2026 22:16:40 -0500 Subject: [PATCH 07/13] Addressing concerns in review about validating info.dockerNetwork value in addition to other three outstanding constraints Signed-off-by: Whit Waldo --- pkg/standalone/standalone.go | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/pkg/standalone/standalone.go b/pkg/standalone/standalone.go index 610a7980f..d06625606 100644 --- a/pkg/standalone/standalone.go +++ b/pkg/standalone/standalone.go @@ -724,16 +724,16 @@ func runSchedulerService(wg *sync.WaitGroup, errorChan chan<- error, info initIn } } - // On elevated Windows, shut down WSL2 and stop WinNAT so Docker can - // re-acquire the scheduler's port bindings (especially etcd :2379) that - // WSL2 may be holding. + // On elevated Windows with host-port publishing and WSL2 installed, shut + // down WSL2 and stop WinNAT so Docker can re-acquire the scheduler's port + // bindings (especially etcd :2379) that WSL2 may be holding. + // Skipped when using a Docker network (no host ports) or when WSL is not + // present, to avoid unnecessary service disruption. winNATStopped := false - if runtime.GOOS == daprWindowsOS && isWindowsElevated() { - if isWSLAvailable() { - print.InfoStatusEvent(os.Stdout, "Temporarily shutting down WSL to free ports for scheduler installation...") - if wslErr := shutdownWSL(); wslErr != nil { - print.WarningStatusEvent(os.Stdout, "Failed to shut down WSL: %s. Continuing...", wslErr) - } + if info.dockerNetwork == "" && runtime.GOOS == daprWindowsOS && isWindowsElevated() && isWSLAvailable() { + print.InfoStatusEvent(os.Stdout, "Temporarily shutting down WSL to free ports for scheduler installation...") + if wslErr := shutdownWSL(); wslErr != nil { + print.WarningStatusEvent(os.Stdout, "Failed to shut down WSL: %s. Continuing...", wslErr) } print.InfoStatusEvent(os.Stdout, "Temporarily stopping Windows NAT service to free scheduler ports...") if stopErr := stopWinNAT(); stopErr != nil { @@ -746,16 +746,14 @@ func runSchedulerService(wg *sync.WaitGroup, errorChan chan<- error, info initIn _, err = utils.RunCmdAndWait(runtimeCmd, args...) // Restore WinNAT and restart WSL regardless of whether the scheduler container started successfully. - if runtime.GOOS == daprWindowsOS && isWindowsElevated() { + if info.dockerNetwork == "" && runtime.GOOS == daprWindowsOS && isWindowsElevated() && isWSLAvailable() { if winNATStopped { if startErr := startWinNAT(); startErr != nil { print.WarningStatusEvent(os.Stdout, "Failed to restart Windows NAT service: %s", startErr) } } - if isWSLAvailable() { - print.InfoStatusEvent(os.Stdout, "Restarting WSL...") - startWSLBackground() - } + print.InfoStatusEvent(os.Stdout, "Restarting WSL...") + startWSLBackground() } if err != nil { From 9867baa4d906dd4a78b89a770e138b685d2a2b94 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 5 May 2026 22:24:11 -0500 Subject: [PATCH 08/13] Addressed comment scope Signed-off-by: Whit Waldo --- pkg/standalone/wsl_windows.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/standalone/wsl_windows.go b/pkg/standalone/wsl_windows.go index 10c06de60..fe7a3cd97 100644 --- a/pkg/standalone/wsl_windows.go +++ b/pkg/standalone/wsl_windows.go @@ -35,7 +35,7 @@ func isWSLAvailable() bool { return err == nil } -// shutdownWSL runs `wsl --shutdown` to terminate the WSL2 VM and free any +// shutdownWSL runs `wsl --shutdown` to terminate the WSL2 instance and free any // ports it holds. func shutdownWSL() error { _, err := utils.RunCmdAndWait("wsl", "--shutdown") From 1bcd6e459a8f94586103049f80ded82aefe837c9 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 5 May 2026 22:24:37 -0500 Subject: [PATCH 09/13] Addressing comment raised in review Signed-off-by: Whit Waldo --- pkg/standalone/wsl_windows.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/standalone/wsl_windows.go b/pkg/standalone/wsl_windows.go index fe7a3cd97..237f22fde 100644 --- a/pkg/standalone/wsl_windows.go +++ b/pkg/standalone/wsl_windows.go @@ -56,7 +56,7 @@ func startWinNAT() error { return err } -// startWSLBackground starts WSL in the background to re-initialize WSL2 +// startWSLBackground starts WSL in the background to re-initialize WSL // networking after a wsl --shutdown. We run a no-op command so the session // exits immediately once WSL services are up, then wait in a goroutine to // clean up the process handle. From 99eb57072b6a28813ac365ca3324078fa7968517 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 5 May 2026 22:25:15 -0500 Subject: [PATCH 10/13] Removed call-out about WSL2 per review comment Signed-off-by: Whit Waldo --- pkg/standalone/standalone.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/standalone/standalone.go b/pkg/standalone/standalone.go index d06625606..d4b6e4e22 100644 --- a/pkg/standalone/standalone.go +++ b/pkg/standalone/standalone.go @@ -714,7 +714,7 @@ func runSchedulerService(wg *sync.WaitGroup, errorChan chan<- error, info initIn if portErr := checkSchedulerPorts(osPort); portErr != nil { errorChan <- fmt.Errorf( "failed to start scheduler service: %s\n\n"+ - "A required port is already in use (often due to WSL2).\n"+ + "A required port is already in use (often due to WSL).\n"+ "To resolve this, re-run 'dapr init' in an elevated (Administrator)\n"+ "terminal (e.g. right-click → \"Run as administrator\"). When running\n"+ "elevated, the CLI will automatically stop and restart WSL and\n"+ From 32ce6571fa30b87e1fe5a639e4aa3d99cbdb416f Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 5 May 2026 22:33:43 -0500 Subject: [PATCH 11/13] Addressing latest Copilot comment regarding race condition in tests Signed-off-by: Whit Waldo --- pkg/standalone/wsl_test.go | 33 +++++++++------------------------ 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/pkg/standalone/wsl_test.go b/pkg/standalone/wsl_test.go index 88f56ceef..9c7be4018 100644 --- a/pkg/standalone/wsl_test.go +++ b/pkg/standalone/wsl_test.go @@ -30,9 +30,9 @@ func TestCheckPorts(t *testing.T) { }) t.Run("returns nil when all ports are free", func(t *testing.T) { - p1 := freePort(t) - p2 := freePort(t) - assert.NoError(t, checkPorts(p1, p2)) + // Port 0 always passes CheckIfPortAvailable (the OS selects a free + // ephemeral port), so there is no bind/close race here. + assert.NoError(t, checkPorts(0, 0)) }) t.Run("returns error containing port number when port is in use", func(t *testing.T) { @@ -46,14 +46,13 @@ func TestCheckPorts(t *testing.T) { }) t.Run("returns error for first occupied port in the list", func(t *testing.T) { - free := freePort(t) - ln := holdPort(t) defer ln.Close() busy := ln.Addr().(*net.TCPAddr).Port - // free comes first — we still expect failure once the busy port is reached. - err := checkPorts(free, busy) + // Port 0 is always free (OS picks an ephemeral port); busy comes second. + // We still expect failure once the busy port is reached. + err := checkPorts(0, busy) require.Error(t, err) assert.Contains(t, err.Error(), fmt.Sprintf("port %d", busy)) }) @@ -63,13 +62,11 @@ func TestCheckPorts(t *testing.T) { defer ln.Close() busy := ln.Addr().(*net.TCPAddr).Port - free := freePort(t) - - // busy comes first — error must name it, not the free port. - err := checkPorts(busy, free) + // busy comes first, port 0 (always free) follows — error must name busy only. + err := checkPorts(busy, 0) require.Error(t, err) assert.Contains(t, err.Error(), fmt.Sprintf("port %d", busy)) - assert.NotContains(t, err.Error(), fmt.Sprintf("port %d", free)) + assert.NotContains(t, err.Error(), "port 0") }) } @@ -95,15 +92,3 @@ func holdPort(t *testing.T) net.Listener { require.NoError(t, err) return ln } - -// freePort returns an available port number by briefly binding then releasing -// it. There is a theoretical race between Close and the subsequent -// checkPorts call, but in practice this window is negligible for unit tests. -func freePort(t *testing.T) int { - t.Helper() - ln, err := net.Listen("tcp", ":0") - require.NoError(t, err) - port := ln.Addr().(*net.TCPAddr).Port - ln.Close() - return port -} From c0ef62fd826ca0eae1842285eb7ddb5228ef8ef3 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 5 May 2026 23:22:59 -0500 Subject: [PATCH 12/13] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Whit Waldo --- pkg/standalone/standalone.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/standalone/standalone.go b/pkg/standalone/standalone.go index d4b6e4e22..b8b40fd7e 100644 --- a/pkg/standalone/standalone.go +++ b/pkg/standalone/standalone.go @@ -713,7 +713,7 @@ func runSchedulerService(wg *sync.WaitGroup, errorChan chan<- error, info initIn if info.dockerNetwork == "" && runtime.GOOS == daprWindowsOS && !isWindowsElevated() && isWSLAvailable() { if portErr := checkSchedulerPorts(osPort); portErr != nil { errorChan <- fmt.Errorf( - "failed to start scheduler service: %s\n\n"+ + "failed to start scheduler service: %v\n\n"+ "A required port is already in use (often due to WSL).\n"+ "To resolve this, re-run 'dapr init' in an elevated (Administrator)\n"+ "terminal (e.g. right-click → \"Run as administrator\"). When running\n"+ From 99de3068986db41e4b3063387899f2538c800f58 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 5 May 2026 23:24:40 -0500 Subject: [PATCH 13/13] Addressing how to emit errors properly per Copilot feedback Signed-off-by: Whit Whit --- pkg/standalone/standalone.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/standalone/standalone.go b/pkg/standalone/standalone.go index d4b6e4e22..d1688ee00 100644 --- a/pkg/standalone/standalone.go +++ b/pkg/standalone/standalone.go @@ -733,11 +733,11 @@ func runSchedulerService(wg *sync.WaitGroup, errorChan chan<- error, info initIn if info.dockerNetwork == "" && runtime.GOOS == daprWindowsOS && isWindowsElevated() && isWSLAvailable() { print.InfoStatusEvent(os.Stdout, "Temporarily shutting down WSL to free ports for scheduler installation...") if wslErr := shutdownWSL(); wslErr != nil { - print.WarningStatusEvent(os.Stdout, "Failed to shut down WSL: %s. Continuing...", wslErr) + print.WarningStatusEvent(os.Stdout, "Failed to shut down WSL: %v. Continuing...", wslErr) } print.InfoStatusEvent(os.Stdout, "Temporarily stopping Windows NAT service to free scheduler ports...") if stopErr := stopWinNAT(); stopErr != nil { - print.WarningStatusEvent(os.Stdout, "Failed to stop Windows NAT service: %s. Continuing...", stopErr) + print.WarningStatusEvent(os.Stdout, "Failed to stop Windows NAT service: %v. Continuing...", stopErr) } else { winNATStopped = true } @@ -749,7 +749,7 @@ func runSchedulerService(wg *sync.WaitGroup, errorChan chan<- error, info initIn if info.dockerNetwork == "" && runtime.GOOS == daprWindowsOS && isWindowsElevated() && isWSLAvailable() { if winNATStopped { if startErr := startWinNAT(); startErr != nil { - print.WarningStatusEvent(os.Stdout, "Failed to restart Windows NAT service: %s", startErr) + print.WarningStatusEvent(os.Stdout, "Failed to restart Windows NAT service: %v", startErr) } } print.InfoStatusEvent(os.Stdout, "Restarting WSL...")