Merge pull request #26553 from l0rd/wsl-utf8

Enforce WSL UTF-8 encoded output
This commit is contained in:
openshift-merge-bot[bot]
2025-07-07 17:52:48 +00:00
committed by GitHub
6 changed files with 64 additions and 59 deletions

View File

@ -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
}

2
go.mod
View File

@ -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

View File

@ -20,11 +20,10 @@ 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"
"golang.org/x/text/encoding/unicode"
"golang.org/x/text/transform"
)
var (
@ -100,13 +99,9 @@ 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")
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))
cmd := wutil.NewWSLCommand("--import", dist, distTarget, imagePath, "--version", "2")
err = runCmdPassThroughTee(cmdOutput, cmd)
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
@ -373,13 +368,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 +463,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 +565,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
@ -581,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 {
@ -598,7 +594,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 +617,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 +681,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 +699,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 +712,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 +743,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

View File

@ -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)
}

View File

@ -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 (
@ -29,8 +27,14 @@ type wslStatus struct {
wslFeatureEnabled bool
}
func NewWSLCommand(arg ...string) *exec.Cmd {
cmd := exec.Command("wsl", arg...)
cmd.Env = append(os.Environ(), "WSL_UTF8=1")
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 +44,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 +57,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 +83,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 {
@ -95,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 {

View File

@ -8,7 +8,6 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"golang.org/x/text/encoding/unicode"
)
const (
@ -136,11 +135,15 @@ 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))
})
}
}
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")
}