mirror of
https://github.com/containers/podman.git
synced 2025-10-17 03:04:21 +08:00

this patch changes how the detection of wsl works. The old way of using wsl --status command output to detect some missing features required by WSL is not fully reliable. WSL checks if the wsl feature is enabled and if the vmcompute service do exist. However, this is not enough to identify if the virtual machine platform feature is enabled. The vmcompute service could exist because it has been installed by other tools or it could exist but being stopped. The way proposed by this patch is to try execute the import command and, if it fails, check the error and if it is related to the Host Compute Service try to install all features required by WSL. The flow is the same as before, the user is asked to execute the podman machine init command with elevated privileges. Eventually, after enabling WSL and VMP features, the user is asked to reboot the machine. When the machine restarts, the powershell gets invoked again and execute the command init. The code also fixes some issues that could cause misbehaviors when invoking recursively the elevated shell, like an unreleased lock, or a missing file. Signed-off-by: lstocchi <lstocchi@redhat.com>
298 lines
8.3 KiB
Go
298 lines
8.3 KiB
Go
//go:build windows
|
|
|
|
package wsl
|
|
|
|
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/v5/pkg/machine/define"
|
|
"github.com/containers/storage/pkg/fileutils"
|
|
"github.com/containers/storage/pkg/homedir"
|
|
"github.com/sirupsen/logrus"
|
|
"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"
|
|
|
|
// "Application: Installation (Planned)" A planned restart or shutdown to perform application installation.
|
|
// ref: https://learn.microsoft.com/en-us/windows/win32/shutdown/system-shutdown-reason-codes
|
|
rebootReason = windows.SHTDN_REASON_MAJOR_APPLICATION | windows.SHTDN_REASON_MINOR_INSTALLATION | windows.SHTDN_REASON_FLAG_PLANNED
|
|
|
|
// ref: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-exitwindowsex#parameters
|
|
rebootFlags = windows.EWX_REBOOT | windows.EWX_RESTARTAPPS | windows.EWX_FORCEIFHUNG
|
|
)
|
|
|
|
func winVersionAtLeast(major uint, minor uint, build uint) bool {
|
|
var out [3]uint32
|
|
|
|
in := []uint32{uint32(major), uint32(minor), uint32(build)}
|
|
out[0], out[1], out[2] = windows.RtlGetNtVersionNumbers()
|
|
|
|
for i, o := range out {
|
|
if in[i] > o {
|
|
return false
|
|
}
|
|
if in[i] < o {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func HasAdminRights() bool {
|
|
var sid *windows.SID
|
|
|
|
// See: https://coolaj86.com/articles/golang-and-windows-and-admins-oh-my/
|
|
if err := windows.AllocateAndInitializeSid(
|
|
&windows.SECURITY_NT_AUTHORITY,
|
|
2,
|
|
windows.SECURITY_BUILTIN_DOMAIN_RID,
|
|
windows.DOMAIN_ALIAS_RID_ADMINS,
|
|
0, 0, 0, 0, 0, 0,
|
|
&sid); err != nil {
|
|
logrus.Warnf("SID allocation error: %s", err)
|
|
return false
|
|
}
|
|
defer func() {
|
|
_ = windows.FreeSid(sid)
|
|
}()
|
|
|
|
// From MS docs:
|
|
// "If TokenHandle is NULL, CheckTokenMembership uses the impersonation
|
|
// token of the calling thread. If the thread is not impersonating,
|
|
// the function duplicates the thread's primary token to create an
|
|
// impersonation token."
|
|
token := windows.Token(0)
|
|
|
|
member, err := token.IsMember(sid)
|
|
if err != nil {
|
|
logrus.Warnf("Token Membership Error: %s", err)
|
|
return false
|
|
}
|
|
|
|
return member || token.IsElevated()
|
|
}
|
|
|
|
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 ...interface{}) 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`
|
|
wtPrefix = `%LocalAppData%\Microsoft\WindowsApps\wt -p "Windows PowerShell" `
|
|
localAppData = "LocalAppData"
|
|
pShellLaunch = `powershell -noexit "powershell -EncodedCommand (Get-Content '%s')"`
|
|
)
|
|
|
|
exe, _ := os.Executable()
|
|
relaunch := fmt.Sprintf("& %s %s", syscall.EscapeArg(exe), buildCommandArgs(false))
|
|
encoded := base64.StdEncoding.EncodeToString(encodeUTF16Bytes(relaunch))
|
|
|
|
dataDir, err := homedir.GetDataHome()
|
|
if err != nil {
|
|
return fmt.Errorf("could not determine data directory: %w", err)
|
|
}
|
|
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
|
return fmt.Errorf("could not create data directory: %w", err)
|
|
}
|
|
commFile := filepath.Join(dataDir, "podman-relaunch.dat")
|
|
if err := os.WriteFile(commFile, []byte(encoded), 0600); err != nil {
|
|
return fmt.Errorf("could not serialize command state: %w", err)
|
|
}
|
|
|
|
command := fmt.Sprintf(pShellLaunch, commFile)
|
|
if err := fileutils.Lexists(filepath.Join(os.Getenv(localAppData), wtLocation)); err == nil {
|
|
wtCommand := wtPrefix + command
|
|
// RunOnce is limited to 260 chars (supposedly no longer in Builds >= 19489)
|
|
// For now fallback in cases of long usernames (>89 chars)
|
|
if len(wtCommand) < 260 {
|
|
command = wtCommand
|
|
}
|
|
}
|
|
|
|
if err := addRunOnceRegistryEntry(command); err != nil {
|
|
return err
|
|
}
|
|
|
|
message := "To continue the process of enabling WSL, the system needs to reboot. " +
|
|
"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 {
|
|
fmt.Println("Reboot is required to continue installation, please reboot at your convenience")
|
|
os.Exit(ErrorSuccessRebootRequired)
|
|
return nil
|
|
}
|
|
|
|
if err := winio.RunWithPrivilege(rebootPrivilege, func() error {
|
|
if err := windows.ExitWindowsEx(rebootFlags, rebootReason); err != nil {
|
|
return fmt.Errorf("execute ExitWindowsEx to reboot system failed: %w", err)
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return fmt.Errorf("cannot reboot system: %w", err)
|
|
}
|
|
|
|
return define.ErrRebootInitiated
|
|
}
|
|
|
|
func addRunOnceRegistryEntry(command string) error {
|
|
k, _, err := registry.CreateKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\RunOnce`, registry.WRITE)
|
|
if err != nil {
|
|
return fmt.Errorf("could not open RunOnce registry entry: %w", err)
|
|
}
|
|
|
|
defer k.Close()
|
|
|
|
if err := k.SetExpandStringValue("podman-machine", command); err != nil {
|
|
return fmt.Errorf("could not open RunOnce registry entry: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func encodeUTF16Bytes(s string) []byte {
|
|
u16 := utf16.Encode([]rune(s))
|
|
buf := new(bytes.Buffer)
|
|
for _, r := range u16 {
|
|
_ = binary.Write(buf, binary.LittleEndian, r)
|
|
}
|
|
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, " ")
|
|
}
|