diff --git a/cmd/podman/machine/set.go b/cmd/podman/machine/set.go index 1c975622df..bf6a21c66d 100644 --- a/cmd/podman/machine/set.go +++ b/cmd/podman/machine/set.go @@ -128,7 +128,6 @@ func setMachine(cmd *cobra.Command, args []string) error { setOpts.DiskSize = &newDiskSizeGB } if cmd.Flags().Changed("user-mode-networking") { - // TODO This needs help setOpts.UserModeNetworking = &setFlags.UserModeNetworking } if cmd.Flags().Changed("usb") { diff --git a/go.mod b/go.mod index 8b373c3eea..9a7584eea1 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/containers/ocicrypt v1.1.9 github.com/containers/psgo v1.9.0 github.com/containers/storage v1.52.1-0.20240202181245-1419a5980565 + github.com/containers/winquit v1.1.0 github.com/coreos/go-systemd/v22 v22.5.1-0.20231103132048-7d375ecc2b09 github.com/coreos/stream-metadata-go v0.4.4 github.com/crc-org/crc/v2 v2.32.0 diff --git a/go.sum b/go.sum index c7430c366f..2270feaf4f 100644 --- a/go.sum +++ b/go.sum @@ -93,6 +93,8 @@ github.com/containers/psgo v1.9.0 h1:eJ74jzSaCHnWt26OlKZROSyUyRcGDf+gYBdXnxrMW4g github.com/containers/psgo v1.9.0/go.mod h1:0YoluUm43Mz2UnBIh1P+6V6NWcbpTL5uRtXyOcH0B5A= github.com/containers/storage v1.52.1-0.20240202181245-1419a5980565 h1:Gcirfx2DNoayB/+ypLgl5+ABzIPPDAoncs1qgZHHQHE= github.com/containers/storage v1.52.1-0.20240202181245-1419a5980565/go.mod h1:2E/QBqWVcJXwumP7nVUrampwRNL4XKjHL/aQya7ZdhI= +github.com/containers/winquit v1.1.0 h1:jArun04BNDQvt2W0Y78kh9TazN2EIEMG5Im6/JY7+pE= +github.com/containers/winquit v1.1.0/go.mod h1:PsPeZlnbkmGGIToMPHF1zhWjBUkd8aHjMOr/vFcPxw8= github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo= github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f h1:JOrtw2xFKzlg+cbHpyrpLDmnN1HqhBfnX7WDiW7eG2c= diff --git a/pkg/machine/applehv/stubber.go b/pkg/machine/applehv/stubber.go index be1d1179c2..3ebaf1eaa4 100644 --- a/pkg/machine/applehv/stubber.go +++ b/pkg/machine/applehv/stubber.go @@ -16,6 +16,7 @@ import ( "github.com/containers/podman/v5/pkg/machine/applehv/vfkit" "github.com/containers/podman/v5/pkg/machine/define" "github.com/containers/podman/v5/pkg/machine/ignition" + "github.com/containers/podman/v5/pkg/machine/shim/diskpull" "github.com/containers/podman/v5/pkg/machine/sockets" "github.com/containers/podman/v5/pkg/machine/vmconfigs" "github.com/containers/podman/v5/utils" @@ -38,6 +39,18 @@ type AppleHVStubber struct { vmconfigs.AppleHVConfig } +func (a AppleHVStubber) UserModeNetworkEnabled(_ *vmconfigs.MachineConfig) bool { +return true +} + +func (a AppleHVStubber) UseProviderNetworkSetup() bool { + return false +} + +func (a AppleHVStubber) RequireExclusiveActive() bool { + return true +} + func (a AppleHVStubber) CreateVM(opts define.CreateVMOpts, mc *vmconfigs.MachineConfig, ignBuilder *ignition.IgnitionBuilder) error { mc.AppleHypervisor = new(vmconfigs.AppleHVConfig) mc.AppleHypervisor.Vfkit = vfkit.VfkitHelper{} @@ -314,6 +327,10 @@ func (a AppleHVStubber) PrepareIgnition(_ *vmconfigs.MachineConfig, _ *ignition. return nil, nil } -func (a AppleHVStubber) PostStartNetworking(mc *vmconfigs.MachineConfig) error { +func (a AppleHVStubber) PostStartNetworking(mc *vmconfigs.MachineConfig, noInfo bool) error { return nil } + +func (a AppleHVStubber) GetDisk(userInputPath string, dirs *define.MachineDirs, mc *vmconfigs.MachineConfig) error { + return diskpull.GetDisk(userInputPath, dirs, mc.ImagePath, a.VMType(), mc.Name) +} diff --git a/pkg/machine/compression/decompress.go b/pkg/machine/compression/decompress.go index 14a6d526de..5ab0221b3a 100644 --- a/pkg/machine/compression/decompress.go +++ b/pkg/machine/compression/decompress.go @@ -32,8 +32,8 @@ func Decompress(localPath *define.VMFile, uncompressedPath string) error { return err } defer func() { - if err := uncompressedFileWriter.Close(); err != nil { - logrus.Errorf("unable to to close decompressed file %s: %q", uncompressedPath, err) + if err := uncompressedFileWriter.Close(); err != nil && !errors.Is(err, os.ErrClosed) { + logrus.Warnf("unable to close decompressed file %s: %q", uncompressedPath, err) } }() sourceFile, err := localPath.Read() diff --git a/pkg/machine/config.go b/pkg/machine/config.go index e21b3fca26..29663baaea 100644 --- a/pkg/machine/config.go +++ b/pkg/machine/config.go @@ -331,11 +331,55 @@ func NewVirtualization(artifact define.Artifact, compression compression.ImageCo } } +func dialSocket(socket string, timeout time.Duration) (net.Conn, error) { + scheme := "unix" + if strings.Contains(socket, "://") { + url, err := url.Parse(socket) + if err != nil { + return nil, err + } + scheme = url.Scheme + socket = url.Path + } + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + var dial func() (net.Conn, error) + switch scheme { + default: + fallthrough + case "unix": + dial = func() (net.Conn, error) { + var dialer net.Dialer + return dialer.DialContext(ctx, "unix", socket) + } + case "npipe": + dial = func() (net.Conn, error) { + return DialNamedPipe(ctx, socket) + } + } + + backoff := 500 * time.Millisecond + for { + conn, err := dial() + if !errors.Is(err, os.ErrNotExist) { + return conn, err + } + + select { + case <-time.After(backoff): + backoff *= 2 + case <-ctx.Done(): + return nil, ctx.Err() + } + } +} + func WaitAndPingAPI(sock string) { client := http.Client{ Transport: &http.Transport{ DialContext: func(context.Context, string, string) (net.Conn, error) { - con, err := net.DialTimeout("unix", sock, apiUpTimeout) + con, err := dialSocket(sock, apiUpTimeout) if err != nil { return nil, err } diff --git a/pkg/machine/define/config.go b/pkg/machine/define/config.go index c7217ac234..b03da89bdf 100644 --- a/pkg/machine/define/config.go +++ b/pkg/machine/define/config.go @@ -10,8 +10,10 @@ var ( ) type CreateVMOpts struct { - Name string - Dirs *MachineDirs + Name string + Dirs *MachineDirs + ReExec bool + UserModeNetworking bool } type MachineDirs struct { diff --git a/pkg/machine/e2e/machine_test.go b/pkg/machine/e2e/machine_test.go index e109532bac..866b23b878 100644 --- a/pkg/machine/e2e/machine_test.go +++ b/pkg/machine/e2e/machine_test.go @@ -12,6 +12,8 @@ import ( "testing" "time" + "github.com/containers/podman/v5/pkg/machine/wsl" + "github.com/containers/podman/v5/pkg/machine" "github.com/containers/podman/v5/pkg/machine/compression" "github.com/containers/podman/v5/pkg/machine/define" @@ -61,9 +63,20 @@ var _ = BeforeSuite(func() { downloadLocation := os.Getenv("MACHINE_IMAGE") if downloadLocation == "" { - downloadLocation, err = GetDownload(testProvider.VMType()) - if err != nil { - Fail("unable to derive download disk from fedora coreos") + // TODO so beautifully gross ... ideally we can spend some time + // here making life easier on the next person + switch testProvider.VMType() { + case define.WSLVirt: + dl, _, _, _, err := wsl.GetFedoraDownloadForWSL() + if err != nil { + Fail("unable to determine WSL download") + } + downloadLocation = dl.String() + default: + downloadLocation, err = GetDownload(testProvider.VMType()) + if err != nil { + Fail("unable to derive download disk from fedora coreos") + } } } diff --git a/pkg/machine/e2e/set_test.go b/pkg/machine/e2e/set_test.go index 6f49e82ef9..439539746b 100644 --- a/pkg/machine/e2e/set_test.go +++ b/pkg/machine/e2e/set_test.go @@ -171,6 +171,9 @@ var _ = Describe("podman machine set", func() { if testProvider.VMType() != define.WSLVirt { Skip("Test is only for WSL") } + // TODO - this currently fails + Skip("test fails bc usermode network needs plumbing for WSL") + name := randomString() i := new(initMachine) session, err := mb.setName(name).setCmd(i.withImagePath(mb.imagePath)).run() diff --git a/pkg/machine/gvproxy.go b/pkg/machine/gvproxy.go index 7f1ae58af4..6a008d22d4 100644 --- a/pkg/machine/gvproxy.go +++ b/pkg/machine/gvproxy.go @@ -1,16 +1,12 @@ package machine import ( - "errors" "fmt" - "runtime" "strconv" - "syscall" "time" "github.com/containers/podman/v5/pkg/machine/define" psutil "github.com/shirou/gopsutil/v3/process" - "github.com/sirupsen/logrus" ) const ( @@ -39,49 +35,6 @@ func backoffForProcess(p *psutil.Process) error { return fmt.Errorf("process %d has not ended", p.Pid) } -// waitOnProcess takes a pid and sends a sigterm to it. it then waits for the -// process to not exist. if the sigterm does not end the process after an interval, -// then sigkill is sent. it also waits for the process to exit after the sigkill too. -func waitOnProcess(processID int) error { - logrus.Infof("Going to stop gvproxy (PID %d)", processID) - - p, err := psutil.NewProcess(int32(processID)) - if err != nil { - return fmt.Errorf("looking up PID %d: %w", processID, err) - } - - // Try to kill the pid with sigterm - if runtime.GOOS != "windows" { // FIXME: temporary work around because signals are lame in windows - if err := p.SendSignal(syscall.SIGTERM); err != nil { - if errors.Is(err, syscall.ESRCH) { - return nil - } - return fmt.Errorf("sending SIGTERM to grproxy: %w", err) - } - - if err := backoffForProcess(p); err == nil { - return nil - } - } - - running, err := p.IsRunning() - if err != nil { - return fmt.Errorf("checking if gvproxy is running: %w", err) - } - if !running { - return nil - } - - if err := p.Kill(); err != nil { - if errors.Is(err, syscall.ESRCH) { - logrus.Debugf("Gvproxy already dead, exiting cleanly") - return nil - } - return err - } - return backoffForProcess(p) -} - // CleanupGVProxy reads the --pid-file for gvproxy attempts to stop it func CleanupGVProxy(f define.VMFile) error { gvPid, err := f.Read() diff --git a/pkg/machine/gvproxy_unix.go b/pkg/machine/gvproxy_unix.go new file mode 100644 index 0000000000..431e34740f --- /dev/null +++ b/pkg/machine/gvproxy_unix.go @@ -0,0 +1,41 @@ +//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd + +package machine + +import ( + "errors" + "fmt" + "syscall" + + psutil "github.com/shirou/gopsutil/v3/process" + "github.com/sirupsen/logrus" +) + +// / waitOnProcess takes a pid and sends a sigterm to it. it then waits for the +// process to not exist. if the sigterm does not end the process after an interval, +// then sigkill is sent. it also waits for the process to exit after the sigkill too. +func waitOnProcess(processID int) error { + logrus.Infof("Going to stop gvproxy (PID %d)", processID) + + p, err := psutil.NewProcess(int32(processID)) + if err != nil { + return fmt.Errorf("looking up PID %d: %w", processID, err) + } + + running, err := p.IsRunning() + if err != nil { + return fmt.Errorf("checking if gvproxy is running: %w", err) + } + if !running { + return nil + } + + if err := p.Kill(); err != nil { + if errors.Is(err, syscall.ESRCH) { + logrus.Debugf("Gvproxy already dead, exiting cleanly") + return nil + } + return err + } + return backoffForProcess(p) +} diff --git a/pkg/machine/gvproxy_windows.go b/pkg/machine/gvproxy_windows.go new file mode 100644 index 0000000000..75da8ad20d --- /dev/null +++ b/pkg/machine/gvproxy_windows.go @@ -0,0 +1,45 @@ +package machine + +import ( + "os" + "time" + + "github.com/containers/winquit/pkg/winquit" + "github.com/sirupsen/logrus" +) + +func waitOnProcess(processID int) error { + logrus.Infof("Going to stop gvproxy (PID %d)", processID) + + p, err := os.FindProcess(processID) + if err != nil { + // FindProcess on Windows will return an error when the process is not found + // if a process can not be found then it has already exited and there is + // nothing left to do, so return without error + return nil + } + + // Gracefully quit and force kill after 30 seconds + if err := winquit.QuitProcess(processID, 30*time.Second); err != nil { + return err + } + + logrus.Debugf("completed grace quit || kill of gvproxy (PID %d)", processID) + + // Make sure the process is gone (Hard kills are async) + done := make(chan struct{}) + go func() { + _, _ = p.Wait() + done <- struct{}{} + }() + + select { + case <-done: + logrus.Debugf("verified gvproxy termination (PID %d)", processID) + case <-time.After(10 * time.Second): + // Very unlikely but track just in case + logrus.Errorf("was not able to kill gvproxy (PID %d)", processID) + } + + return nil +} diff --git a/pkg/machine/hyperv/stubber.go b/pkg/machine/hyperv/stubber.go index 21d436eb7c..080d78c039 100644 --- a/pkg/machine/hyperv/stubber.go +++ b/pkg/machine/hyperv/stubber.go @@ -10,6 +10,8 @@ import ( "os/exec" "path/filepath" + "github.com/containers/podman/v5/pkg/machine/shim/diskpull" + "github.com/Microsoft/go-winio" "github.com/containers/common/pkg/strongunits" gvproxy "github.com/containers/gvisor-tap-vsock/pkg/types" @@ -27,6 +29,18 @@ type HyperVStubber struct { vmconfigs.HyperVConfig } +func (h HyperVStubber) UserModeNetworkEnabled(mc *vmconfigs.MachineConfig) bool { + return true +} + +func (h HyperVStubber) UseProviderNetworkSetup() bool { + return false +} + +func (h HyperVStubber) RequireExclusiveActive() bool { + return true +} + func (h HyperVStubber) CreateVM(opts define.CreateVMOpts, mc *vmconfigs.MachineConfig, builder *ignition.IgnitionBuilder) error { var ( err error @@ -368,7 +382,7 @@ func (h HyperVStubber) PrepareIgnition(mc *vmconfigs.MachineConfig, ignBuilder * return &ignOpts, nil } -func (h HyperVStubber) PostStartNetworking(mc *vmconfigs.MachineConfig) error { +func (h HyperVStubber) PostStartNetworking(mc *vmconfigs.MachineConfig, noInfo bool) error { var ( err error executable string @@ -377,25 +391,6 @@ func (h HyperVStubber) PostStartNetworking(mc *vmconfigs.MachineConfig) error { defer callbackFuncs.CleanIfErr(&err) go callbackFuncs.CleanOnSignal() - winProxyOpts := machine.WinProxyOpts{ - Name: mc.Name, - IdentityPath: mc.SSH.IdentityPath, - Port: mc.SSH.Port, - RemoteUsername: mc.SSH.RemoteUsername, - Rootful: mc.HostUser.Rootful, - VMType: h.VMType(), - } - // TODO Should this process be fatal on error; currenty, no error is - // returned but an error can occur in the func itself - // TODO we do not currently pass "noinfo" (quiet) into the StartVM - // func so this is hard set to false - machine.LaunchWinProxy(winProxyOpts, false) - - winProxyCallbackFunc := func() error { - return machine.StopWinProxy(mc.Name, h.VMType()) - } - callbackFuncs.Add(winProxyCallbackFunc) - if len(mc.Mounts) != 0 { var ( dirs *define.MachineDirs @@ -459,6 +454,10 @@ func (h HyperVStubber) PostStartNetworking(mc *vmconfigs.MachineConfig) error { return err } +func (h HyperVStubber) GetDisk(userInputPath string, dirs *define.MachineDirs, mc *vmconfigs.MachineConfig) error { + return diskpull.GetDisk(userInputPath, dirs, mc.ImagePath, h.VMType(), mc.Name) +} + func resizeDisk(newSize strongunits.GiB, imagePath *define.VMFile) error { resize := exec.Command("powershell", []string{"-command", fmt.Sprintf("Resize-VHD %s %d", imagePath.GetPath(), newSize.ToBytes())}...) logrus.Debug(resize.Args) diff --git a/pkg/machine/machine_common.go b/pkg/machine/machine_common.go index 70116118a9..a8eb8be396 100644 --- a/pkg/machine/machine_common.go +++ b/pkg/machine/machine_common.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "os" + "strings" "github.com/containers/podman/v5/pkg/machine/connection" "github.com/containers/storage/pkg/ioutils" @@ -46,19 +47,6 @@ func WaitAPIAndPrintInfo(forwardState APIForwardingState, name, helper, forwardS WaitAndPingAPI(forwardSock) if !noInfo { - if !rootful { - fmtString = ` -This machine is currently configured in rootless mode. If your containers -require root permissions (e.g. ports < 1024), or if you run into compatibility -issues with non-podman clients, you can switch using the following command: - - podman machine set --rootful%s - -` - - fmt.Printf(fmtString, suffix) - } - fmt.Printf("API forwarding listening on: %s\n", forwardSock) if forwardState == DockerGlobal { fmt.Printf("Docker API clients default to this address. You do not need to set DOCKER_HOST.\n\n") @@ -79,7 +67,7 @@ address can't be used by podman. ` sudo %s install podman machine stop%[2]s; podman machine start%[2]s - ` +` fmt.Printf(fmtString, helper, suffix) } case MachineLocal: @@ -93,15 +81,35 @@ address can't be used by podman. ` fmtString = `You can %sconnect Docker API clients by setting DOCKER_HOST using the following command in your terminal session: - export DOCKER_HOST='unix://%s' + %s' ` - - fmt.Printf(fmtString, stillString, forwardSock) + prefix := "" + if !strings.Contains(forwardSock, "://") { + prefix = "unix://" + } + fmt.Printf(fmtString, stillString, GetEnvSetString("DOCKER_HOST", prefix+forwardSock)) } } } +func PrintRootlessWarning(name string) { + suffix := "" + if name != DefaultMachineName { + suffix = " " + name + } + + fmtString := ` +This machine is currently configured in rootless mode. If your containers +require root permissions (e.g. ports < 1024), or if you run into compatibility +issues with non-podman clients, you can switch using the following command: + + podman machine set --rootful%s + +` + fmt.Printf(fmtString, suffix) +} + // SetRootful modifies the machine's default connection to be either rootful or // rootless func SetRootful(rootful bool, name, rootfulName string) error { diff --git a/pkg/machine/machine_unix.go b/pkg/machine/machine_unix.go index 66c92ed4e8..10bcf5be8c 100644 --- a/pkg/machine/machine_unix.go +++ b/pkg/machine/machine_unix.go @@ -3,7 +3,10 @@ package machine import ( + "context" "errors" + "fmt" + "net" "strings" ) @@ -32,3 +35,11 @@ func ParseVolumeFromPath(v string) (source, target, options string, readonly boo } return } + +func DialNamedPipe(ctx context.Context, path string) (net.Conn, error) { + return nil, errors.New("not implemented") +} + +func GetEnvSetString(env string, val string) string { + return fmt.Sprintf("export %s='%s'", env, val) +} diff --git a/pkg/machine/machine_windows.go b/pkg/machine/machine_windows.go index 68ff8785ff..3e19f4d98e 100644 --- a/pkg/machine/machine_windows.go +++ b/pkg/machine/machine_windows.go @@ -3,7 +3,11 @@ package machine import ( + "context" + "errors" "fmt" + "io/fs" + "net" "os" "os/exec" "path/filepath" @@ -11,17 +15,22 @@ import ( "syscall" "time" + winio "github.com/Microsoft/go-winio" "github.com/containers/podman/v5/pkg/machine/define" "github.com/sirupsen/logrus" ) const ( - pipePrefix = "npipe:////./pipe/" - globalPipe = "docker_engine" - winSShProxy = "win-sshproxy.exe" - winSshProxyTid = "win-sshproxy.tid" - rootfulSock = "/run/podman/podman.sock" - rootlessSock = "/run/user/1000/podman/podman.sock" + NamedPipePrefix = "npipe:////./pipe/" + GlobalNamedPipe = "docker_engine" + winSShProxy = "win-sshproxy.exe" + winSshProxyTid = "win-sshproxy.tid" + rootfulSock = "/run/podman/podman.sock" + rootlessSock = "/run/user/1000/podman/podman.sock" + + // machine wait is longer since we must hard fail + MachineNameWait = 5 * time.Second + GlobalNameWait = 250 * time.Millisecond ) const WM_QUIT = 0x12 //nolint @@ -50,9 +59,20 @@ func GetProcessState(pid int) (active bool, exitCode int) { return code == 259, int(code) } -func PipeNameAvailable(pipeName string) bool { - _, err := os.Stat(`\\.\pipe\` + pipeName) - return os.IsNotExist(err) +func PipeNameAvailable(pipeName string, maxWait time.Duration) bool { + const interval = 250 * time.Millisecond + var wait time.Duration + for { + _, err := os.Stat(`\\.\pipe\` + pipeName) + if errors.Is(err, fs.ErrNotExist) { + return true + } + if wait >= maxWait { + return false + } + time.Sleep(interval) + wait += interval + } } func WaitPipeExists(pipeName string, retries int, checkFailure func() error) error { @@ -71,6 +91,11 @@ func WaitPipeExists(pipeName string, retries int, checkFailure func() error) err return err } +func DialNamedPipe(ctx context.Context, path string) (net.Conn, error) { + path = strings.Replace(path, "/", "\\", -1) + return winio.DialPipeContext(ctx, path) +} + func LaunchWinProxy(opts WinProxyOpts, noInfo bool) { globalName, pipeName, err := launchWinProxy(opts) if !noInfo { @@ -97,12 +122,12 @@ func LaunchWinProxy(opts WinProxyOpts, noInfo bool) { func launchWinProxy(opts WinProxyOpts) (bool, string, error) { machinePipe := ToDist(opts.Name) - if !PipeNameAvailable(machinePipe) { + if !PipeNameAvailable(machinePipe, MachineNameWait) { return false, "", fmt.Errorf("could not start api proxy since expected pipe is not available: %s", machinePipe) } globalName := false - if PipeNameAvailable(globalPipe) { + if PipeNameAvailable(GlobalNamedPipe, GlobalNameWait) { globalName = true } @@ -125,11 +150,11 @@ func launchWinProxy(opts WinProxyOpts) (bool, string, error) { } dest := fmt.Sprintf("ssh://%s@localhost:%d%s", forwardUser, opts.Port, destSock) - args := []string{opts.Name, stateDir, pipePrefix + machinePipe, dest, opts.IdentityPath} + args := []string{opts.Name, stateDir, NamedPipePrefix + machinePipe, dest, opts.IdentityPath} waitPipe := machinePipe if globalName { - args = append(args, pipePrefix+globalPipe, dest, opts.IdentityPath) - waitPipe = globalPipe + args = append(args, NamedPipePrefix+GlobalNamedPipe, dest, opts.IdentityPath) + waitPipe = GlobalNamedPipe } cmd := exec.Command(command, args...) @@ -138,7 +163,7 @@ func launchWinProxy(opts WinProxyOpts) (bool, string, error) { return globalName, "", err } - return globalName, pipePrefix + waitPipe, WaitPipeExists(waitPipe, 80, func() error { + return globalName, NamedPipePrefix + waitPipe, WaitPipeExists(waitPipe, 80, func() error { active, exitCode := GetProcessState(cmd.Process.Pid) if !active { return fmt.Errorf("win-sshproxy.exe failed to start, exit code: %d (see windows event logs)", exitCode) @@ -240,3 +265,7 @@ func ToDist(name string) string { } return name } + +func GetEnvSetString(env string, val string) string { + return fmt.Sprintf("$Env:%s=\"%s\"", env, val) +} diff --git a/pkg/machine/provider/platform_windows.go b/pkg/machine/provider/platform_windows.go index d7caf791d0..943a66926c 100644 --- a/pkg/machine/provider/platform_windows.go +++ b/pkg/machine/provider/platform_windows.go @@ -2,9 +2,11 @@ package provider import ( "fmt" - "github.com/containers/podman/v5/pkg/machine/vmconfigs" "os" + "github.com/containers/podman/v5/pkg/machine/vmconfigs" + "github.com/containers/podman/v5/pkg/machine/wsl" + "github.com/containers/common/pkg/config" "github.com/containers/podman/v5/pkg/machine/define" "github.com/containers/podman/v5/pkg/machine/hyperv" @@ -27,9 +29,8 @@ func Get() (vmconfigs.VMProvider, error) { logrus.Debugf("Using Podman machine with `%s` virtualization provider", resolvedVMType.String()) switch resolvedVMType { - // TODO re-enable this with WSL - //case define.WSLVirt: - // return wsl.VirtualizationProvider(), nil + case define.WSLVirt: + return new(wsl.WSLStubber), nil case define.HyperVVirt: return new(hyperv.HyperVStubber), nil default: diff --git a/pkg/machine/qemu/machine.go b/pkg/machine/qemu/machine.go index 1bcb46ce46..eb610f890d 100644 --- a/pkg/machine/qemu/machine.go +++ b/pkg/machine/qemu/machine.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "io/fs" "os" "os/exec" @@ -266,6 +267,22 @@ func (q *QEMUStubber) State(mc *vmconfigs.MachineConfig, bypass bool) (define.St return "", err } if err := monitor.Connect(); err != nil { + // There is a case where if we stop the same vm (from running) two + // consecutive times we can get an econnreset when trying to get the + // state + if errors.Is(err, syscall.ECONNRESET) { + // try again + logrus.Debug("received ECCONNRESET from QEMU monitor; trying again") + secondTry := monitor.Connect() + if errors.Is(secondTry, io.EOF) { + return define.Stopped, nil + } + if secondTry != nil { + logrus.Debugf("second attempt to connect to QEMU monitor failed") + return "", secondTry + } + } + return "", err } defer func() { diff --git a/pkg/machine/qemu/stubber.go b/pkg/machine/qemu/stubber.go index ed5934ed0e..28614246bb 100644 --- a/pkg/machine/qemu/stubber.go +++ b/pkg/machine/qemu/stubber.go @@ -11,14 +11,14 @@ import ( "strings" "time" - "github.com/containers/podman/v5/pkg/machine/ignition" - "github.com/containers/common/pkg/config" "github.com/containers/common/pkg/strongunits" gvproxy "github.com/containers/gvisor-tap-vsock/pkg/types" "github.com/containers/podman/v5/pkg/machine" "github.com/containers/podman/v5/pkg/machine/define" + "github.com/containers/podman/v5/pkg/machine/ignition" "github.com/containers/podman/v5/pkg/machine/qemu/command" + "github.com/containers/podman/v5/pkg/machine/shim/diskpull" "github.com/containers/podman/v5/pkg/machine/sockets" "github.com/containers/podman/v5/pkg/machine/vmconfigs" "github.com/sirupsen/logrus" @@ -30,6 +30,18 @@ type QEMUStubber struct { Command command.QemuCmd } +func (q QEMUStubber) UserModeNetworkEnabled(*vmconfigs.MachineConfig) bool { + return true +} + +func (q QEMUStubber) UseProviderNetworkSetup() bool { + return false +} + +func (q QEMUStubber) RequireExclusiveActive() bool { + return true +} + func (q *QEMUStubber) setQEMUCommandLine(mc *vmconfigs.MachineConfig) error { qemuBinary, err := findQEMUBinary() if err != nil { @@ -69,7 +81,7 @@ func (q *QEMUStubber) setQEMUCommandLine(mc *vmconfigs.MachineConfig) error { return nil } -func (q *QEMUStubber) CreateVM(opts define.CreateVMOpts, mc *vmconfigs.MachineConfig, _ *ignition.IgnitionBuilder) error { +func (q *QEMUStubber) CreateVM(opts define.CreateVMOpts, mc *vmconfigs.MachineConfig, builder *ignition.IgnitionBuilder) error { monitor, err := command.NewQMPMonitor(opts.Name, opts.Dirs.RuntimeDir) if err != nil { return err @@ -323,6 +335,10 @@ func (q *QEMUStubber) MountType() vmconfigs.VolumeMountType { return vmconfigs.NineP } -func (q *QEMUStubber) PostStartNetworking(mc *vmconfigs.MachineConfig) error { +func (q *QEMUStubber) PostStartNetworking(mc *vmconfigs.MachineConfig, noInfo bool) error { return nil } + +func (q *QEMUStubber) GetDisk(userInputPath string, dirs *define.MachineDirs, mc *vmconfigs.MachineConfig) error { + return diskpull.GetDisk(userInputPath, dirs, mc.ImagePath, q.VMType(), mc.Name) +} diff --git a/pkg/machine/shim/diskpull/diskpull.go b/pkg/machine/shim/diskpull/diskpull.go new file mode 100644 index 0000000000..302c40d155 --- /dev/null +++ b/pkg/machine/shim/diskpull/diskpull.go @@ -0,0 +1,32 @@ +package diskpull + +import ( + "context" + "strings" + + "github.com/containers/podman/v5/pkg/machine/define" + "github.com/containers/podman/v5/pkg/machine/ocipull" + "github.com/containers/podman/v5/pkg/machine/stdpull" +) + +func GetDisk(userInputPath string, dirs *define.MachineDirs, imagePath *define.VMFile, vmType define.VMType, name string) error { + var ( + err error + mydisk ocipull.Disker + ) + + if userInputPath == "" { + mydisk, err = ocipull.NewVersioned(context.Background(), dirs.DataDir, name, vmType.String(), imagePath) + } else { + if strings.HasPrefix(userInputPath, "http") { + // TODO probably should use tempdir instead of datadir + mydisk, err = stdpull.NewDiskFromURL(userInputPath, imagePath, dirs.DataDir, nil) + } else { + mydisk, err = stdpull.NewStdDiskPull(userInputPath, imagePath) + } + } + if err != nil { + return err + } + return mydisk.Get() +} diff --git a/pkg/machine/shim/host.go b/pkg/machine/shim/host.go index ece54f4dd5..9bf7bf0623 100644 --- a/pkg/machine/shim/host.go +++ b/pkg/machine/shim/host.go @@ -1,12 +1,10 @@ package shim import ( - "context" "errors" "fmt" "os" "runtime" - "strings" "time" "github.com/containers/common/pkg/util" @@ -14,30 +12,13 @@ import ( "github.com/containers/podman/v5/pkg/machine/connection" machineDefine "github.com/containers/podman/v5/pkg/machine/define" "github.com/containers/podman/v5/pkg/machine/ignition" - "github.com/containers/podman/v5/pkg/machine/ocipull" - "github.com/containers/podman/v5/pkg/machine/stdpull" "github.com/containers/podman/v5/pkg/machine/vmconfigs" "github.com/sirupsen/logrus" ) -/* -Host - ├ Info - ├ OS Apply - ├ SSH - ├ List - ├ Init - ├ VMExists - ├ CheckExclusiveActiveVM *HyperV/WSL need to check their hypervisors as well -*/ - -func Info() {} -func OSApply() {} -func SSH() {} - // List is done at the host level to allow for a *possible* future where // more than one provider is used -func List(vmstubbers []vmconfigs.VMProvider, opts machine.ListOptions) ([]*machine.ListResponse, error) { +func List(vmstubbers []vmconfigs.VMProvider, _ machine.ListOptions) ([]*machine.ListResponse, error) { var ( lrs []*machine.ListResponse ) @@ -114,6 +95,10 @@ func Init(opts machineDefine.InitOptions, mp vmconfigs.VMProvider) (*vmconfigs.M Dirs: dirs, } + if umn := opts.UserModeNetworking; umn != nil { + createOpts.UserModeNetworking = *umn + } + // Get Image // TODO This needs rework bigtime; my preference is most of below of not living in here. // ideally we could get a func back that pulls the image, and only do so IF everything works because @@ -137,8 +122,7 @@ func Init(opts machineDefine.InitOptions, mp vmconfigs.VMProvider) (*vmconfigs.M if err != nil { return nil, err } - - var mydisk ocipull.Disker + mc.ImagePath = imagePath // TODO The following stanzas should be re-written in a differeent place. It should have a custom // parser for our image pulling. It would be nice if init just got an error and mydisk back. @@ -149,25 +133,12 @@ func Init(opts machineDefine.InitOptions, mp vmconfigs.VMProvider) (*vmconfigs.M // "/path // "docker://quay.io/something/someManifest - if opts.ImagePath == "" { - mydisk, err = ocipull.NewVersioned(context.Background(), dirs.DataDir, opts.Name, mp.VMType().String(), imagePath) - } else { - if strings.HasPrefix(opts.ImagePath, "http") { - // TODO probably should use tempdir instead of datadir - mydisk, err = stdpull.NewDiskFromURL(opts.ImagePath, imagePath, dirs.DataDir) - } else { - mydisk, err = stdpull.NewStdDiskPull(opts.ImagePath, imagePath) - } - } - if err != nil { - return nil, err - } - err = mydisk.Get() + // TODO Ideally this changes into some way better ... + err = mp.GetDisk(opts.ImagePath, dirs, mc) if err != nil { return nil, err } - mc.ImagePath = imagePath callbackFuncs.Add(mc.ImagePath.Delete) logrus.Debugf("--> imagePath is %q", imagePath.GetPath()) @@ -182,8 +153,18 @@ func Init(opts machineDefine.InitOptions, mp vmconfigs.VMProvider) (*vmconfigs.M uid = 1000 } + // TODO the definition of "user" should go into + // common for WSL + userName := opts.Username + if mp.VMType() == machineDefine.WSLVirt { + if opts.Username == "core" { + userName = "user" + mc.SSH.RemoteUsername = "user" + } + } + ignBuilder := ignition.NewIgnitionBuilder(ignition.DynamicIgnition{ - Name: opts.Username, + Name: userName, Key: sshKey, TimeZone: opts.TimeZone, UID: uid, @@ -223,7 +204,9 @@ func Init(opts machineDefine.InitOptions, mp vmconfigs.VMProvider) (*vmconfigs.M ignBuilder.WithUnit(readyUnit) // Mounts - mc.Mounts = CmdLineVolumesToMounts(opts.Volumes, mp.MountType()) + if mp.VMType() != machineDefine.WSLVirt { + mc.Mounts = CmdLineVolumesToMounts(opts.Volumes, mp.MountType()) + } // TODO AddSSHConnectionToPodmanSocket could take an machineconfig instead if err := connection.AddSSHConnectionsToPodmanSocket(mc.HostUser.UID, mc.SSH.Port, mc.SSH.IdentityPath, mc.Name, mc.SSH.RemoteUsername, opts); err != nil { @@ -273,6 +256,11 @@ func VMExists(name string, vmstubbers []vmconfigs.VMProvider) (*vmconfigs.Machin // CheckExclusiveActiveVM checks if any of the machines are already running func CheckExclusiveActiveVM(provider vmconfigs.VMProvider, mc *vmconfigs.MachineConfig) error { + // Don't check if provider supports parallel running machines + if !provider.RequireExclusiveActive() { + return nil + } + // Check if any other machines are running; if so, we error localMachines, err := getMCsOverProviders([]vmconfigs.VMProvider{provider}) if err != nil { @@ -347,21 +335,23 @@ func Stop(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, dirs *machineDef } // Stop GvProxy and remove PID file - gvproxyPidFile, err := dirs.RuntimeDir.AppendToNewVMFile("gvproxy.pid", nil) - if err != nil { - return err - } - - defer func() { - if err := machine.CleanupGVProxy(*gvproxyPidFile); err != nil { - logrus.Errorf("unable to clean up gvproxy: %q", err) + if !mp.UseProviderNetworkSetup() { + gvproxyPidFile, err := dirs.RuntimeDir.AppendToNewVMFile("gvproxy.pid", nil) + if err != nil { + return err } - }() + + defer func() { + if err := machine.CleanupGVProxy(*gvproxyPidFile); err != nil { + logrus.Errorf("unable to clean up gvproxy: %q", err) + } + }() + } return nil } -func Start(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, dirs *machineDefine.MachineDirs, opts machine.StartOptions) error { +func Start(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, _ *machineDefine.MachineDirs, opts machine.StartOptions) error { defaultBackoff := 500 * time.Millisecond maxBackoffs := 6 @@ -396,7 +386,11 @@ func Start(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, dirs *machineDe } } - err = mp.PostStartNetworking(mc) + if !opts.NoInfo && !mc.HostUser.Rootful { + machine.PrintRootlessWarning(mc.Name) + } + + err = mp.PostStartNetworking(mc, opts.NoInfo) if err != nil { return err } @@ -423,15 +417,6 @@ func Start(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, dirs *machineDe return err } - machine.WaitAPIAndPrintInfo( - forwardingState, - mc.Name, - findClaimHelper(), - forwardSocketPath, - opts.NoInfo, - mc.HostUser.Rootful, - ) - // update the podman/docker socket service if the host user has been modified at all (UID or Rootful) if mc.HostUser.Modified { if machine.UpdatePodmanDockerSockService(mc) == nil { @@ -443,5 +428,22 @@ func Start(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, dirs *machineDe } } } + + // Provider is responsible for waiting + if mp.UseProviderNetworkSetup() { + return nil + } + + noInfo := opts.NoInfo + + machine.WaitAPIAndPrintInfo( + forwardingState, + mc.Name, + findClaimHelper(), + forwardSocketPath, + noInfo, + mc.HostUser.Rootful, + ) + return nil } diff --git a/pkg/machine/shim/networking.go b/pkg/machine/shim/networking.go index 28dc2db01b..57ad53b7eb 100644 --- a/pkg/machine/shim/networking.go +++ b/pkg/machine/shim/networking.go @@ -1,10 +1,9 @@ package shim import ( + "errors" "fmt" - "io/fs" "net" - "os" "path/filepath" "strings" "time" @@ -23,16 +22,17 @@ const ( dockerConnectTimeout = 5 * time.Second ) -func startNetworking(mc *vmconfigs.MachineConfig, provider vmconfigs.VMProvider) (string, machine.APIForwardingState, error) { - var ( - forwardingState machine.APIForwardingState - forwardSock string - ) - // the guestSock is "inside" the guest machine - guestSock := fmt.Sprintf(defaultGuestSock, mc.HostUser.UID) +var ( + ErrNotRunning = errors.New("machine not in running state") + ErrSSHNotListening = errors.New("machine is not listening on ssh port") +) + +func startHostForwarder(mc *vmconfigs.MachineConfig, provider vmconfigs.VMProvider, dirs *define.MachineDirs, hostSocks []string) error { forwardUser := mc.SSH.RemoteUsername - // TODO should this go up the stack higher + // TODO should this go up the stack higher or + // the guestSock is "inside" the guest machine + guestSock := fmt.Sprintf(defaultGuestSock, mc.HostUser.UID) if mc.HostUser.Rootful { guestSock = "/run/podman/podman.sock" forwardUser = "root" @@ -40,38 +40,18 @@ func startNetworking(mc *vmconfigs.MachineConfig, provider vmconfigs.VMProvider) cfg, err := config.Default() if err != nil { - return "", 0, err + return err } binary, err := cfg.FindHelperBinary(machine.ForwarderBinaryName, false) if err != nil { - return "", 0, err - } - - dataDir, err := mc.DataDir() - if err != nil { - return "", 0, err - } - hostSocket, err := dataDir.AppendToNewVMFile("podman.sock", nil) - if err != nil { - return "", 0, err - } - - runDir, err := mc.RuntimeDir() - if err != nil { - return "", 0, err - } - - linkSocketPath := filepath.Dir(dataDir.GetPath()) - linkSocket, err := define.NewMachineFile(filepath.Join(linkSocketPath, "podman.sock"), nil) - if err != nil { - return "", 0, err + return err } cmd := gvproxy.NewGvproxyCommand() // GvProxy PID file path is now derived - cmd.PidFile = filepath.Join(runDir.GetPath(), "gvproxy.pid") + cmd.PidFile = filepath.Join(dirs.RuntimeDir.GetPath(), "gvproxy.pid") // TODO This can be re-enabled when gvisor-tap-vsock #305 is merged // debug is set, we dump to a logfile as well @@ -81,10 +61,13 @@ func startNetworking(mc *vmconfigs.MachineConfig, provider vmconfigs.VMProvider) cmd.SSHPort = mc.SSH.Port - cmd.AddForwardSock(hostSocket.GetPath()) - cmd.AddForwardDest(guestSock) - cmd.AddForwardUser(forwardUser) - cmd.AddForwardIdentity(mc.SSH.IdentityPath) + // Windows providers listen on multiple sockets since they do not involve links + for _, hostSock := range hostSocks { + cmd.AddForwardSock(hostSock) + cmd.AddForwardDest(guestSock) + cmd.AddForwardUser(forwardUser) + cmd.AddForwardIdentity(mc.SSH.IdentityPath) + } if logrus.IsLevelEnabled(logrus.DebugLevel) { cmd.Debug = true @@ -94,80 +77,40 @@ func startNetworking(mc *vmconfigs.MachineConfig, provider vmconfigs.VMProvider) // This allows a provider to perform additional setup as well as // add in any provider specific options for gvproxy if err := provider.StartNetworking(mc, &cmd); err != nil { - return "", 0, err - } - - if mc.HostUser.UID != -1 { - forwardSock, forwardingState = setupAPIForwarding(hostSocket, linkSocket) + return err } c := cmd.Cmd(binary) logrus.Debugf("gvproxy command-line: %s %s", binary, strings.Join(cmd.ToCmdline(), " ")) if err := c.Start(); err != nil { - return forwardSock, 0, fmt.Errorf("unable to execute: %q: %w", cmd.ToCmdline(), err) + return fmt.Errorf("unable to execute: %q: %w", cmd.ToCmdline(), err) } - return forwardSock, forwardingState, nil -} - -type apiOptions struct { //nolint:unused - socketpath, destinationSocketPath *define.VMFile - fowardUser string + return nil } -func setupAPIForwarding(hostSocket, linkSocket *define.VMFile) (string, machine.APIForwardingState) { - // The linking pattern is /var/run/docker.sock -> user global sock (link) -> machine sock (socket) - // This allows the helper to only have to maintain one constant target to the user, which can be - // repositioned without updating docker.sock. - - if !dockerClaimSupported() { - return hostSocket.GetPath(), machine.ClaimUnsupported - } - - if !dockerClaimHelperInstalled() { - return hostSocket.GetPath(), machine.NotInstalled +func startNetworking(mc *vmconfigs.MachineConfig, provider vmconfigs.VMProvider) (string, machine.APIForwardingState, error) { + // Provider has its own networking code path (e.g. WSL) + if provider.UseProviderNetworkSetup() { + return "", 0, provider.StartNetworking(mc, nil) } - if !alreadyLinked(hostSocket.GetPath(), linkSocket.GetPath()) { - if checkSockInUse(linkSocket.GetPath()) { - return hostSocket.GetPath(), machine.MachineLocal - } - - _ = linkSocket.Delete() - - if err := os.Symlink(hostSocket.GetPath(), linkSocket.GetPath()); err != nil { - logrus.Warnf("could not create user global API forwarding link: %s", err.Error()) - return hostSocket.GetPath(), machine.MachineLocal - } + dirs, err := machine.GetMachineDirs(provider.VMType()) + if err != nil { + return "", 0, err } - if !alreadyLinked(linkSocket.GetPath(), dockerSock) { - if checkSockInUse(dockerSock) { - return hostSocket.GetPath(), machine.MachineLocal - } - - if !claimDockerSock() { - logrus.Warn("podman helper is installed, but was not able to claim the global docker sock") - return hostSocket.GetPath(), machine.MachineLocal - } + hostSocks, forwardSock, forwardingState, err := setupMachineSockets(mc.Name, dirs) + if err != nil { + return "", 0, err } - return dockerSock, machine.DockerGlobal -} - -func alreadyLinked(target string, link string) bool { - read, err := os.Readlink(link) - return err == nil && read == target -} - -func checkSockInUse(sock string) bool { - if info, err := os.Stat(sock); err == nil && info.Mode()&fs.ModeSocket == fs.ModeSocket { - _, err = net.DialTimeout("unix", dockerSock, dockerConnectTimeout) - return err == nil + if err := startHostForwarder(mc, provider, dirs, hostSocks); err != nil { + return "", 0, err } - return false + return forwardSock, forwardingState, nil } // conductVMReadinessCheck checks to make sure the machine is in the proper state @@ -182,22 +125,30 @@ func conductVMReadinessCheck(mc *vmconfigs.MachineConfig, maxBackoffs int, backo if err != nil { return false, nil, err } - if state == define.Running && isListening(mc.SSH.Port) { - // Also make sure that SSH is up and running. The - // ready service's dependencies don't fully make sure - // that clients can SSH into the machine immediately - // after boot. - // - // CoreOS users have reported the same observation but - // the underlying source of the issue remains unknown. - - if sshError = machine.CommonSSH(mc.SSH.RemoteUsername, mc.SSH.IdentityPath, mc.Name, mc.SSH.Port, []string{"true"}); sshError != nil { - logrus.Debugf("SSH readiness check for machine failed: %v", sshError) - continue - } - connected = true - break + if state != define.Running { + sshError = ErrNotRunning + continue + } + if !isListening(mc.SSH.Port) { + sshError = ErrSSHNotListening + continue + } + + // Also make sure that SSH is up and running. The + // ready service's dependencies don't fully make sure + // that clients can SSH into the machine immediately + // after boot. + // + // CoreOS users have reported the same observation but + // the underlying source of the issue remains unknown. + + if sshError = machine.CommonSSHSilent(mc.SSH.RemoteUsername, mc.SSH.IdentityPath, mc.Name, mc.SSH.Port, []string{"true"}); sshError != nil { + logrus.Debugf("SSH readiness check for machine failed: %v", sshError) + continue } + connected = true + sshError = nil + break } return } diff --git a/pkg/machine/shim/networking_unix.go b/pkg/machine/shim/networking_unix.go new file mode 100644 index 0000000000..869f99d5ed --- /dev/null +++ b/pkg/machine/shim/networking_unix.go @@ -0,0 +1,84 @@ +//go:build dragonfly || freebsd || linux || netbsd || openbsd || darwin + +package shim + +import ( + "io/fs" + "net" + "os" + "path/filepath" + + "github.com/containers/podman/v5/pkg/machine" + "github.com/containers/podman/v5/pkg/machine/define" + "github.com/sirupsen/logrus" +) + +func setupMachineSockets(name string, dirs *define.MachineDirs) ([]string, string, machine.APIForwardingState, error) { + hostSocket, err := dirs.DataDir.AppendToNewVMFile("podman.sock", nil) + if err != nil { + return nil, "", 0, err + } + + linkSocketPath := filepath.Dir(dirs.DataDir.GetPath()) + linkSocket, err := define.NewMachineFile(filepath.Join(linkSocketPath, "podman.sock"), nil) + if err != nil { + return nil, "", 0, err + } + + forwardSock, state := setupForwardingLinks(hostSocket, linkSocket) + return []string{hostSocket.GetPath()}, forwardSock, state, nil +} + +func setupForwardingLinks(hostSocket, linkSocket *define.VMFile) (string, machine.APIForwardingState) { + // The linking pattern is /var/run/docker.sock -> user global sock (link) -> machine sock (socket) + // This allows the helper to only have to maintain one constant target to the user, which can be + // repositioned without updating docker.sock. + + if !dockerClaimSupported() { + return hostSocket.GetPath(), machine.ClaimUnsupported + } + + if !dockerClaimHelperInstalled() { + return hostSocket.GetPath(), machine.NotInstalled + } + + if !alreadyLinked(hostSocket.GetPath(), linkSocket.GetPath()) { + if checkSockInUse(linkSocket.GetPath()) { + return hostSocket.GetPath(), machine.MachineLocal + } + + _ = linkSocket.Delete() + + if err := os.Symlink(hostSocket.GetPath(), linkSocket.GetPath()); err != nil { + logrus.Warnf("could not create user global API forwarding link: %s", err.Error()) + return hostSocket.GetPath(), machine.MachineLocal + } + } + + if !alreadyLinked(linkSocket.GetPath(), dockerSock) { + if checkSockInUse(dockerSock) { + return hostSocket.GetPath(), machine.MachineLocal + } + + if !claimDockerSock() { + logrus.Warn("podman helper is installed, but was not able to claim the global docker sock") + return hostSocket.GetPath(), machine.MachineLocal + } + } + + return dockerSock, machine.DockerGlobal +} + +func alreadyLinked(target string, link string) bool { + read, err := os.Readlink(link) + return err == nil && read == target +} + +func checkSockInUse(sock string) bool { + if info, err := os.Stat(sock); err == nil && info.Mode()&fs.ModeSocket == fs.ModeSocket { + _, err = net.DialTimeout("unix", dockerSock, dockerConnectTimeout) + return err == nil + } + + return false +} diff --git a/pkg/machine/shim/networking_windows.go b/pkg/machine/shim/networking_windows.go new file mode 100644 index 0000000000..970aefd5aa --- /dev/null +++ b/pkg/machine/shim/networking_windows.go @@ -0,0 +1,24 @@ +package shim + +import ( + "fmt" + + "github.com/containers/podman/v5/pkg/machine" + "github.com/containers/podman/v5/pkg/machine/define" +) + +func setupMachineSockets(name string, dirs *define.MachineDirs) ([]string, string, machine.APIForwardingState, error) { + machinePipe := machine.ToDist(name) + if !machine.PipeNameAvailable(machinePipe, machine.MachineNameWait) { + return nil, "", 0, fmt.Errorf("could not start api proxy since expected pipe is not available: %s", machinePipe) + } + sockets := []string{machine.NamedPipePrefix + machinePipe} + state := machine.MachineLocal + + if machine.PipeNameAvailable(machine.GlobalNamedPipe, machine.GlobalNameWait) { + sockets = append(sockets, machine.NamedPipePrefix+machine.GlobalNamedPipe) + state = machine.DockerGlobal + } + + return sockets, sockets[len(sockets)-1], state, nil +} diff --git a/pkg/machine/ssh.go b/pkg/machine/ssh.go index de0d2bfad6..7a66984cf2 100644 --- a/pkg/machine/ssh.go +++ b/pkg/machine/ssh.go @@ -2,7 +2,6 @@ package machine import ( "fmt" - "os" "os/exec" "strconv" @@ -13,24 +12,38 @@ import ( // and a port // TODO This should probably be taught about an machineconfig to reduce input func CommonSSH(username, identityPath, name string, sshPort int, inputArgs []string) error { + return commonSSH(username, identityPath, name, sshPort, inputArgs, false) +} + +func CommonSSHSilent(username, identityPath, name string, sshPort int, inputArgs []string) error { + return commonSSH(username, identityPath, name, sshPort, inputArgs, true) +} + +func commonSSH(username, identityPath, name string, sshPort int, inputArgs []string, silent bool) error { sshDestination := username + "@localhost" port := strconv.Itoa(sshPort) + interactive := true args := []string{"-i", identityPath, "-p", port, sshDestination, "-o", "IdentitiesOnly=yes", "-o", "StrictHostKeyChecking=no", "-o", "LogLevel=ERROR", "-o", "SetEnv=LC_ALL="} if len(inputArgs) > 0 { + interactive = false args = append(args, inputArgs...) } else { + // ensure we have a tty + args = append(args, "-t") fmt.Printf("Connecting to vm %s. To close connection, use `~.` or `exit`\n", name) } cmd := exec.Command("ssh", args...) logrus.Debugf("Executing: ssh %v\n", args) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Stdin = os.Stdin + if !silent { + if err := setupIOPassthrough(cmd, interactive); err != nil { + return err + } + } return cmd.Run() } diff --git a/pkg/machine/ssh_unix.go b/pkg/machine/ssh_unix.go new file mode 100644 index 0000000000..17e5acd06f --- /dev/null +++ b/pkg/machine/ssh_unix.go @@ -0,0 +1,16 @@ +//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd + +package machine + +import ( + "os" + "os/exec" +) + +func setupIOPassthrough(cmd *exec.Cmd, interactive bool) error { + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + return nil +} diff --git a/pkg/machine/ssh_windows.go b/pkg/machine/ssh_windows.go new file mode 100644 index 0000000000..3440dc1687 --- /dev/null +++ b/pkg/machine/ssh_windows.go @@ -0,0 +1,42 @@ +package machine + +import ( + "io" + "os" + "os/exec" + + "github.com/sirupsen/logrus" +) + +func setupIOPassthrough(cmd *exec.Cmd, interactive bool) error { + cmd.Stdin = os.Stdin + + if interactive { + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return nil + } + + // OpenSSh mucks with the associated virtual console when there is no pty, + // leaving it in a broken state. Pipe the output to isolate stdout/stderr + stdout, err := cmd.StdoutPipe() + if err != nil { + return err + } + + stderr, err := cmd.StderrPipe() + if err != nil { + return err + } + + copier := func(name string, dest string, from io.Reader, to io.Writer) { + if _, err := io.Copy(to, from); err != nil { + logrus.Warnf("could not copy output from command %s to %s", name, dest) + } + } + + go copier(cmd.Path, "stdout", stdout, os.Stdout) + go copier(cmd.Path, "stderr", stderr, os.Stderr) + + return nil +} diff --git a/pkg/machine/stdpull/url.go b/pkg/machine/stdpull/url.go index cc271329c3..a8cc27a3f8 100644 --- a/pkg/machine/stdpull/url.go +++ b/pkg/machine/stdpull/url.go @@ -23,7 +23,7 @@ type DiskFromURL struct { tempLocation *define.VMFile } -func NewDiskFromURL(inputPath string, finalPath *define.VMFile, tempDir *define.VMFile) (*DiskFromURL, error) { +func NewDiskFromURL(inputPath string, finalPath *define.VMFile, tempDir *define.VMFile, optionalTempFileName *string) (*DiskFromURL, error) { var ( err error ) @@ -40,6 +40,9 @@ func NewDiskFromURL(inputPath string, finalPath *define.VMFile, tempDir *define. } remoteImageName := path.Base(inputPath) + if optionalTempFileName != nil { + remoteImageName = *optionalTempFileName + } if remoteImageName == "" { return nil, fmt.Errorf("invalid url: unable to determine image name in %q", inputPath) } diff --git a/pkg/machine/vmconfigs/config.go b/pkg/machine/vmconfigs/config.go index 44179c0fc0..8ddaef8137 100644 --- a/pkg/machine/vmconfigs/config.go +++ b/pkg/machine/vmconfigs/config.go @@ -107,6 +107,10 @@ func (f fcosMachineImage) path() string { type VMProvider interface { //nolint:interfacebloat CreateVM(opts define.CreateVMOpts, mc *MachineConfig, builder *ignition.IgnitionBuilder) error + // GetDisk should be only temporary. It is largely here only because WSL disk pulling is different + // TODO + // Let's deprecate this ASAP + GetDisk(userInputPath string, dirs *define.MachineDirs, mc *MachineConfig) error PrepareIgnition(mc *MachineConfig, ignBuilder *ignition.IgnitionBuilder) (*ignition.ReadyUnitOpts, error) GetHyperVisorVMs() ([]string, error) MountType() VolumeMountType @@ -115,12 +119,15 @@ type VMProvider interface { //nolint:interfacebloat RemoveAndCleanMachines(dirs *define.MachineDirs) error SetProviderAttrs(mc *MachineConfig, opts define.SetOptions) error StartNetworking(mc *MachineConfig, cmd *gvproxy.GvproxyCommand) error - PostStartNetworking(mc *MachineConfig) error + PostStartNetworking(mc *MachineConfig, noInfo bool) error StartVM(mc *MachineConfig) (func() error, func() error, error) State(mc *MachineConfig, bypass bool) (define.Status, error) StopVM(mc *MachineConfig, hardStop bool) error StopHostNetworking(mc *MachineConfig, vmType define.VMType) error VMType() define.VMType + UserModeNetworkEnabled(mc *MachineConfig) bool + UseProviderNetworkSetup() bool + RequireExclusiveActive() bool } // HostUser describes the host user diff --git a/pkg/machine/vmconfigs/config_windows.go b/pkg/machine/vmconfigs/config_windows.go index aa24dd707d..0562490c7c 100644 --- a/pkg/machine/vmconfigs/config_windows.go +++ b/pkg/machine/vmconfigs/config_windows.go @@ -13,7 +13,8 @@ type HyperVConfig struct { } type WSLConfig struct { - //wslstuff *aThing + // Uses usermode networking + UserModeNetworking bool } // Stubs diff --git a/pkg/machine/vmconfigs/machine.go b/pkg/machine/vmconfigs/machine.go index 1176c92109..463b017fd8 100644 --- a/pkg/machine/vmconfigs/machine.go +++ b/pkg/machine/vmconfigs/machine.go @@ -77,6 +77,7 @@ func NewMachineConfig(opts define.InitOptions, dirs *define.MachineDirs, sshIden } mc.Resources = mrc + // TODO WSL had a locking port mechanism, we should consider this. sshPort, err := utils.GetRandomPort() if err != nil { return nil, err diff --git a/pkg/machine/wsl/declares.go b/pkg/machine/wsl/declares.go new file mode 100644 index 0000000000..a8a8179008 --- /dev/null +++ b/pkg/machine/wsl/declares.go @@ -0,0 +1,249 @@ +//go:build windows + +package wsl + +const ( + ErrorSuccessRebootInitiated = 1641 + ErrorSuccessRebootRequired = 3010 + currentMachineVersion = 3 +) + +const containersConf = `[containers] + +[engine] +cgroup_manager = "cgroupfs" +` + +const registriesConf = `unqualified-search-registries=["docker.io"] +` + +const appendPort = `grep -q Port\ %d /etc/ssh/sshd_config || echo Port %d >> /etc/ssh/sshd_config` + +const changePort = `sed -E -i 's/^Port[[:space:]]+[0-9]+/Port %d/' /etc/ssh/sshd_config` + +const configServices = `ln -fs /usr/lib/systemd/system/sshd.service /etc/systemd/system/multi-user.target.wants/sshd.service +ln -fs /usr/lib/systemd/system/podman.socket /etc/systemd/system/sockets.target.wants/podman.socket +rm -f /etc/systemd/system/getty.target.wants/console-getty.service +rm -f /etc/systemd/system/getty.target.wants/getty@tty1.service +rm -f /etc/systemd/system/multi-user.target.wants/systemd-resolved.service +rm -f /etc/systemd/system/sysinit.target.wants//systemd-resolved.service +rm -f /etc/systemd/system/dbus-org.freedesktop.resolve1.service +ln -fs /dev/null /etc/systemd/system/console-getty.service +ln -fs /dev/null /etc/systemd/system/systemd-oomd.socket +mkdir -p /etc/systemd/system/systemd-sysusers.service.d/ +echo CREATE_MAIL_SPOOL=no >> /etc/default/useradd +adduser -m [USER] -G wheel +mkdir -p /home/[USER]/.config/systemd/[USER]/ +chown [USER]:[USER] /home/[USER]/.config +` + +const sudoers = `%wheel ALL=(ALL) NOPASSWD: ALL +` + +const bootstrap = `#!/bin/bash +ps -ef | grep -v grep | grep -q systemd && exit 0 +nohup unshare --kill-child --fork --pid --mount --mount-proc --propagation shared /lib/systemd/systemd >/dev/null 2>&1 & +sleep 0.1 +` + +const wslmotd = ` +You will be automatically entered into a nested process namespace where +systemd is running. If you need to access the parent namespace, hit ctrl-d +or type exit. This also means to log out you need to exit twice. + +` + +const sysdpid = "SYSDPID=`ps -eo cmd,pid | grep -m 1 ^/lib/systemd/systemd | awk '{print $2}'`" + +const profile = sysdpid + ` +if [ ! -z "$SYSDPID" ] && [ "$SYSDPID" != "1" ]; then + cat /etc/wslmotd + /usr/local/bin/enterns +fi +` + +const enterns = "#!/bin/bash\n" + sysdpid + ` +if [ ! -z "$SYSDPID" ] && [ "$SYSDPID" != "1" ]; then + NSENTER=("nsenter" "-m" "-p" "-t" "$SYSDPID" "--wd=$PWD") + + if [ "$UID" != "0" ]; then + NSENTER=("sudo" "${NSENTER[@]}") + if [ "$#" != "0" ]; then + NSENTER+=("sudo" "-u" "$USER") + else + NSENTER+=("su" "-l" "$USER") + fi + fi + "${NSENTER[@]}" "$@" +fi` + +const waitTerm = sysdpid + ` +if [ ! -z "$SYSDPID" ]; then + timeout 60 tail -f /dev/null --pid $SYSDPID +fi +` + +const wslConf = `[user] +default=[USER] +` + +const wslConfUserNet = ` +[network] +generateResolvConf = false +` + +const resolvConfUserNet = ` +nameserver 192.168.127.1 +` + +// WSL kernel does not have sg and crypto_user modules +const overrideSysusers = `[Service] +LoadCredential= +` + +const lingerService = `[Unit] +Description=A systemd user unit demo +After=network-online.target +Wants=network-online.target podman.socket +[Service] +ExecStart=/usr/bin/sleep infinity +` + +const lingerSetup = `mkdir -p /home/[USER]/.config/systemd/user/default.target.wants +ln -fs /home/[USER]/.config/systemd/user/linger-example.service \ + /home/[USER]/.config/systemd/user/default.target.wants/linger-example.service +` + +const bindMountSystemService = ` +[Unit] +Description=Bind mount for system podman sockets +After=podman.socket + +[Service] +RemainAfterExit=true +Type=oneshot +# Ensure user services can register sockets as well +ExecStartPre=mkdir -p -m 777 /mnt/wsl/podman-sockets +ExecStartPre=mkdir -p -m 777 /mnt/wsl/podman-sockets/%[1]s +ExecStartPre=touch /mnt/wsl/podman-sockets/%[1]s/podman-root.sock +ExecStart=mount --bind %%t/podman/podman.sock /mnt/wsl/podman-sockets/%[1]s/podman-root.sock +ExecStop=umount /mnt/wsl/podman-sockets/%[1]s/podman-root.sock +` + +const bindMountUserService = ` +[Unit] +Description=Bind mount for user podman sockets +After=podman.socket + +[Service] +RemainAfterExit=true +Type=oneshot +# Consistency with system service (supports racing) +ExecStartPre=mkdir -p -m 777 /mnt/wsl/podman-sockets +ExecStartPre=mkdir -p -m 777 /mnt/wsl/podman-sockets/%[1]s +ExecStartPre=touch /mnt/wsl/podman-sockets/%[1]s/podman-user.sock +# Relies on /etc/fstab entry for user mounting +ExecStart=mount /mnt/wsl/podman-sockets/%[1]s/podman-user.sock +ExecStop=umount /mnt/wsl/podman-sockets/%[1]s/podman-user.sock +` + +const bindMountFsTab = `/run/user/1000/podman/podman.sock /mnt/wsl/podman-sockets/%s/podman-user.sock none noauto,user,bind,defaults 0 0 +` +const ( + defaultTargetWants = "default.target.wants" + userSystemdPath = "/home/%[1]s/.config/systemd/user" + sysSystemdPath = "/etc/systemd/system" + userSystemdWants = userSystemdPath + "/" + defaultTargetWants + sysSystemdWants = sysSystemdPath + "/" + defaultTargetWants + bindUnitFileName = "podman-mnt-bindings.service" + bindUserUnitPath = userSystemdPath + "/" + bindUnitFileName + bindUserUnitWant = userSystemdWants + "/" + bindUnitFileName + bindSysUnitPath = sysSystemdPath + "/" + bindUnitFileName + bindSysUnitWant = sysSystemdWants + "/" + bindUnitFileName + podmanSocketDropin = "podman.socket.d" + podmanSocketDropinPath = sysSystemdPath + "/" + podmanSocketDropin +) + +const configBindServices = "mkdir -p " + userSystemdWants + " " + sysSystemdWants + " " + podmanSocketDropinPath + "\n" + + "ln -fs " + bindUserUnitPath + " " + bindUserUnitWant + "\n" + + "ln -fs " + bindSysUnitPath + " " + bindSysUnitWant + "\n" + +const overrideSocketGroup = ` +[Socket] +SocketMode=0660 +SocketGroup=wheel +` + +const proxyConfigSetup = `#!/bin/bash + +SYSTEMD_CONF=/etc/systemd/system.conf.d/default-env.conf +ENVD_CONF=/etc/environment.d/default-env.conf +PROFILE_CONF=/etc/profile.d/default-env.sh + +IFS="|" +read proxies + +mkdir -p /etc/profile.d /etc/environment.d /etc/systemd/system.conf.d/ +rm -f $SYSTEMD_CONF +for proxy in $proxies; do + output+="$proxy " +done +echo "[Manager]" >> $SYSTEMD_CONF +echo -ne "DefaultEnvironment=" >> $SYSTEMD_CONF + +echo $output >> $SYSTEMD_CONF +rm -f $ENVD_CONF +for proxy in $proxies; do + echo "$proxy" >> $ENVD_CONF +done +rm -f $PROFILE_CONF +for proxy in $proxies; do + echo "export $proxy" >> $PROFILE_CONF +done +` + +const proxyConfigAttempt = `if [ -f /usr/local/bin/proxyinit ]; \ +then /usr/local/bin/proxyinit; \ +else exit 42; \ +fi` + +const clearProxySettings = `rm -f /etc/systemd/system.conf.d/default-env.conf \ + /etc/environment.d/default-env.conf \ + /etc/profile.d/default-env.sh` + +const wslInstallError = `Could not %s. See previous output for any potential failure details. +If you can not resolve the issue, and rerunning fails, try the "wsl --install" process +outlined in the following article: + +http://docs.microsoft.com/en-us/windows/wsl/install + +` + +const wslKernelError = `Could not %s. See previous output for any potential failure details. +If you can not resolve the issue, try rerunning the "podman machine init command". If that fails +try the "wsl --update" command and then rerun "podman machine init". Finally, if all else fails, +try following the steps outlined in the following article: + +http://docs.microsoft.com/en-us/windows/wsl/install + +` + +const wslInstallKernel = "install the WSL Kernel" + +const wslOldVersion = `Automatic installation of WSL can not be performed on this version of Windows +Either update to Build 19041 (or later), or perform the manual installation steps +outlined in the following article: + +http://docs.microsoft.com/en-us/windows/wsl/install\ + +` + +const ( + gvProxy = "gvproxy.exe" + winSShProxy = "win-sshproxy.exe" + pipePrefix = "npipe:////./pipe/" + globalPipe = "docker_engine" + userModeDist = "podman-net-usermode" + rootfulSock = "/run/podman/podman.sock" + rootlessSock = "/run/user/1000/podman/podman.sock" +) diff --git a/pkg/machine/wsl/fedora.go b/pkg/machine/wsl/fedora.go index 5ad76acb2a..4e4fb9a5b3 100644 --- a/pkg/machine/wsl/fedora.go +++ b/pkg/machine/wsl/fedora.go @@ -1,5 +1,3 @@ -//go:build windows - package wsl import ( @@ -14,6 +12,8 @@ import ( "strings" "time" + "github.com/sirupsen/logrus" + "github.com/containers/podman/v5/pkg/machine" "github.com/containers/podman/v5/pkg/machine/define" ) @@ -27,8 +27,10 @@ type FedoraDownload struct { machine.Download } +// NewFedoraDownloader +// deprecated func NewFedoraDownloader(vmType define.VMType, vmName, releaseStream string) (machine.DistributionDownload, error) { - downloadURL, version, arch, size, err := getFedoraDownload() + downloadURL, version, arch, size, err := GetFedoraDownloadForWSL() if err != nil { return nil, err } @@ -82,7 +84,7 @@ func (f FedoraDownload) CleanCache() error { return machine.RemoveImageAfterExpire(f.CacheDir, expire) } -func getFedoraDownload() (*url.URL, string, string, int64, error) { +func GetFedoraDownloadForWSL() (*url.URL, string, string, int64, error) { var releaseURL string arch := machine.DetermineMachineArch() switch arch { @@ -118,11 +120,14 @@ func getFedoraDownload() (*url.URL, string, string, int64, error) { return nil, "", "", -1, fmt.Errorf("get request failed: %s: %w", verURL.String(), err) } - defer resp.Body.Close() - bytes, err := io.ReadAll(&io.LimitedReader{R: resp.Body, N: 1024}) + defer func() { + if err := resp.Body.Close(); err != nil { + logrus.Errorf("error closing http boddy: %q", err) + } + }() + b, err := io.ReadAll(&io.LimitedReader{R: resp.Body, N: 1024}) if err != nil { return nil, "", "", -1, fmt.Errorf("failed reading: %s: %w", verURL.String(), err) } - - return downloadURL, strings.TrimSpace(string(bytes)), arch, contentLen, nil + return downloadURL, strings.TrimSpace(string(b)), arch, contentLen, nil } diff --git a/pkg/machine/wsl/machine.go b/pkg/machine/wsl/machine.go index 42a6331bbe..01eb0df2de 100644 --- a/pkg/machine/wsl/machine.go +++ b/pkg/machine/wsl/machine.go @@ -4,11 +4,9 @@ package wsl import ( "bufio" - "encoding/json" "errors" "fmt" "io" - "net/url" "os" "os/exec" "path/filepath" @@ -16,8 +14,6 @@ import ( "strings" "time" - "github.com/containers/podman/v5/pkg/machine/connection" - "github.com/containers/common/pkg/config" "github.com/containers/podman/v5/pkg/machine" "github.com/containers/podman/v5/pkg/machine/define" @@ -26,7 +22,6 @@ import ( "github.com/containers/podman/v5/pkg/machine/wsl/wutil" "github.com/containers/podman/v5/utils" "github.com/containers/storage/pkg/homedir" - "github.com/containers/storage/pkg/lockfile" "github.com/sirupsen/logrus" "golang.org/x/text/encoding/unicode" "golang.org/x/text/transform" @@ -37,277 +32,6 @@ var ( vmtype = define.WSLVirt ) -const ( - ErrorSuccessRebootInitiated = 1641 - ErrorSuccessRebootRequired = 3010 - currentMachineVersion = 3 -) - -const containersConf = `[containers] - -[engine] -cgroup_manager = "cgroupfs" -` - -const registriesConf = `unqualified-search-registries=["docker.io"] -` - -const appendPort = `grep -q Port\ %d /etc/ssh/sshd_config || echo Port %d >> /etc/ssh/sshd_config` - -const changePort = `sed -E -i 's/^Port[[:space:]]+[0-9]+/Port %d/' /etc/ssh/sshd_config` - -const configServices = `ln -fs /usr/lib/systemd/system/sshd.service /etc/systemd/system/multi-user.target.wants/sshd.service -ln -fs /usr/lib/systemd/system/podman.socket /etc/systemd/system/sockets.target.wants/podman.socket -rm -f /etc/systemd/system/getty.target.wants/console-getty.service -rm -f /etc/systemd/system/getty.target.wants/getty@tty1.service -rm -f /etc/systemd/system/multi-user.target.wants/systemd-resolved.service -rm -f /etc/systemd/system/sysinit.target.wants//systemd-resolved.service -rm -f /etc/systemd/system/dbus-org.freedesktop.resolve1.service -ln -fs /dev/null /etc/systemd/system/console-getty.service -ln -fs /dev/null /etc/systemd/system/systemd-oomd.socket -mkdir -p /etc/systemd/system/systemd-sysusers.service.d/ -echo CREATE_MAIL_SPOOL=no >> /etc/default/useradd -adduser -m [USER] -G wheel -mkdir -p /home/[USER]/.config/systemd/[USER]/ -chown [USER]:[USER] /home/[USER]/.config -` - -const sudoers = `%wheel ALL=(ALL) NOPASSWD: ALL -` - -const bootstrap = `#!/bin/bash -ps -ef | grep -v grep | grep -q systemd && exit 0 -nohup unshare --kill-child --fork --pid --mount --mount-proc --propagation shared /lib/systemd/systemd >/dev/null 2>&1 & -sleep 0.1 -` - -const wslmotd = ` -You will be automatically entered into a nested process namespace where -systemd is running. If you need to access the parent namespace, hit ctrl-d -or type exit. This also means to log out you need to exit twice. - -` - -const sysdpid = "SYSDPID=`ps -eo cmd,pid | grep -m 1 ^/lib/systemd/systemd | awk '{print $2}'`" - -const profile = sysdpid + ` -if [ ! -z "$SYSDPID" ] && [ "$SYSDPID" != "1" ]; then - cat /etc/wslmotd - /usr/local/bin/enterns -fi -` - -const enterns = "#!/bin/bash\n" + sysdpid + ` -if [ ! -z "$SYSDPID" ] && [ "$SYSDPID" != "1" ]; then - NSENTER=("nsenter" "-m" "-p" "-t" "$SYSDPID" "--wd=$PWD") - - if [ "$UID" != "0" ]; then - NSENTER=("sudo" "${NSENTER[@]}") - if [ "$#" != "0" ]; then - NSENTER+=("sudo" "-u" "$USER") - else - NSENTER+=("su" "-l" "$USER") - fi - fi - "${NSENTER[@]}" "$@" -fi` - -const waitTerm = sysdpid + ` -if [ ! -z "$SYSDPID" ]; then - timeout 60 tail -f /dev/null --pid $SYSDPID -fi -` - -const wslConf = `[user] -default=[USER] -` - -const wslConfUserNet = ` -[network] -generateResolvConf = false -` - -const resolvConfUserNet = ` -nameserver 192.168.127.1 -` - -// WSL kernel does not have sg and crypto_user modules -const overrideSysusers = `[Service] -LoadCredential= -` - -const lingerService = `[Unit] -Description=A systemd user unit demo -After=network-online.target -Wants=network-online.target podman.socket -[Service] -ExecStart=/usr/bin/sleep infinity -` - -const lingerSetup = `mkdir -p /home/[USER]/.config/systemd/user/default.target.wants -ln -fs /home/[USER]/.config/systemd/user/linger-example.service \ - /home/[USER]/.config/systemd/user/default.target.wants/linger-example.service -` - -const bindMountSystemService = ` -[Unit] -Description=Bind mount for system podman sockets -After=podman.socket - -[Service] -RemainAfterExit=true -Type=oneshot -# Ensure user services can register sockets as well -ExecStartPre=mkdir -p -m 777 /mnt/wsl/podman-sockets -ExecStartPre=mkdir -p -m 777 /mnt/wsl/podman-sockets/%[1]s -ExecStartPre=touch /mnt/wsl/podman-sockets/%[1]s/podman-root.sock -ExecStart=mount --bind %%t/podman/podman.sock /mnt/wsl/podman-sockets/%[1]s/podman-root.sock -ExecStop=umount /mnt/wsl/podman-sockets/%[1]s/podman-root.sock -` - -const bindMountUserService = ` -[Unit] -Description=Bind mount for user podman sockets -After=podman.socket - -[Service] -RemainAfterExit=true -Type=oneshot -# Consistency with system service (supports racing) -ExecStartPre=mkdir -p -m 777 /mnt/wsl/podman-sockets -ExecStartPre=mkdir -p -m 777 /mnt/wsl/podman-sockets/%[1]s -ExecStartPre=touch /mnt/wsl/podman-sockets/%[1]s/podman-user.sock -# Relies on /etc/fstab entry for user mounting -ExecStart=mount /mnt/wsl/podman-sockets/%[1]s/podman-user.sock -ExecStop=umount /mnt/wsl/podman-sockets/%[1]s/podman-user.sock -` - -const bindMountFsTab = `/run/user/1000/podman/podman.sock /mnt/wsl/podman-sockets/%s/podman-user.sock none noauto,user,bind,defaults 0 0 -` -const ( - defaultTargetWants = "default.target.wants" - userSystemdPath = "/home/%[1]s/.config/systemd/user" - sysSystemdPath = "/etc/systemd/system" - userSystemdWants = userSystemdPath + "/" + defaultTargetWants - sysSystemdWants = sysSystemdPath + "/" + defaultTargetWants - bindUnitFileName = "podman-mnt-bindings.service" - bindUserUnitPath = userSystemdPath + "/" + bindUnitFileName - bindUserUnitWant = userSystemdWants + "/" + bindUnitFileName - bindSysUnitPath = sysSystemdPath + "/" + bindUnitFileName - bindSysUnitWant = sysSystemdWants + "/" + bindUnitFileName - podmanSocketDropin = "podman.socket.d" - podmanSocketDropinPath = sysSystemdPath + "/" + podmanSocketDropin -) - -const configBindServices = "mkdir -p " + userSystemdWants + " " + sysSystemdWants + " " + podmanSocketDropinPath + "\n" + - "ln -fs " + bindUserUnitPath + " " + bindUserUnitWant + "\n" + - "ln -fs " + bindSysUnitPath + " " + bindSysUnitWant + "\n" - -const overrideSocketGroup = ` -[Socket] -SocketMode=0660 -SocketGroup=wheel -` - -const proxyConfigSetup = `#!/bin/bash - -SYSTEMD_CONF=/etc/systemd/system.conf.d/default-env.conf -ENVD_CONF=/etc/environment.d/default-env.conf -PROFILE_CONF=/etc/profile.d/default-env.sh - -IFS="|" -read proxies - -mkdir -p /etc/profile.d /etc/environment.d /etc/systemd/system.conf.d/ -rm -f $SYSTEMD_CONF -for proxy in $proxies; do - output+="$proxy " -done -echo "[Manager]" >> $SYSTEMD_CONF -echo -ne "DefaultEnvironment=" >> $SYSTEMD_CONF - -echo $output >> $SYSTEMD_CONF -rm -f $ENVD_CONF -for proxy in $proxies; do - echo "$proxy" >> $ENVD_CONF -done -rm -f $PROFILE_CONF -for proxy in $proxies; do - echo "export $proxy" >> $PROFILE_CONF -done -` - -const proxyConfigAttempt = `if [ -f /usr/local/bin/proxyinit ]; \ -then /usr/local/bin/proxyinit; \ -else exit 42; \ -fi` - -const clearProxySettings = `rm -f /etc/systemd/system.conf.d/default-env.conf \ - /etc/environment.d/default-env.conf \ - /etc/profile.d/default-env.sh` - -const wslInstallError = `Could not %s. See previous output for any potential failure details. -If you can not resolve the issue, and rerunning fails, try the "wsl --install" process -outlined in the following article: - -http://docs.microsoft.com/en-us/windows/wsl/install - -` - -const wslKernelError = `Could not %s. See previous output for any potential failure details. -If you can not resolve the issue, try rerunning the "podman machine init command". If that fails -try the "wsl --update" command and then rerun "podman machine init". Finally, if all else fails, -try following the steps outlined in the following article: - -http://docs.microsoft.com/en-us/windows/wsl/install - -` - -const wslInstallKernel = "install the WSL Kernel" - -const wslOldVersion = `Automatic installation of WSL can not be performed on this version of Windows -Either update to Build 19041 (or later), or perform the manual installation steps -outlined in the following article: - -http://docs.microsoft.com/en-us/windows/wsl/install\ - -` - -const ( - gvProxy = "gvproxy.exe" - winSShProxy = "win-sshproxy.exe" - pipePrefix = "npipe:////./pipe/" - globalPipe = "docker_engine" - userModeDist = "podman-net-usermode" - rootfulSock = "/run/podman/podman.sock" - rootlessSock = "/run/user/1000/podman/podman.sock" -) - -type MachineVM struct { - // ConfigPath is the path to the configuration file - ConfigPath string - // Created contains the original created time instead of querying the file mod time - Created time.Time - // ImageStream is the version of fcos being used - ImageStream string - // ImagePath is the fq path to - ImagePath string - // LastUp contains the last recorded uptime - LastUp time.Time - // Name of the vm - Name string - // Whether this machine should run in a rootful or rootless manner - Rootful bool - // SSH identity, username, etc - vmconfigs.SSHConfig - // machine version - Version int - // Whether to use user-mode networking - UserModeNetworking bool - // Used at runtime for serializing write operations - lock *lockfile.LockFile -} - type ExitCodeError struct { code uint } @@ -329,153 +53,14 @@ func getConfigPathExt(name string, extension string) (string, error) { return filepath.Join(vmConfigDir, fmt.Sprintf("%s.%s", name, extension)), nil } -// readAndMigrate returns the content of the VM's -// configuration file in json -func readAndMigrate(configPath string, name string) (*MachineVM, error) { - vm := new(MachineVM) - b, err := os.ReadFile(configPath) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return nil, fmt.Errorf("%v: %w", name, define.ErrNoSuchVM) - } - return vm, err - } - err = json.Unmarshal(b, vm) - if err == nil && vm.Version < currentMachineVersion { - err = vm.migrateMachine(configPath) - } - - return vm, err -} - -func (v *MachineVM) migrateMachine(configPath string) error { - if v.Created.IsZero() { - if err := v.migrate40(configPath); err != nil { - return err - } - } - - // Update older machines to use lingering - if err := enableUserLinger(v, toDist(v.Name)); err != nil { - return err - } - - // Update older machines missing unqualified search config - if err := configureRegistries(v, toDist(v.Name)); err != nil { - return err - } - - v.Version = currentMachineVersion - return v.writeConfig() -} - -func (v *MachineVM) migrate40(configPath string) error { - v.ConfigPath = configPath - fi, err := os.Stat(configPath) - if err != nil { - return err - } - v.Created = fi.ModTime() - v.LastUp = getLegacyLastStart(v) - return nil -} - -func getLegacyLastStart(vm *MachineVM) time.Time { - vmDataDir, err := machine.GetDataDir(vmtype) - if err != nil { - return vm.Created - } - distDir := filepath.Join(vmDataDir, "wsldist") - start := filepath.Join(distDir, vm.Name, "laststart") - info, err := os.Stat(start) - if err != nil { - return vm.Created - } - return info.ModTime() -} - -// Init writes the json configuration file to the filesystem for -// other verbs (start, stop) -func (v *MachineVM) Init(opts define.InitOptions) (bool, error) { - var ( - err error - ) - // cleanup half-baked files if init fails at any point - callbackFuncs := machine.InitCleanup() - defer callbackFuncs.CleanIfErr(&err) - go callbackFuncs.CleanOnSignal() - - if cont, err := checkAndInstallWSL(opts); !cont { - appendOutputIfError(opts.ReExec, err) - return cont, err - } - - _ = setupWslProxyEnv() - v.IdentityPath, err = machine.GetSSHIdentityPath(define.DefaultIdentityName) - if err != nil { - return false, err - } - v.Rootful = opts.Rootful - v.Version = currentMachineVersion - - if v.UserModeNetworking { - if err = verifyWSLUserModeCompat(); err != nil { - return false, err - } - } - - if err = downloadDistro(v, opts); err != nil { - return false, err - } - callbackFuncs.Add(v.removeMachineImage) - - const prompt = "Importing operating system into WSL (this may take a few minutes on a new WSL install)..." - dist, err := provisionWSLDist(v.Name, v.ImagePath, prompt) - if err != nil { - return false, err - } - callbackFuncs.Add(v.unprovisionWSL) - - if v.UserModeNetworking { - if err = installUserModeDist(dist, v.ImagePath); err != nil { - _ = unregisterDist(dist) - return false, err - } - } - - fmt.Println("Configuring system...") - if err = configureSystem(v, dist); err != nil { - return false, err - } - - if err = installScripts(dist); err != nil { - return false, err - } - - if err = createKeys(v, dist); err != nil { - return false, err - } - - // Cycle so that user change goes into effect - _ = terminateDist(dist) - - if err = v.writeConfig(); err != nil { - return false, err - } - callbackFuncs.Add(v.removeMachineConfig) - - if err = setupConnections(v, opts); err != nil { - return false, err - } - callbackFuncs.Add(v.removeSystemConnections) - return true, nil -} - -func (v *MachineVM) unprovisionWSL() error { - if err := terminateDist(toDist(v.Name)); err != nil { +// TODO like provisionWSL, i think this needs to be pushed to use common +// paths and types where possible +func unprovisionWSL(mc *vmconfigs.MachineConfig) error { + dist := machine.ToDist(mc.Name) + if err := terminateDist(dist); err != nil { logrus.Error(err) } - if err := unregisterDist(toDist(v.Name)); err != nil { + if err := unregisterDist(dist); err != nil { logrus.Error(err) } @@ -484,84 +69,13 @@ func (v *MachineVM) unprovisionWSL() error { return err } distDir := filepath.Join(vmDataDir, "wsldist") - distTarget := filepath.Join(distDir, v.Name) + distTarget := filepath.Join(distDir, mc.Name) return utils.GuardedRemoveAll(distTarget) } -func (v *MachineVM) removeMachineConfig() error { - return utils.GuardedRemoveAll(v.ConfigPath) -} - -func (v *MachineVM) removeMachineImage() error { - return utils.GuardedRemoveAll(v.ImagePath) -} - -func (v *MachineVM) removeSystemConnections() error { - return connection.RemoveConnections(v.Name, fmt.Sprintf("%s-root", v.Name)) -} - -func downloadDistro(v *MachineVM, opts define.InitOptions) error { - var ( - dd machine.DistributionDownload - err error - ) - - // The default FCOS stream names are reserved indicators for the standard Fedora fetch - if machine.IsValidFCOSStreamString(opts.ImagePath) { - v.ImageStream = opts.ImagePath - dd, err = NewFedoraDownloader(vmtype, v.Name, opts.ImagePath) - } else { - v.ImageStream = "custom" - dd, err = machine.NewGenericDownloader(vmtype, v.Name, opts.ImagePath) - } - if err != nil { - return err - } - - v.ImagePath = dd.Get().LocalUncompressedFile - return machine.DownloadImage(dd) -} - -func (v *MachineVM) writeConfig() error { - return machine.WriteConfig(v.ConfigPath, v) -} - -func constructSSHUris(v *MachineVM) ([]url.URL, []string) { - uri := connection.SSHRemoteConnection.MakeSSHURL(connection.LocalhostIP, rootlessSock, strconv.Itoa(v.Port), v.RemoteUsername) - uriRoot := connection.SSHRemoteConnection.MakeSSHURL(connection.LocalhostIP, rootfulSock, strconv.Itoa(v.Port), "root") - - uris := []url.URL{uri, uriRoot} - names := []string{v.Name, v.Name + "-root"} - - return uris, names -} - -func setupConnections(v *MachineVM, opts define.InitOptions) error { - uris, names := constructSSHUris(v) - - // The first connection defined when connections is empty will become the default - // regardless of IsDefault, so order according to rootful - if opts.Rootful { - uris[0], names[0], uris[1], names[1] = uris[1], names[1], uris[0], names[0] - } - - // We need to prevent racing connection updates to containers.conf globally - // across all backends to prevent connection overwrites - flock, err := obtainGlobalConfigLock() - if err != nil { - return fmt.Errorf("could not obtain global lock: %w", err) - } - defer flock.unlock() - - for i := 0; i < 2; i++ { - if err := connection.AddConnection(&uris[i], names[i], v.IdentityPath, opts.IsDefault && i == 0); err != nil { - return err - } - } - - return nil -} - +// TODO there are some differences here that I dont fully groak but I think +// we should push this stuff be more common (dir names, etc) and also use +// typed things where possible like vmfiles func provisionWSLDist(name string, imagePath string, prompt string) (string, error) { vmDataDir, err := machine.GetDataDir(vmtype) if err != nil { @@ -574,7 +88,7 @@ func provisionWSLDist(name string, imagePath string, prompt string) (string, err return "", fmt.Errorf("could not create wsldist directory: %w", err) } - dist := toDist(name) + dist := machine.ToDist(name) fmt.Println(prompt) if err = runCmdPassThrough("wsl", "--import", dist, distTarget, imagePath, "--version", "2"); err != nil { return "", fmt.Errorf("the WSL import of guest OS failed: %w", err) @@ -588,18 +102,24 @@ func provisionWSLDist(name string, imagePath string, prompt string) (string, err return dist, nil } -func createKeys(v *MachineVM, dist string) error { - user := v.RemoteUsername +func createKeys(mc *vmconfigs.MachineConfig, dist string) error { + user := mc.SSH.RemoteUsername if err := terminateDist(dist); err != nil { return fmt.Errorf("could not cycle WSL dist: %w", err) } - key, err := wslCreateKeys(v.IdentityPath, dist) + identityPath := mc.SSH.IdentityPath + ".pub" + + // TODO We could audit vmfile reads and see if a 'ReadToString' + // method makes sense. + pubKey, err := os.ReadFile(identityPath) if err != nil { return fmt.Errorf("could not create ssh keys: %w", err) } + key := string(pubKey) + if err := wslPipe(key+"\n", dist, "sh", "-c", "mkdir -p /root/.ssh;"+ "cat >> /root/.ssh/authorized_keys; chmod 600 /root/.ssh/authorized_keys"); err != nil { return fmt.Errorf("could not create root authorized keys on guest OS: %w", err) @@ -609,15 +129,15 @@ func createKeys(v *MachineVM, dist string) error { "cat >> /home/[USER]/.ssh/authorized_keys; chown -R [USER]:[USER] /home/[USER]/.ssh;"+ "chmod 600 /home/[USER]/.ssh/authorized_keys", user) if err := wslPipe(key+"\n", dist, "sh", "-c", userAuthCmd); err != nil { - return fmt.Errorf("could not create '%s' authorized keys on guest OS: %w", v.RemoteUsername, err) + return fmt.Errorf("could not create '%s' authorized keys on guest OS: %w", user, err) } return nil } -func configureSystem(v *MachineVM, dist string) error { - user := v.RemoteUsername - if err := wslInvoke(dist, "sh", "-c", fmt.Sprintf(appendPort, v.Port, v.Port)); err != nil { +func configureSystem(mc *vmconfigs.MachineConfig, dist string) error { + user := mc.SSH.RemoteUsername + if err := wslInvoke(dist, "sh", "-c", fmt.Sprintf(appendPort, mc.SSH.Port, mc.SSH.Port)); err != nil { return fmt.Errorf("could not configure SSH port for guest OS: %w", err) } @@ -639,7 +159,7 @@ func configureSystem(v *MachineVM, dist string) error { return fmt.Errorf("could not generate linger service for guest OS: %w", err) } - if err := enableUserLinger(v, dist); err != nil { + if err := enableUserLinger(mc, dist); err != nil { return err } @@ -651,11 +171,11 @@ func configureSystem(v *MachineVM, dist string) error { return fmt.Errorf("could not create containers.conf for guest OS: %w", err) } - if err := configureRegistries(v, dist); err != nil { + if err := configureRegistries(dist); err != nil { return err } - if err := v.setupPodmanDockerSock(dist, v.Rootful); err != nil { + if err := setupPodmanDockerSock(dist, mc.HostUser.Rootful); err != nil { return err } @@ -667,7 +187,7 @@ func configureSystem(v *MachineVM, dist string) error { return err } - return changeDistUserModeNetworking(dist, user, v.ImagePath, v.UserModeNetworking) + return changeDistUserModeNetworking(dist, user, mc.ImagePath.GetPath(), mc.WSLHypervisor.UserModeNetworking) } func configureBindMounts(dist string, user string) error { @@ -712,7 +232,7 @@ func getBindMountFsTab(dist string) string { return fmt.Sprintf(bindMountFsTab, dist) } -func (v *MachineVM) setupPodmanDockerSock(dist string, rootful bool) error { +func setupPodmanDockerSock(dist string, rootful bool) error { content := ignition.GetPodmanDockerTmpConfig(1000, rootful, true) if err := wslPipe(content, dist, "sh", "-c", "cat > "+ignition.PodmanDockerTmpConfPath); err != nil { @@ -757,8 +277,8 @@ func configureProxy(dist string, useProxy bool, quiet bool) error { return nil } -func enableUserLinger(v *MachineVM, dist string) error { - lingerCmd := "mkdir -p /var/lib/systemd/linger; touch /var/lib/systemd/linger/" + v.RemoteUsername +func enableUserLinger(mc *vmconfigs.MachineConfig, dist string) error { + lingerCmd := "mkdir -p /var/lib/systemd/linger; touch /var/lib/systemd/linger/" + mc.SSH.RemoteUsername if err := wslInvoke(dist, "sh", "-c", lingerCmd); err != nil { return fmt.Errorf("could not enable linger for remote user on guest OS: %w", err) } @@ -766,7 +286,7 @@ func enableUserLinger(v *MachineVM, dist string) error { return nil } -func configureRegistries(v *MachineVM, dist string) error { +func configureRegistries(dist string) error { cmd := "cat > /etc/containers/registries.conf.d/999-podman-machine.conf" if err := wslPipe(registriesConf, dist, "sh", "-c", cmd); err != nil { return fmt.Errorf("could not configure registries on guest OS: %w", err) @@ -811,7 +331,7 @@ func writeWslConf(dist string, user string) error { return nil } -func checkAndInstallWSL(opts define.InitOptions) (bool, error) { +func checkAndInstallWSL(reExec bool) (bool, error) { if wutil.IsWSLInstalled() { return true, nil } @@ -819,11 +339,11 @@ func checkAndInstallWSL(opts define.InitOptions) (bool, error) { admin := hasAdminRights() if !IsWSLFeatureEnabled() { - return false, attemptFeatureInstall(opts, admin) + return false, attemptFeatureInstall(reExec, admin) } skip := false - if !opts.ReExec && !admin { + if reExec && !admin { fmt.Println("Launching WSL Kernel Install...") if err := launchElevate(wslInstallKernel); err != nil { return false, err @@ -838,7 +358,7 @@ func checkAndInstallWSL(opts define.InitOptions) (bool, error) { return false, err } - if opts.ReExec { + if reExec { return false, nil } } @@ -846,7 +366,7 @@ func checkAndInstallWSL(opts define.InitOptions) (bool, error) { return true, nil } -func attemptFeatureInstall(opts define.InitOptions, admin bool) error { +func attemptFeatureInstall(reExec, admin bool) error { if !winVersionAtLeast(10, 0, 18362) { return errors.New("your version of Windows does not support WSL. Update to Windows 10 Build 19041 or later") } else if !winVersionAtLeast(10, 0, 19041) { @@ -864,11 +384,11 @@ func attemptFeatureInstall(opts define.InitOptions, 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 !opts.ReExec && MessageBox(message, "Podman Machine", false) != 1 { + if reExec && MessageBox(message, "Podman Machine", false) != 1 { return errors.New("the WSL installation aborted") } - if !opts.ReExec && !admin { + if reExec && !admin { return launchElevate("install the Windows WSL Features") } @@ -1028,12 +548,6 @@ func isMsiError(err error) bool { return true } -func toDist(name string) string { - if !strings.HasPrefix(name, "podman") { - name = "podman-" + name - } - return name -} func withUser(s string, user string) string { return strings.ReplaceAll(s, "[USER]", user) @@ -1105,126 +619,6 @@ func setupWslProxyEnv() (hasProxy bool) { return } -func (v *MachineVM) Set(_ string, opts define.SetOptions) ([]error, error) { - // If one setting fails to be applied, the others settings will not fail and still be applied. - // The setting(s) that failed to be applied will have its errors returned in setErrors - var setErrors []error - - v.lock.Lock() - defer v.lock.Unlock() - - if opts.Rootful != nil && v.Rootful != *opts.Rootful { - err := v.setRootful(*opts.Rootful) - if err != nil { - setErrors = append(setErrors, fmt.Errorf("setting rootful option: %w", err)) - } else { - if v.isRunning() { - logrus.Warn("restart is necessary for rootful change to go into effect") - } - v.Rootful = *opts.Rootful - } - } - - if opts.CPUs != nil { - setErrors = append(setErrors, errors.New("changing CPUs not supported for WSL machines")) - } - - if opts.Memory != nil { - setErrors = append(setErrors, errors.New("changing memory not supported for WSL machines")) - } - - if opts.USBs != nil { - setErrors = append(setErrors, errors.New("changing USBs not supported for WSL machines")) - } - - if opts.DiskSize != nil { - setErrors = append(setErrors, errors.New("changing disk size not supported for WSL machines")) - } - - if opts.UserModeNetworking != nil && *opts.UserModeNetworking != v.UserModeNetworking { - update := true - if v.isRunning() { - update = false - setErrors = append(setErrors, fmt.Errorf("user-mode networking can only be changed when the machine is not running")) - } else { - dist := toDist(v.Name) - if err := changeDistUserModeNetworking(dist, v.RemoteUsername, v.ImagePath, *opts.UserModeNetworking); err != nil { - update = false - setErrors = append(setErrors, err) - } - } - - if update { - v.UserModeNetworking = *opts.UserModeNetworking - } - } - err := v.writeConfig() - if err != nil { - setErrors = append(setErrors, err) - } - - if len(setErrors) > 0 { - return setErrors, setErrors[0] - } - return setErrors, nil -} - -func (v *MachineVM) Start(name string, opts machine.StartOptions) error { - v.lock.Lock() - defer v.lock.Unlock() - - if v.isRunning() { - return define.ErrVMAlreadyRunning - } - - dist := toDist(name) - useProxy := setupWslProxyEnv() - if err := configureProxy(dist, useProxy, opts.Quiet); err != nil { - return err - } - - if !machine.IsLocalPortAvailable(v.Port) { - logrus.Warnf("SSH port conflict detected, reassigning a new port") - if err := v.reassignSshPort(); err != nil { - return err - } - } - - // Startup user-mode networking if enabled - if err := v.startUserModeNetworking(); err != nil { - return err - } - - err := wslInvoke(dist, "/root/bootstrap") - if err != nil { - return fmt.Errorf("the WSL bootstrap script failed: %w", err) - } - - if !v.Rootful && !opts.NoInfo { - fmt.Printf("\nThis machine is currently configured in rootless mode. If your containers\n") - fmt.Printf("require root permissions (e.g. ports < 1024), or if you run into compatibility\n") - fmt.Printf("issues with non-podman clients, you can switch using the following command: \n") - - suffix := "" - if name != machine.DefaultMachineName { - suffix = " " + name - } - fmt.Printf("\n\tpodman machine set --rootful%s\n\n", suffix) - } - winProxyOpts := machine.WinProxyOpts{ - Name: v.Name, - IdentityPath: v.IdentityPath, - Port: v.Port, - RemoteUsername: v.RemoteUsername, - Rootful: v.Rootful, - VMType: vmtype, - } - machine.LaunchWinProxy(winProxyOpts, opts.NoInfo) - - _, _, err = v.updateTimeStamps(true) - return err -} - func obtainGlobalConfigLock() (*fileLock, error) { lockDir, err := machine.GetGlobalDataDir() if err != nil { @@ -1236,63 +630,6 @@ func obtainGlobalConfigLock() (*fileLock, error) { return lockFile(filepath.Join(lockDir, "podman-config.lck")) } -func (v *MachineVM) reassignSshPort() error { - dist := toDist(v.Name) - newPort, err := machine.AllocateMachinePort() - if err != nil { - return err - } - - success := false - defer func() { - if !success { - if err := machine.ReleaseMachinePort(newPort); err != nil { - logrus.Warnf("could not release port allocation as part of failure rollback (%d): %w", newPort, err) - } - } - }() - - // We need to prevent racing connection updates to containers.conf globally - // across all backends to prevent connection overwrites - flock, err := obtainGlobalConfigLock() - if err != nil { - return fmt.Errorf("could not obtain global lock: %w", err) - } - defer flock.unlock() - - // Write a transient invalid port, to force a retry on failure - oldPort := v.Port - v.Port = 0 - if err := v.writeConfig(); err != nil { - return err - } - - if err := machine.ReleaseMachinePort(oldPort); err != nil { - logrus.Warnf("could not release current ssh port allocation (%d): %w", oldPort, err) - } - - if err := wslInvoke(dist, "sh", "-c", fmt.Sprintf(changePort, newPort)); err != nil { - return fmt.Errorf("could not change SSH port for guest OS: %w", err) - } - - v.Port = newPort - uris, names := constructSSHUris(v) - for i := 0; i < 2; i++ { - if err := connection.ChangeConnectionURI(names[i], &uris[i]); err != nil { - return err - } - } - - // Write updated port back - if err := v.writeConfig(); err != nil { - return err - } - - success = true - - return nil -} - func IsWSLFeatureEnabled() bool { return wutil.SilentExec("wsl", "--set-default-version", "2") == nil } @@ -1368,58 +705,6 @@ func isSystemdRunning(dist string) (bool, error) { return result, nil } -func (v *MachineVM) Stop(name string, _ machine.StopOptions) error { - v.lock.Lock() - defer v.lock.Unlock() - - dist := toDist(v.Name) - - wsl, err := isWSLRunning(dist) - if err != nil { - return err - } - - sysd := false - if wsl { - sysd, err = isSystemdRunning(dist) - if err != nil { - return err - } - } - - if !wsl || !sysd { - return nil - } - - // Stop user-mode networking if enabled - if err := v.stopUserModeNetworking(dist); err != nil { - fmt.Fprintf(os.Stderr, "Could not cleanly stop user-mode networking: %s\n", err.Error()) - } - - _, _, _ = v.updateTimeStamps(true) - - if err := machine.StopWinProxy(v.Name, vmtype); err != nil { - 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.Stdin = strings.NewReader(waitTerm) - if err = cmd.Start(); err != nil { - return fmt.Errorf("executing wait command: %w", err) - } - - exitCmd := exec.Command("wsl", "-u", "root", "-d", dist, "/usr/local/bin/enterns", "systemctl", "exit", "0") - if err = exitCmd.Run(); err != nil { - return fmt.Errorf("stopping sysd: %w", err) - } - - if err = cmd.Wait(); err != nil { - return err - } - - return terminateDist(dist) -} - func terminateDist(dist string) error { cmd := exec.Command("wsl", "--terminate", dist) return cmd.Run() @@ -1430,79 +715,11 @@ func unregisterDist(dist string) error { return cmd.Run() } -func (v *MachineVM) State(bypass bool) (define.Status, error) { - if v.isRunning() { - return define.Running, nil - } - - return define.Stopped, nil -} - -//nolint:cyclop -func (v *MachineVM) Remove(name string, opts machine.RemoveOptions) (string, func() error, error) { - var files []string - - if v.isRunning() { - if !opts.Force { - return "", nil, &define.ErrVMRunningCannotDestroyed{Name: v.Name} - } - if err := v.Stop(v.Name, machine.StopOptions{}); err != nil { - return "", nil, err - } - } - - v.lock.Lock() - defer v.lock.Unlock() - - // Collect all the files that need to be destroyed - if !opts.SaveImage { - files = append(files, v.ImagePath) - } - - vmConfigDir, err := machine.GetConfDir(vmtype) - if err != nil { - return "", nil, err - } - files = append(files, filepath.Join(vmConfigDir, v.Name+".json")) - - vmDataDir, err := machine.GetDataDir(vmtype) - if err != nil { - return "", nil, err - } - files = append(files, filepath.Join(vmDataDir, "wsldist", v.Name)) - - confirmationMessage := "\nThe following files will be deleted:\n\n" - for _, msg := range files { - confirmationMessage += msg + "\n" - } - - confirmationMessage += "\n" - return confirmationMessage, func() error { - if err := connection.RemoveConnections(v.Name, v.Name+"-root"); err != nil { - logrus.Error(err) - } - if err := runCmdPassThrough("wsl", "--unregister", toDist(v.Name)); err != nil { - logrus.Error(err) - } - for _, f := range files { - if err := utils.GuardedRemoveAll(f); err != nil { - logrus.Error(err) - } - } - if err := machine.ReleaseMachinePort(v.Port); err != nil { - logrus.Warnf("could not release port allocation as part of removal (%d): %w", v.Port, err) - } - - return nil - }, nil -} - -func (v *MachineVM) isRunning() bool { - dist := toDist(v.Name) - +func isRunning(name string) (bool, error) { + dist := machine.ToDist(name) wsl, err := isWSLRunning(dist) if err != nil { - return false + return false, err } sysd := false @@ -1510,65 +727,20 @@ func (v *MachineVM) isRunning() bool { sysd, err = isSystemdRunning(dist) if err != nil { - return false + return false, err } } - return sysd + return sysd, err } -// SSH opens an interactive SSH session to the vm specified. -// Added ssh function to VM interface: pkg/machine/config/go : line 58 -func (v *MachineVM) SSH(name string, opts machine.SSHOptions) error { - if !v.isRunning() { - return fmt.Errorf("vm %q is not running.", v.Name) - } - - username := opts.Username - if username == "" { - username = v.RemoteUsername - } - - sshDestination := username + "@localhost" - port := strconv.Itoa(v.Port) - - args := []string{"-i", v.IdentityPath, "-p", port, sshDestination, - "-o", "IdentitiesOnly yes", - "-o", "UserKnownHostsFile /dev/null", - "-o", "StrictHostKeyChecking no"} - if len(opts.Args) > 0 { - args = append(args, opts.Args...) - } else { - fmt.Printf("Connecting to vm %s. To close connection, use `~.` or `exit`\n", v.Name) - } - - cmd := exec.Command("ssh", args...) - logrus.Debugf("Executing: ssh %v\n", args) - - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Stdin = os.Stdin - - return cmd.Run() -} - -func (v *MachineVM) updateTimeStamps(updateLast bool) (time.Time, time.Time, error) { - var err error - if updateLast { - v.LastUp = time.Now() - err = v.writeConfig() - } - - return v.Created, v.LastUp, err -} - -func getDiskSize(vm *MachineVM) uint64 { +func getDiskSize(name string) uint64 { vmDataDir, err := machine.GetDataDir(vmtype) if err != nil { return 0 } distDir := filepath.Join(vmDataDir, "wsldist") - disk := filepath.Join(distDir, vm.Name, "ext4.vhdx") + disk := filepath.Join(distDir, name, "ext4.vhdx") info, err := os.Stat(disk) if err != nil { return 0 @@ -1576,8 +748,8 @@ func getDiskSize(vm *MachineVM) uint64 { return uint64(info.Size()) } -func getCPUs(vm *MachineVM) (uint64, error) { - dist := toDist(vm.Name) +func getCPUs(name string) (uint64, error) { + dist := machine.ToDist(name) if run, _ := isWSLRunning(dist); !run { return 0, nil } @@ -1600,8 +772,8 @@ func getCPUs(vm *MachineVM) (uint64, error) { return uint64(ret), err } -func getMem(vm *MachineVM) (uint64, error) { - dist := toDist(vm.Name) +func getMem(name string) (uint64, error) { + dist := machine.ToDist(name) if run, _ := isWSLRunning(dist); !run { return 0, nil } @@ -1636,48 +808,9 @@ func getMem(vm *MachineVM) (uint64, error) { return total - available, err } -func (v *MachineVM) setRootful(rootful bool) error { - if err := machine.SetRootful(rootful, v.Name, v.Name+"-root"); err != nil { - return err - } - - dist := toDist(v.Name) - return v.setupPodmanDockerSock(dist, rootful) -} - -// Inspect returns verbose detail about the machine -func (v *MachineVM) Inspect() (*machine.InspectInfo, error) { - state, err := v.State(false) - if err != nil { - return nil, err - } - - connInfo := new(machine.ConnectionConfig) - machinePipe := toDist(v.Name) - connInfo.PodmanPipe = &define.VMFile{Path: `\\.\pipe\` + machinePipe} - - created, lastUp, _ := v.updateTimeStamps(state == define.Running) - return &machine.InspectInfo{ - ConfigPath: define.VMFile{Path: v.ConfigPath}, - ConnectionInfo: *connInfo, - Created: created, - Image: machine.ImageConfig{ - ImagePath: define.VMFile{Path: v.ImagePath}, - ImageStream: v.ImageStream, - }, - LastUp: lastUp, - Name: v.Name, - Resources: v.getResources(), - SSHConfig: v.SSHConfig, - State: state, - UserModeNetworking: v.UserModeNetworking, - Rootful: v.Rootful, - }, nil -} - -func (v *MachineVM) getResources() (resources vmconfigs.ResourceConfig) { - resources.CPUs, _ = getCPUs(v) - resources.Memory, _ = getMem(v) - resources.DiskSize = getDiskSize(v) +func getResources(mc *vmconfigs.MachineConfig) (resources vmconfigs.ResourceConfig) { + resources.CPUs, _ = getCPUs(mc.Name) + resources.Memory, _ = getMem(mc.Name) + resources.DiskSize = getDiskSize(mc.Name) return } diff --git a/pkg/machine/wsl/stubber.go b/pkg/machine/wsl/stubber.go new file mode 100644 index 0000000000..a3693d9c99 --- /dev/null +++ b/pkg/machine/wsl/stubber.go @@ -0,0 +1,356 @@ +//go:build windows + +package wsl + +import ( + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/containers/podman/v5/pkg/machine/ocipull" + "github.com/containers/podman/v5/pkg/machine/stdpull" + + gvproxy "github.com/containers/gvisor-tap-vsock/pkg/types" + "github.com/containers/podman/v5/pkg/machine" + "github.com/containers/podman/v5/pkg/machine/define" + "github.com/containers/podman/v5/pkg/machine/ignition" + "github.com/containers/podman/v5/pkg/machine/vmconfigs" + "github.com/sirupsen/logrus" +) + +type WSLStubber struct { + vmconfigs.WSLConfig +} + +func (w WSLStubber) CreateVM(opts define.CreateVMOpts, mc *vmconfigs.MachineConfig, _ *ignition.IgnitionBuilder) error { + var ( + err error + ) + // cleanup half-baked files if init fails at any point + callbackFuncs := machine.InitCleanup() + defer callbackFuncs.CleanIfErr(&err) + go callbackFuncs.CleanOnSignal() + mc.WSLHypervisor = new(vmconfigs.WSLConfig) + // TODO + // USB opts are unsupported in WSL. Need to account for that here + // or up the stack + // if len(opts.USBs) > 0 { + // return nil, fmt.Errorf("USB host passthrough is not supported for WSL machines") + // } + + if cont, err := checkAndInstallWSL(opts.ReExec); !cont { + appendOutputIfError(opts.ReExec, err) + return err + } + + _ = setupWslProxyEnv() + + if opts.UserModeNetworking { + if err = verifyWSLUserModeCompat(); err != nil { + return err + } + mc.WSLHypervisor.UserModeNetworking = true + } + + const prompt = "Importing operating system into WSL (this may take a few minutes on a new WSL install)..." + dist, err := provisionWSLDist(mc.Name, mc.ImagePath.GetPath(), prompt) + if err != nil { + return err + } + + unprovisionCallbackFunc := func() error { + return unprovisionWSL(mc) + } + callbackFuncs.Add(unprovisionCallbackFunc) + + if mc.WSLHypervisor.UserModeNetworking { + if err = installUserModeDist(dist, mc.ImagePath.GetPath()); err != nil { + _ = unregisterDist(dist) + return err + } + } + + fmt.Println("Configuring system...") + if err = configureSystem(mc, dist); err != nil { + return err + } + + if err = installScripts(dist); err != nil { + return err + } + + if err = createKeys(mc, dist); err != nil { + return err + } + + // recycle vm + return terminateDist(dist) +} + +func (w WSLStubber) PrepareIgnition(_ *vmconfigs.MachineConfig, _ *ignition.IgnitionBuilder) (*ignition.ReadyUnitOpts, error) { + return nil, nil +} + +func (w WSLStubber) GetHyperVisorVMs() ([]string, error) { + vms, err := getAllWSLDistros(false) + if err != nil { + return nil, err + } + wslVMs := make([]string, 0) + for name := range vms { + wslVMs = append(wslVMs, name) + } + return wslVMs, nil +} + +func (w WSLStubber) MountType() vmconfigs.VolumeMountType { + //TODO implement me + panic("implement me") +} + +func (w WSLStubber) MountVolumesToVM(mc *vmconfigs.MachineConfig, quiet bool) error { + return nil +} + +func (w WSLStubber) Remove(mc *vmconfigs.MachineConfig) ([]string, func() error, error) { + // Note: we could consider swapping the two conditionals + // below if we wanted to hard error on the wsl unregister + // of the vm + wslRemoveFunc := func() error { + if err := runCmdPassThrough("wsl", "--unregister", machine.ToDist(mc.Name)); err != nil { + logrus.Error(err) + } + return machine.ReleaseMachinePort(mc.SSH.Port) + } + + return []string{}, wslRemoveFunc, nil +} + +func (w WSLStubber) RemoveAndCleanMachines(_ *define.MachineDirs) error { + return nil +} + + +func (w WSLStubber) SetProviderAttrs(mc *vmconfigs.MachineConfig, opts define.SetOptions) error { + mc.Lock() + defer mc.Unlock() + + // TODO the check for running when setting rootful is something I have not + // seen in the other distributions. I wonder if this is true everywhere or just + // with WSL? + // TODO maybe the "rule" for set is that it must be done when the machine is + // stopped? + // if opts.Rootful != nil && v.Rootful != *opts.Rootful { + // err := v.setRootful(*opts.Rootful) + // if err != nil { + // setErrors = append(setErrors, fmt.Errorf("setting rootful option: %w", err)) + // } else { + // if v.isRunning() { + // logrus.Warn("restart is necessary for rootful change to go into effect") + // } + // v.Rootful = *opts.Rootful + // } + // } + + if opts.Rootful != nil && mc.HostUser.Rootful != *opts.Rootful { + if err := mc.SetRootful(*opts.Rootful); err != nil { + return err + } + } + + if opts.CPUs != nil { + return errors.New("changing CPUs not supported for WSL machines") + } + + if opts.Memory != nil { + return errors.New("changing memory not supported for WSL machines") + } + + // TODO USB still needs to be plumbed for all providers + // if USBs != nil { + // setErrors = append(setErrors, errors.New("changing USBs not supported for WSL machines")) + // } + + if opts.DiskSize != nil { + return errors.New("changing disk size not supported for WSL machines") + } + + if opts.UserModeNetworking != nil && mc.WSLHypervisor.UserModeNetworking != *opts.UserModeNetworking { + if running, _ := isRunning(mc.Name); running { + return errors.New("user-mode networking can only be changed when the machine is not running") + } + + dist := machine.ToDist(mc.Name) + if err := changeDistUserModeNetworking(dist, mc.SSH.RemoteUsername, mc.ImagePath.GetPath(), *opts.UserModeNetworking); err != nil { + return fmt.Errorf("failure changing state of user-mode networking setting", err) + } + + mc.WSLHypervisor.UserModeNetworking = *opts.UserModeNetworking + } + + return nil +} + +func (w WSLStubber) StartNetworking(mc *vmconfigs.MachineConfig, cmd *gvproxy.GvproxyCommand) error { + // Startup user-mode networking if enabled + if mc.WSLHypervisor.UserModeNetworking { + return startUserModeNetworking(mc) + } + return nil +} + +func (w WSLStubber) UserModeNetworkEnabled(mc *vmconfigs.MachineConfig) bool { + return mc.WSLHypervisor.UserModeNetworking +} + +func (w WSLStubber) UseProviderNetworkSetup() bool { + return true +} + +func (w WSLStubber) RequireExclusiveActive() bool { + return false +} + +func (w WSLStubber) PostStartNetworking(mc *vmconfigs.MachineConfig, noInfo bool) error { + winProxyOpts := machine.WinProxyOpts{ + Name: mc.Name, + IdentityPath: mc.SSH.IdentityPath, + Port: mc.SSH.Port, + RemoteUsername: mc.SSH.RemoteUsername, + Rootful: mc.HostUser.Rootful, + VMType: w.VMType(), + } + machine.LaunchWinProxy(winProxyOpts, noInfo) + + return nil +} + +func (w WSLStubber) StartVM(mc *vmconfigs.MachineConfig) (func() error, func() error, error) { + useProxy := setupWslProxyEnv() + dist := machine.ToDist(mc.Name) + + // TODO Quiet is hard set to false: follow up + if err := configureProxy(dist, useProxy, false); err != nil { + return nil, nil, err + } + + // TODO The original code checked to see if the SSH port was actually open and re-assigned if it was + // we could consider this but it should be higher up the stack + // if !machine.IsLocalPortAvailable(v.Port) { + // logrus.Warnf("SSH port conflict detected, reassigning a new port") + // if err := v.reassignSshPort(); err != nil { + // return err + // } + // } + + err := wslInvoke(dist, "/root/bootstrap") + if err != nil { + err = fmt.Errorf("the WSL bootstrap script failed: %w", err) + } + + readyFunc := func() error { + return nil + } + + return nil, readyFunc, err +} + +func (w WSLStubber) State(mc *vmconfigs.MachineConfig, bypass bool) (define.Status, error) { + running, err := isRunning(mc.Name) + if err != nil { + return "", err + } + if running { + return define.Running, nil + } + return define.Stopped, nil +} + +func (w WSLStubber) StopVM(mc *vmconfigs.MachineConfig, hardStop bool) error { + var ( + err error + ) + mc.Lock() + defer mc.Unlock() + + // recheck after lock + if running, err := isRunning(mc.Name); !running { + return err + } + + dist := machine.ToDist(mc.Name) + + // Stop user-mode networking if enabled + if err := stopUserModeNetworking(mc); err != nil { + fmt.Fprintf(os.Stderr, "Could not cleanly stop user-mode networking: %s\n", err.Error()) + } + + if err := machine.StopWinProxy(mc.Name, vmtype); err != nil { + 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.Stdin = strings.NewReader(waitTerm) + if err = cmd.Start(); err != nil { + return fmt.Errorf("executing wait command: %w", err) + } + + exitCmd := exec.Command("wsl", "-u", "root", "-d", dist, "/usr/local/bin/enterns", "systemctl", "exit", "0") + if err = exitCmd.Run(); err != nil { + return fmt.Errorf("stopping sysd: %w", err) + } + + if err = cmd.Wait(); err != nil { + return err + } + + return terminateDist(dist) +} + +func (w WSLStubber) StopHostNetworking(mc *vmconfigs.MachineConfig, vmType define.VMType) error { + return stopUserModeNetworking(mc) +} + +func (w WSLStubber) VMType() define.VMType { + return define.WSLVirt +} + +func (w WSLStubber) GetDisk(_ string, dirs *define.MachineDirs, mc *vmconfigs.MachineConfig) error { + var ( + myDisk ocipull.Disker + ) + + // check github for the latest version of the WSL dist + downloadURL, downloadVersion, _, _, err := GetFedoraDownloadForWSL() + if err != nil { + return err + } + + // we now save the "cached" rootfs in the form of "v-rootfs.tar.xz" + // i.e.v39.0.31-rootfs.tar.xz + versionedBase := fmt.Sprintf("%s-%s", downloadVersion, filepath.Base(downloadURL.Path)) + + // TODO we need a mechanism for "flushing" old cache files + cachedFile, err := dirs.DataDir.AppendToNewVMFile(versionedBase, nil) + if err != nil { + return err + } + + // if we find the same file cached (determined by filename only), then dont pull + if _, err = os.Stat(cachedFile.GetPath()); err == nil { + logrus.Debugf("%q already exists locally", cachedFile.GetPath()) + myDisk, err = stdpull.NewStdDiskPull(cachedFile.GetPath(), mc.ImagePath) + } else { + // no cached file + myDisk, err = stdpull.NewDiskFromURL(downloadURL.String(), mc.ImagePath, dirs.DataDir, &versionedBase) + } + if err != nil { + return err + } + // up until now, nothing has really happened + // pull if needed and decompress to image location + return myDisk.Get() +} diff --git a/pkg/machine/wsl/usermodenet.go b/pkg/machine/wsl/usermodenet.go index bb2f6528ad..1c7ef4eeda 100644 --- a/pkg/machine/wsl/usermodenet.go +++ b/pkg/machine/wsl/usermodenet.go @@ -5,6 +5,7 @@ package wsl import ( "errors" "fmt" + "github.com/containers/podman/v5/pkg/machine/vmconfigs" "os" "os/exec" "path/filepath" @@ -69,8 +70,8 @@ func verifyWSLUserModeCompat() error { prefix) } -func (v *MachineVM) startUserModeNetworking() error { - if !v.UserModeNetworking { +func startUserModeNetworking(mc *vmconfigs.MachineConfig) error { + if !mc.WSLHypervisor.UserModeNetworking { return nil } @@ -79,7 +80,7 @@ func (v *MachineVM) startUserModeNetworking() error { return fmt.Errorf("could not locate %s, which is necessary for user-mode networking, please reinstall", gvProxy) } - flock, err := v.obtainUserModeNetLock() + flock, err := obtainUserModeNetLock() if err != nil { return err } @@ -93,17 +94,17 @@ func (v *MachineVM) startUserModeNetworking() error { // Start or reuse if !running { - if err := v.launchUserModeNetDist(exe); err != nil { + if err := launchUserModeNetDist(exe); err != nil { return err } } - if err := createUserModeResolvConf(toDist(v.Name)); err != nil { + if err := createUserModeResolvConf(machine.ToDist(mc.Name)); err != nil { return err } // Register in-use - err = v.addUserModeNetEntry() + err = addUserModeNetEntry(mc) if err != nil { return err } @@ -111,23 +112,23 @@ func (v *MachineVM) startUserModeNetworking() error { return nil } -func (v *MachineVM) stopUserModeNetworking(dist string) error { - if !v.UserModeNetworking { +func stopUserModeNetworking(mc *vmconfigs.MachineConfig) error { + if !mc.WSLHypervisor.UserModeNetworking { return nil } - flock, err := v.obtainUserModeNetLock() + flock, err := obtainUserModeNetLock() if err != nil { return err } defer flock.unlock() - err = v.removeUserModeNetEntry() + err = removeUserModeNetEntry(mc.Name) if err != nil { return err } - count, err := v.cleanupAndCountNetEntries() + count, err := cleanupAndCountNetEntries() if err != nil { return err } @@ -159,7 +160,7 @@ func isGvProxyVMRunning() bool { return wslInvoke(userModeDist, "bash", "-c", "ps -eo args | grep -q -m1 ^/usr/local/bin/vm || exit 42") == nil } -func (v *MachineVM) launchUserModeNetDist(exeFile string) error { +func launchUserModeNetDist(exeFile string) error { fmt.Println("Starting user-mode networking...") exe, err := specgen.ConvertWinMountPath(exeFile) @@ -220,7 +221,7 @@ func createUserModeResolvConf(dist string) error { return err } -func (v *MachineVM) getUserModeNetDir() (string, error) { +func getUserModeNetDir() (string, error) { vmDataDir, err := machine.GetDataDir(vmtype) if err != nil { return "", err @@ -234,8 +235,8 @@ func (v *MachineVM) getUserModeNetDir() (string, error) { return dir, nil } -func (v *MachineVM) getUserModeNetEntriesDir() (string, error) { - netDir, err := v.getUserModeNetDir() +func getUserModeNetEntriesDir() (string, error) { + netDir, err := getUserModeNetDir() if err != nil { return "", err } @@ -248,13 +249,13 @@ func (v *MachineVM) getUserModeNetEntriesDir() (string, error) { return dir, nil } -func (v *MachineVM) addUserModeNetEntry() error { - entriesDir, err := v.getUserModeNetEntriesDir() +func addUserModeNetEntry(mc *vmconfigs.MachineConfig) error { + entriesDir, err := getUserModeNetEntriesDir() if err != nil { return err } - path := filepath.Join(entriesDir, toDist(v.Name)) + path := filepath.Join(entriesDir, machine.ToDist(mc.Name)) file, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) if err != nil { return fmt.Errorf("could not add user-mode networking registration: %w", err) @@ -263,18 +264,18 @@ func (v *MachineVM) addUserModeNetEntry() error { return nil } -func (v *MachineVM) removeUserModeNetEntry() error { - entriesDir, err := v.getUserModeNetEntriesDir() +func removeUserModeNetEntry(name string) error { + entriesDir, err := getUserModeNetEntriesDir() if err != nil { return err } - path := filepath.Join(entriesDir, toDist(v.Name)) + path := filepath.Join(entriesDir, machine.ToDist(name)) return os.Remove(path) } -func (v *MachineVM) cleanupAndCountNetEntries() (uint, error) { - entriesDir, err := v.getUserModeNetEntriesDir() +func cleanupAndCountNetEntries() (uint, error) { + entriesDir, err := getUserModeNetEntriesDir() if err != nil { return 0, err } @@ -302,8 +303,8 @@ func (v *MachineVM) cleanupAndCountNetEntries() (uint, error) { return count, nil } -func (v *MachineVM) obtainUserModeNetLock() (*fileLock, error) { - dir, err := v.getUserModeNetDir() +func obtainUserModeNetLock() (*fileLock, error) { + dir, err := getUserModeNetDir() if err != nil { return nil, err diff --git a/vendor/github.com/containers/winquit/LICENSE b/vendor/github.com/containers/winquit/LICENSE new file mode 100644 index 0000000000..d645695673 --- /dev/null +++ b/vendor/github.com/containers/winquit/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/containers/winquit/pkg/winquit/channels_windows.go b/vendor/github.com/containers/winquit/pkg/winquit/channels_windows.go new file mode 100644 index 0000000000..2d7e48a4a4 --- /dev/null +++ b/vendor/github.com/containers/winquit/pkg/winquit/channels_windows.go @@ -0,0 +1,50 @@ +package winquit + +import ( + "os" + "syscall" +) + +type baseChannelType interface { + getKey() any + notifyNonBlocking() + notifyBlocking() +} + +type boolChannelType struct { + channel chan bool +} + +func (b *boolChannelType) getKey() any { + return b.channel +} + +func (b *boolChannelType) notifyNonBlocking() { + select { + case b.channel <- true: + default: + } +} + +func (s *boolChannelType) notifyBlocking() { + s.channel <- true +} + +type sigChannelType struct { + channel chan os.Signal +} + +func (s *sigChannelType) getKey() any { + return s.channel +} + +func (s *sigChannelType) notifyNonBlocking() { + select { + case s.channel <- syscall.SIGTERM: + default: + } +} + +func (s *sigChannelType) notifyBlocking() { + s.channel <- syscall.SIGTERM +} diff --git a/vendor/github.com/containers/winquit/pkg/winquit/client.go b/vendor/github.com/containers/winquit/pkg/winquit/client.go new file mode 100644 index 0000000000..95b813c0ba --- /dev/null +++ b/vendor/github.com/containers/winquit/pkg/winquit/client.go @@ -0,0 +1,31 @@ +package winquit + +import ( + "time" +) + +// RequestQuit sends a Windows quit notification to the specified process id. +// Since communication is performed over the Win32 GUI messaging facilities, +// console applications may not respond, as they require special handling to do +// so. Additionally incorrectly written or buggy GUI applications may not listen +// or respond appropriately to the event. +// +// All applications, console or GUI, which use the notification mechanisms +// provided by this package (NotifyOnQuit, SimulateSigTermOnQuit) will react +// appropriately to the event sent by RequestQuit. +// +// Callers must have appropriate security permissions, otherwise an error will +// be returned. See the notes in the package documentation for more details. +func RequestQuit(pid int) error { + return requestQuit(pid) +} + +// QuitProcess first sends a Windows quit notification to the specified process id, +// and waits, up the amount of time passed in the waitNicely argument, for it to +// exit. If the process does not exit in time, it is forcefully terminated. +// +// Callers must have appropriate security permissions, otherwise an error will +// be returned. See the notes in the package documentation for more details. +func QuitProcess(pid int, waitNicely time.Duration) error { + return quitProcess(pid, waitNicely) +} diff --git a/vendor/github.com/containers/winquit/pkg/winquit/client_unsupported.go b/vendor/github.com/containers/winquit/pkg/winquit/client_unsupported.go new file mode 100644 index 0000000000..8aa5ed06e1 --- /dev/null +++ b/vendor/github.com/containers/winquit/pkg/winquit/client_unsupported.go @@ -0,0 +1,17 @@ +//go:build !windows +// +build !windows + +package winquit + +import ( + "fmt" + "time" +) + +func requestQuit(pid int) error { + return fmt.Errorf("not implemented on non-Windows") +} + +func quitProcess(pid int, waitNicely time.Duration) error { + return fmt.Errorf("not implemented on non-Windows") +} diff --git a/vendor/github.com/containers/winquit/pkg/winquit/client_windows.go b/vendor/github.com/containers/winquit/pkg/winquit/client_windows.go new file mode 100644 index 0000000000..e03919b4cc --- /dev/null +++ b/vendor/github.com/containers/winquit/pkg/winquit/client_windows.go @@ -0,0 +1,47 @@ +package winquit + +import ( + "os" + "time" + + "github.com/containers/winquit/pkg/winquit/win32" + "github.com/sirupsen/logrus" +) + +func requestQuit(pid int) error { + threads, err := win32.GetProcThreads(uint32(pid)) + if err != nil { + return err + } + + for _, thread := range threads { + logrus.Debugf("Closing windows on thread %d", thread) + win32.CloseThreadWindows(uint32(thread)) + } + + return nil +} + +func quitProcess(pid int, waitNicely time.Duration) error { + _ = RequestQuit(pid) + + proc, err := os.FindProcess(pid) + if err != nil { + return nil + } + + done := make(chan bool) + + go func() { + proc.Wait() + done <- true + }() + + select { + case <-done: + return nil + case <-time.After(waitNicely): + } + + return proc.Kill() +} diff --git a/vendor/github.com/containers/winquit/pkg/winquit/doc.go b/vendor/github.com/containers/winquit/pkg/winquit/doc.go new file mode 100644 index 0000000000..079794cb4e --- /dev/null +++ b/vendor/github.com/containers/winquit/pkg/winquit/doc.go @@ -0,0 +1,135 @@ +// Package winquit supports graceful shutdown of Windows applications through +// the sending and receiving of Windows quit events on Win32 message queues. +// This allows golang applications to implement behavior comparable to SIGTERM +// signal handling on UNIX derived systems. Additionally, it supports the +// graceful shutdown mechanism employed by Windows system tools, such as +// taskkill. See the "How it works" section for more details. +// +// To aid application portability, and provide familiarity, the API follows a +// similar convention and approach as the os.signal package. Additionally, the +// SimulateSigTermOnQuit function supports reuse of the same underlying channel, +// supporting the blending of os.signal and winquit together (a subset of +// signals provided by os.signal are still relevant and desirable on Windows, +// for example, break handling in console applications). +// +// # Simple server example +// +// The following example demonstrates usage of NotifyOnQuit() to wait for a +// windows quit event before shutting down: +// +// func server() { +// fmt.Println("Starting server") +// +// // Create a channel, and register it +// done := make(chan bool, 1) +// winquit.NotifyOnQuit(done) +// +// // Wait until we receive a quit event +// <-done +// +// fmt.Println("Shutting down") +// // Perform cleanup tasks +// } +// +// # Blended signal example +// +// The following example demonstrates usage of SimulateSigTermOnQuit() in +// concert with signal.Notify(): +// +// func server() { +// fmt.Println("Starting server") +// +// // Create a channel, and register it +// done := make(chan os.Signal, 1) +// +// // Wait on console interrupt events +// signal.Notify(done, syscall.SIGINT) +// +// // Simulate SIGTERM when a quit occurs +// winquit.SimulateSigTermOnQuit(done) +// +// // Wait until we receive a signal or quit event +// <-done +// +// fmt.Println("Shutting down") +// // Perform cleanup tasks +// } +// +// # Client example +// +// The following example demonstrates how an application can ask or +// force other windows programs to quit: +// +// func client() { +// // Ask nicely for program "one" to quit. This request may not +// // be honored if its a console application, or if the program +// // is hung +// if err := winquit.RequestQuit(pidOne); err != nil { +// fmt.Printf("error sending quit request, %s", err.Error()) +// } +// +// // Force program "two" to quit, but give it 20 seconds to +// // perform any cleanup tasks and quit on it's own +// timeout := time.Second * 20 +// if err := winquit.QuitProcess(pidTwo, timeout); err != nil { +// fmt.Printf("error killing process, %s", err.Error()) +// } +// } +// +// # How it works +// +// Windows GUI applications consist of multiple components (and windows) which +// intercommunicate with events over per-thread message queues and/or direct +// event handoff to window procedures for cross-thread communication. +// Additionally, GUI applications can use the same mechanism to communicate with +// windows and threads owned by other applications, including common desktop +// components. +// +// winquit utilizes this mechanism by creating a standard win32 message loop +// thread and registering a non-visible window to relay a quit message (WM_QUIT) +// in the event of a window close event. WM_CLOSE is sent by Windows in response +// to certain system events, or by other requesting applications. For example, +// the system provided taskkill.exe (similar to the kill command on Unix), works +// by iterating all windows on the system, and sending a WM_CLOSE when the +// process owner matches the specified pid. Note that, unlike UNIX/X11 style +// systems, on Windows the graphical APIs are built-in and accessible to all +// win32 applications, including console based applications. Therefore, the APIs +// provided by winquit *do not* require compilation as a windowsgui app to +// effectively use them. +// +// winquit also provides APIs to trigger a quit of another process using a +// WM_CLOSE event, although in a more efficient manner than taskkill.exe. It +// instead captures a thread snapshot of the target process (effectively a +// memory read on Windows), and enumerates each thread's associated Windows, and +// sending the event to each. In addition to supporting a graceful close of any +// Windows application, which may have multiple message loops, this approach +// also obviates the need for cumbersome approaches to lock code to the main +// thread of the application. The message loop used by winquit does not care +// which thread the golang runtime internally designates. Note that winquit +// purposefully relays through a thread's windows as opposed to posting directly +// to each thread's message queue, since the former is more likely to be +// expected by an application, and it ensures all window procedures have an +// opportunity to perform cleanup work not associated with the thread's message +// loop. +// +// # Limitations +// +// This API is only implemented on Windows platforms. Non-operational stubs +// are provided for compilation purposes. +// +// In addition to requiring appropriate security permissions (typically a user +// can only send events to other applications ran by the same user), Windows +// also restricts inter-app messaging operations to programs running in the same +// user logon session. While logons migrate between RDP and console sessions, +// non-graphical logins (e.g sshd) typically create a logon per connection. For +// this reason, tools like taskkill and winquit are normally disallowed from +// crossing this boundary. Therefore, a user will not be able to gracefully stop +// applications between ssh/winrm sessions, and in between ssh and graphical +// logons. However, the typical user use-case of logging into Windows and +// running multiple applications and terminals will work fine. Additionally, +// multiple back-grounded processes in the same ssh session will be able to +// communicate. Finally, it's possible to bypass this limitation by executing +// code under the system user using the SeTcbPrivilege. The psexec tool does +// exactly this, and can additionally be used as a workaround to this +// limitation. +package winquit diff --git a/vendor/github.com/containers/winquit/pkg/winquit/server.go b/vendor/github.com/containers/winquit/pkg/winquit/server.go new file mode 100644 index 0000000000..e50ad1a96b --- /dev/null +++ b/vendor/github.com/containers/winquit/pkg/winquit/server.go @@ -0,0 +1,45 @@ +package winquit + +import ( + "os" +) + +// NotifyOnQuit relays a Windows quit notification to the boolean done channel. +// This is a one-shot operation (will only be delivered once), however multiple +// channels may be registered. Each registered channel is sent one copy of the +// same one-shot value. +// +// This function is a no-op on non-Windows platforms. While the call will +// succeed, no notifications will be delivered to the passed channel. Each +// channel will only ever receive a "true" value. +// +// It is recommended that registered channels establish a buffer of 1, since +// values are sent non-blocking. Blocking redelivery may be attempted to reduce +// the chance of bugs; however, it should not be relied upon. +// +// If this function is called after a Windows quit notification has occurred, it +// will immediately deliver a "true" value. +func NotifyOnQuit(done chan bool) { + notifyOnQuit(done) +} + +// SimulateSigTermOnQuit relays a Windows quit notification following the same +// semantics as NotifyOnQuit; however, instead of a boolean message value, this +// function will send a SIGTERM signal to the passed channel. +// +// This function allows for the reuse of the same underlying channel used with +// in a separate os.signal.Notify method call. +func SimulateSigTermOnQuit(handler chan os.Signal) { + simulateSigTermOnQuit(handler) +} + +// Returns the thread id of the message loop thread created by winquit, or "0" +// if one is not running. The latter indicates a mistake, as this function +// should only be called after a call to one of the _OnQuit functions. +// +// In most cases this method should not be necessary, as RequestQuit and +// QuitProcess do not require it. It is primarily provided to enable legacy +// patterns that serialize the thread id for later direct signaling. +func GetCurrentMessageLoopThreadId() uint32 { + return getCurrentMessageLoopThreadId() +} diff --git a/vendor/github.com/containers/winquit/pkg/winquit/server_unsupported.go b/vendor/github.com/containers/winquit/pkg/winquit/server_unsupported.go new file mode 100644 index 0000000000..e5013c088e --- /dev/null +++ b/vendor/github.com/containers/winquit/pkg/winquit/server_unsupported.go @@ -0,0 +1,18 @@ +//go:build !windows +// +build !windows + +package winquit + +import ( + "os" +) + +func notifyOnQuit(done chan bool) { +} + +func simulateSigTermOnQuit(handler chan os.Signal) { +} + +func getCurrentMessageLoopThreadId() uint32 { + return 0 +} diff --git a/vendor/github.com/containers/winquit/pkg/winquit/server_windows.go b/vendor/github.com/containers/winquit/pkg/winquit/server_windows.go new file mode 100644 index 0000000000..4309319bf4 --- /dev/null +++ b/vendor/github.com/containers/winquit/pkg/winquit/server_windows.go @@ -0,0 +1,147 @@ +package winquit + +import ( + "os" + "path/filepath" + "runtime" + "strings" + "sync" + "syscall" + + "github.com/containers/winquit/pkg/winquit/win32" + "github.com/sirupsen/logrus" + "golang.org/x/sys/windows" +) + +type receiversType struct { + sync.Mutex + + result bool + channels map[any]baseChannelType +} + +var ( + receivers *receiversType = &receiversType{ + channels: make(map[any]baseChannelType), + } + + loopInit sync.Once + loopTid uint32 +) + +func (r *receiversType) add(channel baseChannelType) { + r.Lock() + defer r.Unlock() + + if _, ok := r.channels[channel.getKey()]; ok { + return + } + + if r.result { + go func() { + channel.notifyBlocking() + }() + return + } + + r.channels[channel.getKey()] = channel +} + +func (r *receiversType) notifyAll() { + r.Lock() + defer r.Unlock() + r.result = true + for _, channel := range r.channels { + channel.notifyNonBlocking() + delete(r.channels, channel.getKey()) + } + for _, channel := range r.channels { + channel.notifyBlocking() + delete(r.channels, channel) + } +} + +func initLoop() { + loopInit.Do(func() { + go messageLoop() + }) +} + +func notifyOnQuit(done chan bool) { + receivers.add(&boolChannelType{done}) + initLoop() +} + +func simulateSigTermOnQuit(handler chan os.Signal) { + receivers.add(&sigChannelType{handler}) + initLoop() +} + +func getCurrentMessageLoopThreadId() uint32 { + return loopTid +} + +func messageLoop() { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + loopTid = windows.GetCurrentThreadId() + registerDummyWindow() + + logrus.Debug("Entering loop for quit") + for { + ret, msg, err := win32.GetMessage(0, 0, 0) + if err != nil { + logrus.Debugf("Error receiving win32 message, %s", err.Error()) + continue + } + if ret == 0 { + logrus.Debug("Received QUIT notification") + receivers.notifyAll() + + return + } + logrus.Debugf("Unhandled message: %d", msg.Message) + win32.TranslateMessage(msg) + win32.DispatchMessage(msg) + } +} + +func getAppName() (string, error) { + exeName, err := os.Executable() + if err != nil { + return "", err + } + suffix := filepath.Ext(exeName) + return strings.TrimSuffix(filepath.Base(exeName), suffix), nil +} + +func registerDummyWindow() error { + var app syscall.Handle + var err error + + app, err = win32.GetModuleHandle("") + if err != nil { + return err + } + + appName, err := getAppName() + if err != nil { + return err + } + + className := appName + "-rclass" + winName := appName + "-root" + + _, err = win32.RegisterDummyWinClass(className, app) + if err != nil { + return err + } + + _, err = win32.CreateDummyWindow(winName, className, app) + if err != nil { + return err + } + + return nil +} diff --git a/vendor/github.com/containers/winquit/pkg/winquit/win32/common.go b/vendor/github.com/containers/winquit/pkg/winquit/win32/common.go new file mode 100644 index 0000000000..dbb13657e4 --- /dev/null +++ b/vendor/github.com/containers/winquit/pkg/winquit/win32/common.go @@ -0,0 +1,17 @@ +//go:build windows +// +build windows + +package win32 + +import ( + "syscall" +) + +const ( + ERROR_NO_MORE_ITEMS = 259 +) + +var ( + kernel32 = syscall.NewLazyDLL("kernel32.dll") + user32 = syscall.NewLazyDLL("user32.dll") +) diff --git a/vendor/github.com/containers/winquit/pkg/winquit/win32/common_unsupported.go b/vendor/github.com/containers/winquit/pkg/winquit/win32/common_unsupported.go new file mode 100644 index 0000000000..705e6b3129 --- /dev/null +++ b/vendor/github.com/containers/winquit/pkg/winquit/win32/common_unsupported.go @@ -0,0 +1,4 @@ +//go:build !windows +// +build !windows + +package win32 diff --git a/vendor/github.com/containers/winquit/pkg/winquit/win32/msg.go b/vendor/github.com/containers/winquit/pkg/winquit/win32/msg.go new file mode 100644 index 0000000000..f6063a5e12 --- /dev/null +++ b/vendor/github.com/containers/winquit/pkg/winquit/win32/msg.go @@ -0,0 +1,87 @@ +//go:build windows +// +build windows + +package win32 + +import ( + "syscall" + "unsafe" +) + +type MSG struct { + HWnd uintptr + Message uint32 + WParam uintptr + LParam uintptr + Time uint32 + Pt struct{ X, Y int32 } +} + +const ( + WM_QUIT = 0x12 + WM_DESTROY = 0x02 + WM_CLOSE = 0x10 +) + +var ( + postQuitMessage = user32.NewProc("PostQuitMessage") + procGetMessage = user32.NewProc("GetMessageW") + procTranslateMessage = user32.NewProc("TranslateMessage") + procDispatchMessage = user32.NewProc("DispatchMessageW") + procSendMessage = user32.NewProc("SendMessageW") +) + +func TranslateMessage(msg *MSG) bool { + ret, _, _ := + procTranslateMessage.Call( // BOOL TranslateMessage() + uintptr(unsafe.Pointer(msg)), // [in] const MSG *lpMsg + ) + + return ret != 0 + +} + +func DispatchMessage(msg *MSG) uintptr { + ret, _, _ := + procDispatchMessage.Call( // LRESULT DispatchMessage() + uintptr(unsafe.Pointer(msg)), // [in] const MSG *lpMsg + ) + + return ret +} + +func SendMessage(handle syscall.Handle, message uint, wparm uintptr, lparam uintptr) uintptr { + ret, _, _ := + procSendMessage.Call( // LRESULT SendMessage() + uintptr(handle), // [in] HWND hWnd + uintptr(message), // [in] UINT Msg + wparm, // [in] WPARAM wParam + lparam, // [in] LPARAM lParam + ) + + return ret +} + +func PostQuitMessage(code int) { + _, _, _ = + postQuitMessage.Call( // void PostQuitMessage() + uintptr(code), // [in] int nExitCode + ) +} + +func GetMessage(handle syscall.Handle, int, max int) (int32, *MSG, error) { + var msg MSG + ret, _, err := + procGetMessage.Call( // // BOOL GetMessage() + uintptr(unsafe.Pointer(&msg)), // [out] LPMSG lpMsg, + uintptr(handle), // [in, optional] HWND hWnd, + 0, // [in] UINT wMsgFilterMin, + 0, // [in] UINT wMsgFilterMax + ) + + if int32(ret) == -1 { + return 0, nil, err + } + + return int32(ret), &msg, nil +} diff --git a/vendor/github.com/containers/winquit/pkg/winquit/win32/proc.go b/vendor/github.com/containers/winquit/pkg/winquit/win32/proc.go new file mode 100644 index 0000000000..6f7ccfc5a5 --- /dev/null +++ b/vendor/github.com/containers/winquit/pkg/winquit/win32/proc.go @@ -0,0 +1,59 @@ +//go:build windows +// +build windows + +package win32 + +import ( + "fmt" + "syscall" +) + +const ( + MAXIMUM_ALLOWED = 0x02000000 +) + +var ( + procOpenProcess = kernel32.NewProc("OpenProcess") + procCloseHandle = kernel32.NewProc("CloseHandle") + procGetModuleHandle = kernel32.NewProc("GetModuleHandleW") +) + +func OpenProcess(pid uint32) (syscall.Handle, error) { + ret, _, err := + procOpenProcess.Call( // HANDLE OpenProcess() + MAXIMUM_ALLOWED, // [in] DWORD dwDesiredAccess, + 0, // [in] BOOL bInheritHandle, + uintptr(pid), // [in] DWORD dwProcessId + ) + + if ret == 0 { + return 0, err + } + + return syscall.Handle(ret), nil +} + +func CloseHandle(handle syscall.Handle) error { + ret, _, err := + procCloseHandle.Call( // BOOL CloseHandle() + uintptr(handle), // [in] HANDLE hObject + ) + if ret != 0 { + return fmt.Errorf("error closing handle: %w", err) + } + + return nil +} + +func GetProcThreads(pid uint32) ([]uint, error) { + process, err := OpenProcess(pid) + if err != nil { + return nil, err + } + + defer func() { + _ = CloseHandle(process) + }() + + return GetProcThreadIds(process) +} diff --git a/vendor/github.com/containers/winquit/pkg/winquit/win32/pss.go b/vendor/github.com/containers/winquit/pkg/winquit/win32/pss.go new file mode 100644 index 0000000000..bd03959a5f --- /dev/null +++ b/vendor/github.com/containers/winquit/pkg/winquit/win32/pss.go @@ -0,0 +1,160 @@ +//go:build windows +// +build windows + +package win32 + +import ( + "fmt" + "syscall" + "unsafe" +) + +type PSS_THREAD_ENTRY struct { + ExitStatus uint32 + TebBaseAddress uintptr + ProcessId uint32 + ThreadId uint32 + AffinityMask uintptr + Priority int32 + BasePriority int32 + LastSyscallFirstArgument uintptr + LastSyscallNumber uint16 + CreateTime uint64 + ExitTime uint64 + KernelTime uint64 + UserTime uint64 + Win32StartAddress uintptr + CaptureTime uint64 + Flags uint32 + SuspendCount uint16 + SizeOfContextRecord uint16 + ContextRecord uintptr +} + +const ( + PSS_CAPTURE_THREADS = 0x00000080 + PSS_WALK_THREADS = 3 + PSS_QUERY_THREAD_INFORMATION = 5 +) + +var ( + procPssCaptureSnapshot = kernel32.NewProc("PssCaptureSnapshot") + procPssFreeSnapshot = kernel32.NewProc("PssFreeSnapshot") + procPssWalkMarkerCreate = kernel32.NewProc("PssWalkMarkerCreate") + procPssWalkMarkerFree = kernel32.NewProc("PssWalkMarkerFree") + procPssWalkSnapshot = kernel32.NewProc("PssWalkSnapshot") +) + +func PssCaptureSnapshot(process syscall.Handle, flags int32, contextFlags int32) (syscall.Handle, error) { + var snapshot syscall.Handle + ret, _, err := + procPssCaptureSnapshot.Call( // DWORD PssCaptureSnapshot() + uintptr(process), // [in] HANDLE ProcessHandle, + uintptr(flags), // [in] PSS_CAPTURE_FLAGS CaptureFlags, + uintptr(contextFlags), // [in, optional] DWORD ThreadContextFlags, + uintptr(unsafe.Pointer(&snapshot)), // [out] HPSS *SnapshotHandle + ) + + if ret != 0 { + return 0, err + } + + return snapshot, nil +} + +func PssFreeSnapshot(process syscall.Handle, snapshot syscall.Handle) error { + ret, _, _ := + procPssFreeSnapshot.Call( // DWORD PssFreeSnapshot() + uintptr(process), // [in] HANDLE ProcessHandle, + uintptr(snapshot), // [in] HPSS SnapshotHandle + ) + if ret != 0 { + return fmt.Errorf("error freeing snapshot: %d", ret) + } + + return nil +} + +func PssWalkMarkerCreate() (syscall.Handle, error) { + var walkptr uintptr + + ret, _, _ := + procPssWalkMarkerCreate.Call( // // DWORD PssWalkMarkerCreate() + 0, // [in, optional] PSS_ALLOCATOR const *Allocator + uintptr(unsafe.Pointer(&walkptr)), // [out] HPSSWALK *WalkMarkerHandle + ) + if ret != 0 { + return 0, fmt.Errorf("error creating process walker mark: %d", ret) + } + + return syscall.Handle(walkptr), nil +} + +func PssWalkMarkerFree(handle syscall.Handle) error { + ret, _, _ := + procPssWalkMarkerFree.Call( // DWORD PssWalkMarkerFree() + uintptr(handle), // [in] HPSSWALK WalkMarkerHandle + ) + if ret != 0 { + return fmt.Errorf("error freeing process walker mark: %d", ret) + } + + return nil +} + +func PssWalkThreadSnapshot(snapshot syscall.Handle, marker syscall.Handle) (*PSS_THREAD_ENTRY, error) { + var thread PSS_THREAD_ENTRY + ret, _, err := + procPssWalkSnapshot.Call( // // DWORD PssWalkSnapshot() + uintptr(snapshot), // [in] HPSS SnapshotHandle, + PSS_WALK_THREADS, // [in] PSS_WALK_INFORMATION_CLASS InformationClass, + uintptr(marker), // [in] HPSSWALK WalkMarkerHandle, + uintptr(unsafe.Pointer(&thread)), // [out] void *Buffer, + unsafe.Sizeof(thread), // [in] DWORD BufferLength + ) + + if ret == ERROR_NO_MORE_ITEMS { + return nil, nil + } + + if ret != 0 { + return nil, fmt.Errorf("error waling thread snapshot: %d (%w)", ret, err) + } + + return &thread, nil +} + +func GetProcThreadIds(process syscall.Handle) ([]uint, error) { + snapshot, err := PssCaptureSnapshot(process, PSS_CAPTURE_THREADS, 0) + if err != nil { + return nil, err + } + defer func() { + _ = PssFreeSnapshot(process, snapshot) + }() + + marker, err := PssWalkMarkerCreate() + if err != nil { + return nil, err + } + + defer func() { + _ = PssWalkMarkerFree(marker) + }() + + var threads []uint + + for { + thread, err := PssWalkThreadSnapshot(snapshot, marker) + if err != nil { + return nil, err + } + if thread == nil { + break + } + + threads = append(threads, uint(thread.ThreadId)) + } + + return threads, nil +} diff --git a/vendor/github.com/containers/winquit/pkg/winquit/win32/win.go b/vendor/github.com/containers/winquit/pkg/winquit/win32/win.go new file mode 100644 index 0000000000..b243b0be8d --- /dev/null +++ b/vendor/github.com/containers/winquit/pkg/winquit/win32/win.go @@ -0,0 +1,162 @@ +//go:build windows +// +build windows + +package win32 + +import ( + "fmt" + "syscall" + "unsafe" +) + +type WNDCLASSEX struct { + cbSize uint32 + style uint32 + lpfnWndProc uintptr + cbClsExtra int32 + cbWndExtra int32 + hInstance syscall.Handle + hIcon syscall.Handle + hCursor syscall.Handle + hbrBackground syscall.Handle + menuName *uint16 + className *uint16 + hIconSm syscall.Handle +} + +const ( + COLOR_WINDOW = 0x05 + CW_USEDEFAULT = ^0x7fffffff +) + +var ( + procEnumThreadWindows = user32.NewProc("EnumThreadWindows") + procRegisterClassEx = user32.NewProc("RegisterClassExW") + procCreateWindowEx = user32.NewProc("CreateWindowExW") + procDefWinProc = user32.NewProc("DefWindowProcW") + + callbackEnumThreadWindows = syscall.NewCallback(wndProcCloseWindow) +) + +func DefWindowProc(hWnd syscall.Handle, msg uint32, wParam uintptr, lParam uintptr) int32 { + + ret, _, _ := + procDefWinProc.Call( // LRESULT DefWindowProcW() + uintptr(hWnd), // [in] HWND hWnd, + uintptr(msg), // [in] UINT Msg, + wParam, // [in] WPARAM wParam, + lParam, // [in] LPARAM lParam + ) + return int32(ret) +} + +func GetModuleHandle(name string) (syscall.Handle, error) { + var name16 *uint16 + var err error + + if len(name) > 0 { + if name16, err = syscall.UTF16PtrFromString(name); err != nil { + return 0, err + } + } + + ret, _, err := + procGetModuleHandle.Call( // HMODULE GetModuleHandleW() + uintptr(unsafe.Pointer(name16)), // [in, optional] LPCWSTR lpModuleName + ) + if ret == 0 { + return 0, fmt.Errorf("could not obtain module handle: %w", err) + } + + return syscall.Handle(ret), nil +} + +func RegisterClassEx(class *WNDCLASSEX) (uint16, error) { + + ret, _, err := + procRegisterClassEx.Call( // ATOM RegisterClassExW() + uintptr(unsafe.Pointer(class)), // [in] const WNDCLASSEXW *unnamedParam1 + ) + if ret == 0 { + return 0, fmt.Errorf("could not register window class: %w", err) + } + + return uint16(ret), nil +} + +func wndProc(hWnd syscall.Handle, msg uint32, wParam uintptr, lParam uintptr) uintptr { + switch msg { + case WM_DESTROY: + PostQuitMessage(0) + return 0 + default: + return uintptr(DefWindowProc(hWnd, msg, wParam, lParam)) + } +} + +func CloseThreadWindows(threadId uint32) { + _, _, _ = + procEnumThreadWindows.Call( // // BOOL EnumThreadWindows() + uintptr(threadId), // [in] DWORD dwThreadId, + callbackEnumThreadWindows, // [in] WNDENUMPROC lpfn, + 0, // [in] LPARAM lParam + ) +} + +func wndProcCloseWindow(hwnd uintptr, lparam uintptr) uintptr { + SendMessage(syscall.Handle(hwnd), WM_CLOSE, 0, 0) + + return 1 +} + +func RegisterDummyWinClass(name string, appInstance syscall.Handle) (uint16, error) { + var class16 *uint16 + var err error + if class16, err = syscall.UTF16PtrFromString(name); err != nil { + return 0, err + } + + class := WNDCLASSEX{ + className: class16, + hInstance: appInstance, + lpfnWndProc: syscall.NewCallback(wndProc), + } + + class.cbSize = uint32(unsafe.Sizeof(class)) + + return RegisterClassEx(&class) +} + +func CreateDummyWindow(name string, className string, appInstance syscall.Handle) (syscall.Handle, error) { + var name16, class16 *uint16 + var err error + + cwDefault := CW_USEDEFAULT + + if name16, err = syscall.UTF16PtrFromString(name); err != nil { + return 0, err + } + if class16, err = syscall.UTF16PtrFromString(className); err != nil { + return 0, err + } + ret, _, err := procCreateWindowEx.Call( //HWND CreateWindowExW( + 0, // [in] DWORD dwExStyle, + uintptr(unsafe.Pointer(class16)), // [in, optional] LPCWSTR lpClassName, + uintptr(unsafe.Pointer(name16)), // [in, optional] LPCWSTR lpWindowName, + 0, // [in] DWORD dwStyle, + uintptr(cwDefault), // [in] int X, + uintptr(cwDefault), // [in] int Y, + 0, // [in] int nWidth, + 0, // [in] int nHeight, + 0, // [in, optional] HWND hWndParent, + 0, // [in, optional] HMENU hMenu, + uintptr(appInstance), // [in, optional] HINSTANCE hInstance, + 0, // [in, optional] LPVOID lpParam + ) + + if ret == 0 { + return 0, fmt.Errorf("could not create window: %w", err) + } + + return syscall.Handle(ret), nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 32731ae700..16d2b7dc76 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -401,6 +401,10 @@ github.com/containers/storage/pkg/tarlog github.com/containers/storage/pkg/truncindex github.com/containers/storage/pkg/unshare github.com/containers/storage/types +# github.com/containers/winquit v1.1.0 +## explicit; go 1.19 +github.com/containers/winquit/pkg/winquit +github.com/containers/winquit/pkg/winquit/win32 # github.com/coreos/go-oidc/v3 v3.9.0 ## explicit; go 1.19 github.com/coreos/go-oidc/v3/oidc