From 9fbe2fffe9d35fa847faf41dda61f2632e400546 Mon Sep 17 00:00:00 2001 From: Mario Loriedo Date: Wed, 2 Jul 2025 09:43:12 +0200 Subject: [PATCH 1/2] WSL commands execution refactoring Introduced a new function to encapsulate the code to execute WSL commands. Signed-off-by: Mario Loriedo --- cmd/podman-wslkerninst/main.go | 2 +- pkg/machine/wsl/machine.go | 64 +++++++++++++++-------------- pkg/machine/wsl/stubber.go | 9 ++-- pkg/machine/wsl/wutil/wutil.go | 15 ++++--- pkg/machine/wsl/wutil/wutil_test.go | 6 +++ 5 files changed, 56 insertions(+), 40 deletions(-) diff --git a/cmd/podman-wslkerninst/main.go b/cmd/podman-wslkerninst/main.go index 9a7d36a28d..c2ebc60240 100644 --- a/cmd/podman-wslkerninst/main.go +++ b/cmd/podman-wslkerninst/main.go @@ -48,7 +48,7 @@ func installWslKernel() error { ) backoff := 500 * time.Millisecond for i := 1; i < 6; i++ { - err = wutil.SilentExec("wsl", "--update") + err = wutil.SilentExec("--update") if err == nil { break } diff --git a/pkg/machine/wsl/machine.go b/pkg/machine/wsl/machine.go index 9808f38d4c..cbdc875303 100644 --- a/pkg/machine/wsl/machine.go +++ b/pkg/machine/wsl/machine.go @@ -20,6 +20,7 @@ import ( "github.com/containers/podman/v5/pkg/machine/env" "github.com/containers/podman/v5/pkg/machine/ignition" "github.com/containers/podman/v5/pkg/machine/vmconfigs" + "github.com/containers/podman/v5/pkg/machine/wsl/wutil" "github.com/containers/podman/v5/utils" "github.com/containers/storage/pkg/homedir" "github.com/sirupsen/logrus" @@ -100,7 +101,8 @@ func provisionWSLDist(name string, imagePath string, prompt string) (string, err // 1. Wsl/Service/RegisterDistro/CreateVm/HCS/ERROR_NOT_SUPPORTED // 2. Wsl/Service/RegisterDistro/CreateVm/HCS/HCS_E_SERVICE_NOT_AVAILABLE cmdOutput := &bytes.Buffer{} - err = runCmdPassThroughTee(cmdOutput, "wsl", "--import", dist, distTarget, imagePath, "--version", "2") + cmd := wutil.NewWSLCommand("--import", dist, distTarget, imagePath, "--version", "2") + err = runCmdPassThroughTee(cmdOutput, cmd) decoder := unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewDecoder() decoded, _, decodeErr := transform.Bytes(decoder, cmdOutput.Bytes()) if decodeErr != nil { @@ -373,13 +375,15 @@ func installWsl() error { return err } defer log.Close() - if err := runCmdPassThroughTee(log, "dism", "/online", "/enable-feature", - "/featurename:Microsoft-Windows-Subsystem-Linux", "/all", "/norestart"); isMsiError(err) { + cmd := exec.Command("dism", "/online", "/enable-feature", + "/featurename:Microsoft-Windows-Subsystem-Linux", "/all", "/norestart") + if err := runCmdPassThroughTee(log, cmd); isMsiError(err) { return fmt.Errorf("could not enable WSL Feature: %w", err) } - if err = runCmdPassThroughTee(log, "dism", "/online", "/enable-feature", - "/featurename:VirtualMachinePlatform", "/all", "/norestart"); isMsiError(err) { + cmd = exec.Command("dism", "/online", "/enable-feature", + "/featurename:VirtualMachinePlatform", "/all", "/norestart") + if err = runCmdPassThroughTee(log, cmd); isMsiError(err) { return fmt.Errorf("could not enable Virtual Machine Feature: %w", err) } @@ -466,50 +470,49 @@ func withUser(s string, user string) string { func wslInvoke(dist string, arg ...string) error { newArgs := []string{"-u", "root", "-d", dist} newArgs = append(newArgs, arg...) - return runCmdPassThrough("wsl", newArgs...) + cmd := wutil.NewWSLCommand(newArgs...) + return runCmdPassThrough(cmd) } func wslPipe(input string, dist string, arg ...string) error { newArgs := []string{"-u", "root", "-d", dist} newArgs = append(newArgs, arg...) - return pipeCmdPassThrough("wsl", input, newArgs...) + cmd := wutil.NewWSLCommand(newArgs...) + return pipeCmdPassThrough(cmd, input) } -func runCmdPassThrough(name string, arg ...string) error { - logrus.Debugf("Running command: %s %v", name, arg) - cmd := exec.Command(name, arg...) +func runCmdPassThrough(cmd *exec.Cmd) error { + logrus.Debugf("Running command: %s %v", cmd.Path, cmd.Args) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { - return fmt.Errorf("command %s %v failed: %w", name, arg, err) + return fmt.Errorf("command %s %v failed: %w", cmd.Path, cmd.Args, err) } return nil } -func runCmdPassThroughTee(out io.Writer, name string, arg ...string) error { - logrus.Debugf("Running command: %s %v", name, arg) +func runCmdPassThroughTee(out io.Writer, cmd *exec.Cmd) error { + logrus.Debugf("Running command: %s %v", cmd.Path, cmd.Args) // TODO - Perhaps improve this with a conpty pseudo console so that // dism installer text bars mirror console behavior (redraw) - cmd := exec.Command(name, arg...) cmd.Stdin = os.Stdin cmd.Stdout = io.MultiWriter(os.Stdout, out) cmd.Stderr = io.MultiWriter(os.Stderr, out) if err := cmd.Run(); isMsiError(err) { - return fmt.Errorf("command %s %v failed: %w", name, arg, err) + return fmt.Errorf("command %s %v failed: %w", cmd.Path, cmd.Args, err) } return nil } -func pipeCmdPassThrough(name string, input string, arg ...string) error { - logrus.Debugf("Running command: %s %v", name, arg) - cmd := exec.Command(name, arg...) +func pipeCmdPassThrough(cmd *exec.Cmd, input string) error { + logrus.Debugf("Running command: %s %v", cmd.Path, cmd.Args) cmd.Stdin = strings.NewReader(input) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { - return fmt.Errorf("command %s %v failed: %w", name, arg, err) + return fmt.Errorf("command %s %v failed: %w", cmd.Path, cmd.Args, err) } return nil } @@ -569,7 +572,7 @@ func getAllWSLDistros(running bool) (map[string]struct{}, error) { if running { args = append(args, "--running") } - cmd := exec.Command("wsl", args...) + cmd := wutil.NewWSLCommand(args...) out, err := cmd.StdoutPipe() if err != nil { return nil, err @@ -598,7 +601,7 @@ func getAllWSLDistros(running bool) (map[string]struct{}, error) { } func isSystemdRunning(dist string) (bool, error) { - cmd := exec.Command("wsl", "-u", "root", "-d", dist, "sh") + cmd := wutil.NewWSLCommand("-u", "root", "-d", dist, "sh") cmd.Stdin = strings.NewReader(sysdpid + "\necho $SYSDPID\n") out, err := cmd.StdoutPipe() if err != nil { @@ -621,26 +624,26 @@ func isSystemdRunning(dist string) (bool, error) { err = cmd.Wait() if err != nil { - return false, fmt.Errorf("command %s %v failed: %w (%s)", cmd.Path, cmd.Args, err, strings.TrimSpace(stderr.String())) + return false, fmt.Errorf("command %s %v failed: %w (%s)", cmd.Path, cmd.Args[1:], err, strings.TrimSpace(stderr.String())) } return result, nil } func terminateDist(dist string) error { - cmd := exec.Command("wsl", "--terminate", dist) + cmd := wutil.NewWSLCommand("--terminate", dist) out, err := cmd.CombinedOutput() if err != nil { - return fmt.Errorf("command %s %v failed: %w (%s)", cmd.Path, cmd.Args, err, strings.TrimSpace(string(out))) + return fmt.Errorf("command %s %v failed: %w (%s)", cmd.Path, cmd.Args[1:], err, strings.TrimSpace(string(out))) } return nil } func unregisterDist(dist string) error { - cmd := exec.Command("wsl", "--unregister", dist) + cmd := wutil.NewWSLCommand("--unregister", dist) out, err := cmd.CombinedOutput() if err != nil { - return fmt.Errorf("command %s %v failed: %w (%s)", cmd.Path, cmd.Args, err, strings.TrimSpace(string(out))) + return fmt.Errorf("command %s %v failed: %w (%s)", cmd.Path, cmd.Args[1:], err, strings.TrimSpace(string(out))) } return nil } @@ -685,13 +688,14 @@ func getCPUs(name string) (uint64, error) { if run, _ := isWSLRunning(dist); !run { return 0, nil } - cmd := exec.Command("wsl", "-u", "root", "-d", dist, "nproc") + cmd := wutil.NewWSLCommand("-u", "root", "-d", dist, "nproc") out, err := cmd.StdoutPipe() if err != nil { return 0, err } stderr := &bytes.Buffer{} cmd.Stderr = stderr + cmd.Env = []string{"WSL_UTF8=1"} if err = cmd.Start(); err != nil { return 0, err } @@ -702,7 +706,7 @@ func getCPUs(name string) (uint64, error) { } err = cmd.Wait() if err != nil { - return 0, fmt.Errorf("command %s %v failed: %w (%s)", cmd.Path, cmd.Args, err, strings.TrimSpace(strings.TrimSpace(stderr.String()))) + return 0, fmt.Errorf("command %s %v failed: %w (%s)", cmd.Path, cmd.Args[1:], err, strings.TrimSpace(strings.TrimSpace(stderr.String()))) } ret, err := strconv.Atoi(result) @@ -715,7 +719,7 @@ func getMem(name string) (strongunits.MiB, error) { if run, _ := isWSLRunning(dist); !run { return 0, nil } - cmd := exec.Command("wsl", "-u", "root", "-d", dist, "cat", "/proc/meminfo") + cmd := wutil.NewWSLCommand("-u", "root", "-d", dist, "cat", "/proc/meminfo") out, err := cmd.StdoutPipe() if err != nil { return 0, err @@ -746,7 +750,7 @@ func getMem(name string) (strongunits.MiB, error) { } err = cmd.Wait() if err != nil { - return 0, fmt.Errorf("command %s %v failed: %w (%s)", cmd.Path, cmd.Args, err, strings.TrimSpace(stderr.String())) + return 0, fmt.Errorf("command %s %v failed: %w (%s)", cmd.Path, cmd.Args[1:], err, strings.TrimSpace(stderr.String())) } return strongunits.MiB(total - available), err diff --git a/pkg/machine/wsl/stubber.go b/pkg/machine/wsl/stubber.go index 6f6720c5dc..edf7e084f4 100644 --- a/pkg/machine/wsl/stubber.go +++ b/pkg/machine/wsl/stubber.go @@ -7,10 +7,10 @@ import ( "errors" "fmt" "os" - "os/exec" "strings" "github.com/containers/podman/v5/pkg/machine/env" + "github.com/containers/podman/v5/pkg/machine/wsl/wutil" gvproxy "github.com/containers/gvisor-tap-vsock/pkg/types" "github.com/containers/podman/v5/pkg/machine" @@ -108,7 +108,8 @@ func (w WSLStubber) Remove(mc *vmconfigs.MachineConfig) ([]string, func() error, // below if we wanted to hard error on the wsl unregister // of the vm wslRemoveFunc := func() error { - if err := runCmdPassThrough("wsl", "--unregister", env.WithPodmanPrefix(mc.Name)); err != nil { + cmd := wutil.NewWSLCommand("--unregister", env.WithPodmanPrefix(mc.Name)) + if err := runCmdPassThrough(cmd); err != nil { return err } return nil @@ -253,7 +254,7 @@ func (w WSLStubber) StopVM(mc *vmconfigs.MachineConfig, hardStop bool) error { fmt.Fprintf(os.Stderr, "Could not stop API forwarding service (win-sshproxy.exe): %s\n", err.Error()) } - cmd := exec.Command("wsl", "-u", "root", "-d", dist, "sh") + cmd := wutil.NewWSLCommand("-u", "root", "-d", dist, "sh") cmd.Stdin = strings.NewReader(waitTerm) out := &bytes.Buffer{} cmd.Stderr = out @@ -263,7 +264,7 @@ func (w WSLStubber) StopVM(mc *vmconfigs.MachineConfig, hardStop bool) error { return fmt.Errorf("executing wait command: %w", err) } - exitCmd := exec.Command("wsl", "-u", "root", "-d", dist, "/usr/local/bin/enterns", "systemctl", "exit", "0") + exitCmd := wutil.NewWSLCommand("-u", "root", "-d", dist, "/usr/local/bin/enterns", "systemctl", "exit", "0") if err = exitCmd.Run(); err != nil { return fmt.Errorf("stopping systemd: %w", err) } diff --git a/pkg/machine/wsl/wutil/wutil.go b/pkg/machine/wsl/wutil/wutil.go index b65ab7bcb2..8a7a1ddfb6 100644 --- a/pkg/machine/wsl/wutil/wutil.go +++ b/pkg/machine/wsl/wutil/wutil.go @@ -29,8 +29,13 @@ type wslStatus struct { wslFeatureEnabled bool } +func NewWSLCommand(arg ...string) *exec.Cmd { + cmd := exec.Command("wsl", arg...) + return cmd +} + func SilentExec(command string, args ...string) error { - cmd := exec.Command(command, args...) + cmd := NewWSLCommand(args...) cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: 0x08000000} cmd.Stdout = nil cmd.Stderr = nil @@ -40,8 +45,8 @@ func SilentExec(command string, args ...string) error { return nil } -func SilentExecCmd(command string, args ...string) *exec.Cmd { - cmd := exec.Command(command, args...) +func SilentExecCmd(args ...string) *exec.Cmd { + cmd := NewWSLCommand(args...) cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: 0x08000000} return cmd } @@ -53,7 +58,7 @@ func parseWSLStatus() wslStatus { vmpFeatureEnabled: false, wslFeatureEnabled: false, } - cmd := SilentExecCmd("wsl", "--status") + cmd := SilentExecCmd("--status") out, err := cmd.StdoutPipe() cmd.Stderr = nil if err != nil { @@ -79,7 +84,7 @@ func IsWSLInstalled() bool { } func IsWSLStoreVersionInstalled() bool { - cmd := SilentExecCmd("wsl", "--version") + cmd := SilentExecCmd("--version") cmd.Stdout = nil cmd.Stderr = nil if err := cmd.Run(); err != nil { diff --git a/pkg/machine/wsl/wutil/wutil_test.go b/pkg/machine/wsl/wutil/wutil_test.go index be27edc1ee..f10427f02c 100644 --- a/pkg/machine/wsl/wutil/wutil_test.go +++ b/pkg/machine/wsl/wutil/wutil_test.go @@ -144,3 +144,9 @@ func TestMatchOutputLine(t *testing.T) { }) } } + +func TestNewWSLCommand(t *testing.T) { + cmd := NewWSLCommand("--status") + assert.Contains(t, cmd.Path, "wsl") + assert.Equal(t, []string{"--status"}, cmd.Args[1:]) +} From 68e71365361fda79a1d477082f93f8c62b953feb Mon Sep 17 00:00:00 2001 From: Mario Loriedo Date: Wed, 2 Jul 2025 09:48:16 +0200 Subject: [PATCH 2/2] Enforce wsl.exe UTF-8 encoded output Currently WSL uses UTF-16 encoded output by default but is planning to use UTF-8. See https://github.com/containers/podman/issues/26527 To get ready for the change we are enforcing UTF-8 encoded output by setting the environment variable `WSL_UTF8=1` and by updating the code that transfomed wsl output from UTF-16 to UTF-8. Fixes https://github.com/containers/podman/issues/26527 Signed-off-by: Mario Loriedo --- go.mod | 2 +- pkg/machine/wsl/machine.go | 11 ++--------- pkg/machine/wsl/wutil/wutil.go | 7 +++---- pkg/machine/wsl/wutil/wutil_test.go | 7 ++----- 4 files changed, 8 insertions(+), 19 deletions(-) diff --git a/go.mod b/go.mod index d9f407109f..8482a540e0 100644 --- a/go.mod +++ b/go.mod @@ -73,7 +73,6 @@ require ( golang.org/x/sync v0.15.0 golang.org/x/sys v0.33.0 golang.org/x/term v0.32.0 - golang.org/x/text v0.26.0 google.golang.org/protobuf v1.36.6 gopkg.in/inf.v0 v0.9.1 gopkg.in/yaml.v3 v3.0.1 @@ -185,6 +184,7 @@ require ( go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/mod v0.25.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/text v0.26.0 // indirect golang.org/x/time v0.11.0 // indirect golang.org/x/tools v0.33.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect diff --git a/pkg/machine/wsl/machine.go b/pkg/machine/wsl/machine.go index cbdc875303..4390a8f438 100644 --- a/pkg/machine/wsl/machine.go +++ b/pkg/machine/wsl/machine.go @@ -24,8 +24,6 @@ import ( "github.com/containers/podman/v5/utils" "github.com/containers/storage/pkg/homedir" "github.com/sirupsen/logrus" - "golang.org/x/text/encoding/unicode" - "golang.org/x/text/transform" ) var ( @@ -103,12 +101,7 @@ func provisionWSLDist(name string, imagePath string, prompt string) (string, err cmdOutput := &bytes.Buffer{} cmd := wutil.NewWSLCommand("--import", dist, distTarget, imagePath, "--version", "2") err = runCmdPassThroughTee(cmdOutput, cmd) - decoder := unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewDecoder() - decoded, _, decodeErr := transform.Bytes(decoder, cmdOutput.Bytes()) - if decodeErr != nil { - return "", fmt.Errorf("failed to decode WSL output: %w", decodeErr) - } - decodedStr := strings.ToLower(string(decoded)) + decodedStr := strings.ToLower(cmdOutput.String()) for _, substr := range []string{"hcs/error_not_supported", "hcs/hcs_e_service_not_available"} { if strings.Contains(decodedStr, substr) { return "", ErrWslNotSupported @@ -584,7 +577,7 @@ func getAllWSLDistros(running bool) (map[string]struct{}, error) { } all := make(map[string]struct{}) - scanner := bufio.NewScanner(transform.NewReader(out, unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewDecoder())) + scanner := bufio.NewScanner(out) for scanner.Scan() { fields := strings.Fields(scanner.Text()) if len(fields) > 0 { diff --git a/pkg/machine/wsl/wutil/wutil.go b/pkg/machine/wsl/wutil/wutil.go index 8a7a1ddfb6..69ec64cccc 100644 --- a/pkg/machine/wsl/wutil/wutil.go +++ b/pkg/machine/wsl/wutil/wutil.go @@ -6,13 +6,11 @@ import ( "bufio" "fmt" "io" + "os" "os/exec" "strings" "sync" "syscall" - - "golang.org/x/text/encoding/unicode" - "golang.org/x/text/transform" ) var ( @@ -31,6 +29,7 @@ type wslStatus struct { func NewWSLCommand(arg ...string) *exec.Cmd { cmd := exec.Command("wsl", arg...) + cmd.Env = append(os.Environ(), "WSL_UTF8=1") return cmd } @@ -100,7 +99,7 @@ func matchOutputLine(output io.ReadCloser) wslStatus { vmpFeatureEnabled: true, wslFeatureEnabled: true, } - scanner := bufio.NewScanner(transform.NewReader(output, unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewDecoder())) + scanner := bufio.NewScanner(output) for scanner.Scan() { line := scanner.Text() for _, match := range wslNotInstalledMessages { diff --git a/pkg/machine/wsl/wutil/wutil_test.go b/pkg/machine/wsl/wutil/wutil_test.go index f10427f02c..fe015a9d6e 100644 --- a/pkg/machine/wsl/wutil/wutil_test.go +++ b/pkg/machine/wsl/wutil/wutil_test.go @@ -8,7 +8,6 @@ import ( "testing" "github.com/stretchr/testify/assert" - "golang.org/x/text/encoding/unicode" ) const ( @@ -136,10 +135,7 @@ func TestMatchOutputLine(t *testing.T) { } for _, tt := range tests { t.Run(tt.winVariant, func(t *testing.T) { - encoder := unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewEncoder() - encodedOutput, err := encoder.String(tt.statusOutput) - assert.Nil(t, err) - reader := io.NopCloser(strings.NewReader(encodedOutput)) + reader := io.NopCloser(strings.NewReader(tt.statusOutput)) assert.Equal(t, tt.want, matchOutputLine(reader)) }) } @@ -149,4 +145,5 @@ func TestNewWSLCommand(t *testing.T) { cmd := NewWSLCommand("--status") assert.Contains(t, cmd.Path, "wsl") assert.Equal(t, []string{"--status"}, cmd.Args[1:]) + assert.Contains(t, cmd.Env, "WSL_UTF8=1") }