-
Notifications
You must be signed in to change notification settings - Fork 212
Improve dapr init experience on Windows machines running WSL
#1641
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
ebfddf9
3600df1
a524ef6
e9321b5
35a7030
d0edaf0
84a8e70
9867baa
1bcd6e4
99eb570
32ce657
c0ef62f
99de306
b3e0860
1334fee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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", | ||
|
WhitWaldo marked this conversation as resolved.
|
||
| portErr) | ||
|
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() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we use the above suggested bool and then inside that if statement we check the scheduler ports first before stopping wsl? So something like: |
||
| 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) | ||
| } | ||
|
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 { | ||
|
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) | ||
| } | ||
|
WhitWaldo marked this conversation as resolved.
|
||
| } | ||
| print.InfoStatusEvent(os.Stdout, "Restarting WSL...") | ||
| startWSLBackground() | ||
| } | ||
|
WhitWaldo marked this conversation as resolved.
|
||
|
|
||
| if err != nil { | ||
| runError := isContainerRunError(err) | ||
| if !runError { | ||
|
|
@@ -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 | ||
|
|
||
| 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() {} |
| 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 | ||
| } |
| 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) | ||
|
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 | ||
| } | ||
| 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 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shall we return err here and/or log it? |
||
| return | ||
| } | ||
| go func() { _ = cmd.Wait() }() | ||
| } | ||
| 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() | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see this if case a few times, can we put it in a shared var and reuse that rather than having this if case a couple of times? Maybe call it
shouldManageWSL? soshouldManageWSL := info.dockerNetwork == "" && runtime.GOOS == daprWindowsOS && !isWindowsElevated() && isWSLAvailable() {and then the if cases are simply:if shouldManageWSL {...}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I see we toggle the
!isWindowsElevated(), maybe we can have the following then: