Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions pkg/standalone/standalone.go
Original file line number Diff line number Diff line change
Expand Up @@ -706,7 +706,56 @@ 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, 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: %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"+
"elevated, the CLI will automatically stop and restart WSL and\n"+
"Windows networking services as part of the installation process",
Comment thread
WhitWaldo marked this conversation as resolved.
portErr)
Comment thread
WhitWaldo marked this conversation as resolved.
return
}
}

// 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 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: %v. Continuing...", wslErr)
}
Comment thread
WhitWaldo marked this conversation as resolved.
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: %v. Continuing...", stopErr)
} else {
Comment thread
WhitWaldo marked this conversation as resolved.
winNATStopped = true
}
}

_, err = utils.RunCmdAndWait(runtimeCmd, args...)

// Restore WinNAT and restart WSL regardless of whether the scheduler container started successfully.
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: %v", startErr)
}
Comment thread
WhitWaldo marked this conversation as resolved.
}
print.InfoStatusEvent(os.Stdout, "Restarting WSL...")
startWSLBackground()
}
Comment thread
WhitWaldo marked this conversation as resolved.

if err != nil {
runError := isContainerRunError(err)
if !runError {
Expand All @@ -719,6 +768,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
Expand Down
34 changes: 34 additions & 0 deletions pkg/standalone/wsl_nowindows.go
Original file line number Diff line number Diff line change
@@ -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() {}
34 changes: 34 additions & 0 deletions pkg/standalone/wsl_nowindows_test.go
Original file line number Diff line number Diff line change
@@ -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
}
94 changes: 94 additions & 0 deletions pkg/standalone/wsl_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
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) {
// 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) {
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) {
ln := holdPort(t)
defer ln.Close()
busy := ln.Addr().(*net.TCPAddr).Port

// 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)
Comment thread
WhitWaldo marked this conversation as resolved.
assert.Contains(t, err.Error(), fmt.Sprintf("port %d", busy))
})

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

// 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(), "port 0")
})
}

// 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
}
69 changes: 69 additions & 0 deletions pkg/standalone/wsl_windows.go
Original file line number Diff line number Diff line change
@@ -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 instance 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 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.
func startWSLBackground() {
cmd := exec.Command("wsl", "--exec", "echo")
if err := cmd.Start(); err != nil {
return
}
go func() { _ = cmd.Wait() }()
}
50 changes: 50 additions & 0 deletions pkg/standalone/wsl_windows_test.go
Original file line number Diff line number Diff line change
@@ -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()
}
Loading