move wsl' util_windows to windows package so it can be reused by hyperv

Signed-off-by: lstocchi <lstocchi@redhat.com>
This commit is contained in:
lstocchi
2026-01-19 11:29:11 +01:00
parent 8c483b4da0
commit 5b0ec91c54
3 changed files with 222 additions and 199 deletions

View File

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

View File

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

View File

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