From 5b0ec91c54ed0ceed4f8d7980f22976073bd8689 Mon Sep 17 00:00:00 2001 From: lstocchi Date: Mon, 19 Jan 2026 11:29:11 +0100 Subject: [PATCH] move wsl' util_windows to windows package so it can be reused by hyperv Signed-off-by: lstocchi --- pkg/machine/windows/util_windows.go | 211 ++++++++++++++++++++++++++++ pkg/machine/wsl/machine.go | 78 ++-------- pkg/machine/wsl/util_windows.go | 132 +---------------- 3 files changed, 222 insertions(+), 199 deletions(-) diff --git a/pkg/machine/windows/util_windows.go b/pkg/machine/windows/util_windows.go index 64e7b261a2..ac43d8dc6b 100644 --- a/pkg/machine/windows/util_windows.go +++ b/pkg/machine/windows/util_windows.go @@ -1,7 +1,17 @@ package windows import ( + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "syscall" + "unsafe" + "github.com/sirupsen/logrus" + "go.podman.io/storage/pkg/homedir" "golang.org/x/sys/windows" ) @@ -38,3 +48,204 @@ func HasAdminRights() bool { return member || token.IsElevated() } + +type SHELLEXECUTEINFO struct { + cbSize uint32 + fMask uint32 + hwnd syscall.Handle + lpVerb uintptr + lpFile uintptr + lpParameters uintptr + lpDirectory uintptr + nShow int + hInstApp syscall.Handle + lpIDList uintptr + lpClass uintptr + hkeyClass syscall.Handle + dwHotKey uint32 + hIconOrMonitor syscall.Handle + hProcess syscall.Handle +} + +// Cleaner to refer to the official OS constant names, and consistent with syscall +// Ref: https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-shellexecuteinfow#members +const ( + SEE_MASK_NOCLOSEPROCESS = 0x40 + SE_ERR_ACCESSDENIED = 0x05 +) + +type ExitCodeError struct { + Code uint +} + +func (e *ExitCodeError) Error() string { + return fmt.Sprintf("process exited with code %d", e.Code) +} + +// BuildCommandArgs builds command line arguments for re-execution, optionally adding --reexec flag +// for specified commands (init, rm) +func BuildCommandArgs(elevate bool) string { + var args []string + for _, arg := range os.Args[1:] { + if arg != "--reexec" { + args = append(args, syscall.EscapeArg(arg)) + if elevate && (arg == "init" || arg == "rm") { + args = append(args, "--reexec") + } + } + } + return strings.Join(args, " ") +} + +// RelaunchElevatedWait launches the current executable with elevated privileges and waits for it to complete +func RelaunchElevatedWait() error { + e, _ := os.Executable() + d, _ := os.Getwd() + exe, _ := syscall.UTF16PtrFromString(e) + cwd, _ := syscall.UTF16PtrFromString(d) + arg, _ := syscall.UTF16PtrFromString(BuildCommandArgs(true)) + verb, _ := syscall.UTF16PtrFromString("runas") + + shell32 := syscall.NewLazyDLL("shell32.dll") + + info := &SHELLEXECUTEINFO{ + fMask: SEE_MASK_NOCLOSEPROCESS, + hwnd: 0, + lpVerb: uintptr(unsafe.Pointer(verb)), + lpFile: uintptr(unsafe.Pointer(exe)), + lpParameters: uintptr(unsafe.Pointer(arg)), + lpDirectory: uintptr(unsafe.Pointer(cwd)), + nShow: syscall.SW_SHOWNORMAL, + } + info.cbSize = uint32(unsafe.Sizeof(*info)) + procShellExecuteEx := shell32.NewProc("ShellExecuteExW") + if ret, _, _ := procShellExecuteEx.Call(uintptr(unsafe.Pointer(info))); ret == 0 { // 0 = False + err := syscall.GetLastError() + if info.hInstApp == SE_ERR_ACCESSDENIED { + return wrapMaybe(err, "request to elevate privileges was denied") + } + return wrapMaybef(err, "could not launch process, ShellEX Error = %d", info.hInstApp) + } + + handle := info.hProcess + defer func() { + _ = syscall.CloseHandle(handle) + }() + + w, err := syscall.WaitForSingleObject(handle, syscall.INFINITE) + switch w { + case syscall.WAIT_OBJECT_0: + break + case syscall.WAIT_FAILED: + return fmt.Errorf("could not wait for process, failed: %w", err) + default: + return fmt.Errorf("could not wait for process, unknown error. event: %X, err: %v", w, err) + } + var code uint32 + if err := syscall.GetExitCodeProcess(handle, &code); err != nil { + return err + } + if code != 0 { + return &ExitCodeError{uint(code)} + } + + return nil +} + +// MessageBox shows a Windows message box and returns the user's choice +func MessageBox(caption, title string, fail bool) int { + var format uint32 + if fail { + format = windows.MB_ICONERROR + } else { + format = windows.MB_OKCANCEL | windows.MB_ICONINFORMATION + } + + captionPtr, _ := syscall.UTF16PtrFromString(caption) + titlePtr, _ := syscall.UTF16PtrFromString(title) + + ret, _ := windows.MessageBox(0, captionPtr, titlePtr, format) + + return int(ret) +} + +func wrapMaybe(err error, message string) error { + if err != nil { + return fmt.Errorf("%v: %w", message, err) + } + + return errors.New(message) +} + +func wrapMaybef(err error, format string, args ...any) error { + if err != nil { + return fmt.Errorf(format+": %w", append(args, err)...) + } + + return fmt.Errorf(format, args...) +} + +// IsReExecuting checks if the current process was re-executed with --reexec flag +func IsReExecuting() bool { + for _, arg := range os.Args { + if arg == "--reexec" { + return true + } + } + return false +} + +func CreateOrTruncateElevatedOutputFile() error { + name, err := getElevatedOutputFileName() + if err != nil { + return err + } + + _, err = os.Create(name) + return err +} + +// getElevatedOutputFileName returns the path to the elevated output log file +func getElevatedOutputFileName() (string, error) { + dir, err := homedir.GetDataHome() + if err != nil { + return "", err + } + return filepath.Join(dir, "podman-elevated-output.log"), nil +} + +func getElevatedOutputFileRead() (*os.File, error) { + return getElevatedOutputFile(os.O_RDONLY) +} + +func GetElevatedOutputFileWrite() (*os.File, error) { + return getElevatedOutputFile(os.O_WRONLY | os.O_CREATE | os.O_APPEND) +} + +func getElevatedOutputFile(mode int) (*os.File, error) { + name, err := getElevatedOutputFileName() + if err != nil { + return nil, err + } + + dir, err := homedir.GetDataHome() + if err != nil { + return nil, err + } + + if err = os.MkdirAll(dir, 0o755); err != nil { + return nil, err + } + + return os.OpenFile(name, mode, 0o644) +} + +func DumpOutputFile() { + file, err := getElevatedOutputFileRead() + if err != nil { + logrus.Debug("could not find elevated child output file") + return + } + defer file.Close() + _, _ = io.Copy(os.Stdout, file) +} diff --git a/pkg/machine/wsl/machine.go b/pkg/machine/wsl/machine.go index 6374b64abf..1c8e57d3f3 100644 --- a/pkg/machine/wsl/machine.go +++ b/pkg/machine/wsl/machine.go @@ -18,12 +18,12 @@ import ( "github.com/containers/podman/v6/pkg/machine/env" "github.com/containers/podman/v6/pkg/machine/ignition" "github.com/containers/podman/v6/pkg/machine/vmconfigs" + winutil "github.com/containers/podman/v6/pkg/machine/windows" "github.com/containers/podman/v6/pkg/machine/wsl/wutil" "github.com/containers/podman/v6/utils" "github.com/sirupsen/logrus" "go.podman.io/common/pkg/config" "go.podman.io/common/pkg/strongunits" - "go.podman.io/storage/pkg/homedir" ) var ( @@ -32,14 +32,6 @@ var ( ErrWslNotSupported = errors.New("wsl features not supported or configured correctly") ) -type ExitCodeError struct { - code uint -} - -func (e *ExitCodeError) Error() string { - return fmt.Sprintf("Process failed with exit code: %d", e.code) -} - //nolint:unused func getConfigPath(name string) (string, error) { return getConfigPathExt(name, "json") @@ -331,7 +323,7 @@ func attemptFeatureInstall(reExec, admin bool) error { message += "NOTE: A system reboot will be required as part of this process. " + "If you prefer, you may abort now, and perform a manual installation using the \"wsl --install\" command." - if !reExec && MessageBox(message, "Podman Machine", false) != 1 { + if !reExec && winutil.MessageBox(message, "Podman Machine", false) != 1 { return fmt.Errorf("the WSL installation aborted: %w", define.ErrInitRelaunchAttempt) } @@ -342,20 +334,20 @@ func attemptFeatureInstall(reExec, admin bool) error { } func launchElevate(operation string) error { - if err := createOrTruncateElevatedOutputFile(); err != nil { + if err := winutil.CreateOrTruncateElevatedOutputFile(); err != nil { return err } - err := relaunchElevatedWait() + err := winutil.RelaunchElevatedWait() if err != nil { - if eerr, ok := err.(*ExitCodeError); ok { - if eerr.code == ErrorSuccessRebootRequired { + if eerr, ok := err.(*winutil.ExitCodeError); ok { + if eerr.Code == ErrorSuccessRebootRequired { fmt.Println("Reboot is required to continue installation, please reboot at your convenience") return define.ErrInitRelaunchAttempt } } fmt.Fprintf(os.Stderr, "Elevated process failed with error: %v\n\n", err) - dumpOutputFile() + winutil.DumpOutputFile() fmt.Fprintf(os.Stderr, wslInstallError, operation) return fmt.Errorf("%w: %w", err, define.ErrInitRelaunchAttempt) } @@ -363,7 +355,7 @@ func launchElevate(operation string) error { } func installWsl() error { - log, err := getElevatedOutputFileWrite() + log, err := winutil.GetElevatedOutputFileWrite() if err != nil { return err } @@ -383,60 +375,6 @@ func installWsl() error { return reboot() } -func getElevatedOutputFileName() (string, error) { - dir, err := homedir.GetDataHome() - if err != nil { - return "", err - } - return filepath.Join(dir, "podman-elevated-output.log"), nil -} - -func dumpOutputFile() { - file, err := getElevatedOutputFileRead() - if err != nil { - logrus.Debug("could not find elevated child output file") - return - } - defer file.Close() - _, _ = io.Copy(os.Stdout, file) -} - -func getElevatedOutputFileRead() (*os.File, error) { - return getElevatedOutputFile(os.O_RDONLY) -} - -func getElevatedOutputFileWrite() (*os.File, error) { - return getElevatedOutputFile(os.O_WRONLY | os.O_CREATE | os.O_APPEND) -} - -func createOrTruncateElevatedOutputFile() error { - name, err := getElevatedOutputFileName() - if err != nil { - return err - } - - _, err = os.Create(name) - return err -} - -func getElevatedOutputFile(mode int) (*os.File, error) { - name, err := getElevatedOutputFileName() - if err != nil { - return nil, err - } - - dir, err := homedir.GetDataHome() - if err != nil { - return nil, err - } - - if err = os.MkdirAll(dir, 0o755); err != nil { - return nil, err - } - - return os.OpenFile(name, mode, 0o644) -} - func isMsiError(err error) bool { if err == nil { return false diff --git a/pkg/machine/wsl/util_windows.go b/pkg/machine/wsl/util_windows.go index 27a8f8f645..70001316e5 100644 --- a/pkg/machine/wsl/util_windows.go +++ b/pkg/machine/wsl/util_windows.go @@ -6,48 +6,21 @@ import ( "bytes" "encoding/base64" "encoding/binary" - "errors" "fmt" "os" "path/filepath" - "strings" "syscall" "unicode/utf16" - "unsafe" "github.com/Microsoft/go-winio" "github.com/containers/podman/v6/pkg/machine/define" + winutil "github.com/containers/podman/v6/pkg/machine/windows" "go.podman.io/storage/pkg/fileutils" "go.podman.io/storage/pkg/homedir" "golang.org/x/sys/windows" "golang.org/x/sys/windows/registry" ) -type SHELLEXECUTEINFO struct { - cbSize uint32 - fMask uint32 - hwnd syscall.Handle - lpVerb uintptr - lpFile uintptr - lpParameters uintptr - lpDirectory uintptr - nShow int - hInstApp syscall.Handle - lpIDList uintptr - lpClass uintptr - hkeyClass syscall.Handle - dwHotKey uint32 - hIconOrMonitor syscall.Handle - hProcess syscall.Handle -} - -// Cleaner to refer to the official OS constant names, and consistent with syscall -// Ref: https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-shellexecuteinfow#members -const ( - SEE_MASK_NOCLOSEPROCESS = 0x40 - SE_ERR_ACCESSDENIED = 0x05 -) - const ( // ref: https://learn.microsoft.com/en-us/windows/win32/secauthz/privilege-constants#constants rebootPrivilege = "SeShutdownPrivilege" @@ -78,76 +51,6 @@ func winVersionAtLeast(major uint, minor uint, build uint) bool { return true } -func relaunchElevatedWait() error { - e, _ := os.Executable() - d, _ := os.Getwd() - exe, _ := syscall.UTF16PtrFromString(e) - cwd, _ := syscall.UTF16PtrFromString(d) - arg, _ := syscall.UTF16PtrFromString(buildCommandArgs(true)) - verb, _ := syscall.UTF16PtrFromString("runas") - - shell32 := syscall.NewLazyDLL("shell32.dll") - - info := &SHELLEXECUTEINFO{ - fMask: SEE_MASK_NOCLOSEPROCESS, - hwnd: 0, - lpVerb: uintptr(unsafe.Pointer(verb)), - lpFile: uintptr(unsafe.Pointer(exe)), - lpParameters: uintptr(unsafe.Pointer(arg)), - lpDirectory: uintptr(unsafe.Pointer(cwd)), - nShow: syscall.SW_SHOWNORMAL, - } - info.cbSize = uint32(unsafe.Sizeof(*info)) - procShellExecuteEx := shell32.NewProc("ShellExecuteExW") - if ret, _, _ := procShellExecuteEx.Call(uintptr(unsafe.Pointer(info))); ret == 0 { // 0 = False - err := syscall.GetLastError() - if info.hInstApp == SE_ERR_ACCESSDENIED { - return wrapMaybe(err, "request to elevate privileges was denied") - } - return wrapMaybef(err, "could not launch process, ShellEX Error = %d", info.hInstApp) - } - - handle := info.hProcess - defer func() { - _ = syscall.CloseHandle(handle) - }() - - w, err := syscall.WaitForSingleObject(handle, syscall.INFINITE) - switch w { - case syscall.WAIT_OBJECT_0: - break - case syscall.WAIT_FAILED: - return fmt.Errorf("could not wait for process, failed: %w", err) - default: - return fmt.Errorf("could not wait for process, unknown error. event: %X, err: %v", w, err) - } - var code uint32 - if err := syscall.GetExitCodeProcess(handle, &code); err != nil { - return err - } - if code != 0 { - return &ExitCodeError{uint(code)} - } - - return nil -} - -func wrapMaybe(err error, message string) error { - if err != nil { - return fmt.Errorf("%v: %w", message, err) - } - - return errors.New(message) -} - -func wrapMaybef(err error, format string, args ...any) error { - if err != nil { - return fmt.Errorf(format+": %w", append(args, err)...) - } - - return fmt.Errorf(format, args...) -} - func reboot() error { const ( wtLocation = `Microsoft\WindowsApps\wt.exe` @@ -157,7 +60,7 @@ func reboot() error { ) exe, _ := os.Executable() - relaunch := fmt.Sprintf("& %s %s", syscall.EscapeArg(exe), buildCommandArgs(false)) + relaunch := fmt.Sprintf("& %s %s", syscall.EscapeArg(exe), winutil.BuildCommandArgs(false)) encoded := base64.StdEncoding.EncodeToString(encodeUTF16Bytes(relaunch)) dataDir, err := homedir.GetDataHome() @@ -190,7 +93,7 @@ func reboot() error { "Alternatively, you can cancel and reboot manually\n\n" + "After rebooting, please wait a minute or two for podman machine to relaunch and continue installing." - if MessageBox(message, "Podman Machine", false) != 1 { + if winutil.MessageBox(message, "Podman Machine", false) != 1 { fmt.Println("Reboot is required to continue installation, please reboot at your convenience") os.Exit(ErrorSuccessRebootRequired) return nil @@ -231,32 +134,3 @@ func encodeUTF16Bytes(s string) []byte { } return buf.Bytes() } - -func MessageBox(caption, title string, fail bool) int { - var format uint32 - if fail { - format = windows.MB_ICONERROR - } else { - format = windows.MB_OKCANCEL | windows.MB_ICONINFORMATION - } - - captionPtr, _ := syscall.UTF16PtrFromString(caption) - titlePtr, _ := syscall.UTF16PtrFromString(title) - - ret, _ := windows.MessageBox(0, captionPtr, titlePtr, format) - - return int(ret) -} - -func buildCommandArgs(elevate bool) string { - var args []string - for _, arg := range os.Args[1:] { - if arg != "--reexec" { - args = append(args, syscall.EscapeArg(arg)) - if elevate && arg == "init" { - args = append(args, "--reexec") - } - } - } - return strings.Join(args, " ") -}