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 9f3c806d0b..d28ad90e4e 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 f85ef3759c..04cbfc6835 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 78095dcfab..3ebaf1eaa4 100644 --- a/pkg/machine/applehv/stubber.go +++ b/pkg/machine/applehv/stubber.go @@ -40,6 +40,14 @@ type AppleHVStubber struct { } func (a AppleHVStubber) UserModeNetworkEnabled(_ *vmconfigs.MachineConfig) bool { +return true +} + +func (a AppleHVStubber) UseProviderNetworkSetup() bool { + return false +} + +func (a AppleHVStubber) RequireExclusiveActive() bool { return true } @@ -319,7 +327,7 @@ 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 } 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 7730d5c525..3548b6ee2e 100644 --- a/pkg/machine/config.go +++ b/pkg/machine/config.go @@ -333,11 +333,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/e2e/machine_test.go b/pkg/machine/e2e/machine_test.go index 918176fa50..866b23b878 100644 --- a/pkg/machine/e2e/machine_test.go +++ b/pkg/machine/e2e/machine_test.go @@ -2,7 +2,6 @@ package e2e_test import ( "fmt" - "github.com/containers/podman/v4/pkg/machine/wsl" "io" url2 "net/url" "os" @@ -13,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" 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..4311aac257 --- /dev/null +++ b/pkg/machine/gvproxy_windows.go @@ -0,0 +1,42 @@ +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 { + 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 + 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 1d4fddf9da..080d78c039 100644 --- a/pkg/machine/hyperv/stubber.go +++ b/pkg/machine/hyperv/stubber.go @@ -33,6 +33,14 @@ 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 @@ -374,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 @@ -383,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 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 3e5ad1168e..01f5fa4dc8 100644 --- a/pkg/machine/machine_windows.go +++ b/pkg/machine/machine_windows.go @@ -3,7 +3,9 @@ package machine import ( + "context" "fmt" + "net" "os" "os/exec" "path/filepath" @@ -12,16 +14,17 @@ import ( "time" "github.com/containers/podman/v5/pkg/machine/define" + winio "github.com/Microsoft/go-winio" "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" ) const WM_QUIT = 0x12 //nolint @@ -71,6 +74,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 { @@ -102,7 +110,7 @@ func launchWinProxy(opts WinProxyOpts) (bool, string, error) { } globalName := false - if PipeNameAvailable(globalPipe) { + if PipeNameAvailable(GlobalNamedPipe) { globalName = true } @@ -125,27 +133,20 @@ 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...) logrus.Debugf("winssh command: %s %v", command, args) - f, err := os.Open("c:\\Users\\baude\\sshproxy.log") - if err != nil { - return false, "", err - } - cmd.Stderr = f - cmd.Stdout = f - defer f.Close() if err := cmd.Start(); err != nil { 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) @@ -247,3 +248,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/qemu/stubber.go b/pkg/machine/qemu/stubber.go index 3fed30c955..28614246bb 100644 --- a/pkg/machine/qemu/stubber.go +++ b/pkg/machine/qemu/stubber.go @@ -34,6 +34,14 @@ 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 { @@ -73,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 @@ -327,7 +335,7 @@ 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 } diff --git a/pkg/machine/shim/host.go b/pkg/machine/shim/host.go index 807c60741c..9bf7bf0623 100644 --- a/pkg/machine/shim/host.go +++ b/pkg/machine/shim/host.go @@ -256,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 { @@ -330,7 +335,7 @@ func Stop(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, dirs *machineDef } // Stop GvProxy and remove PID file - if mp.UserModeNetworkEnabled(mc) { + if !mp.UseProviderNetworkSetup() { gvproxyPidFile, err := dirs.RuntimeDir.AppendToNewVMFile("gvproxy.pid", nil) if err != nil { return err @@ -381,7 +386,11 @@ func Start(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, _ *machineDefin } } - 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 } @@ -408,15 +417,6 @@ func Start(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, _ *machineDefin 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 { @@ -428,5 +428,22 @@ func Start(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, _ *machineDefin } } } + + // 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 dfb39008ba..6305458185 100644 --- a/pkg/machine/shim/networking.go +++ b/pkg/machine/shim/networking.go @@ -2,9 +2,7 @@ package shim import ( "fmt" - "io/fs" "net" - "os" "path/filepath" "strings" "time" @@ -23,7 +21,7 @@ const ( dockerConnectTimeout = 5 * time.Second ) -func startUserModeNetworking(mc *vmconfigs.MachineConfig, provider vmconfigs.VMProvider, dirs *define.MachineDirs, hostSocket *define.VMFile) error { +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 or @@ -57,10 +55,13 @@ func startUserModeNetworking(mc *vmconfigs.MachineConfig, provider vmconfigs.VMP 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 @@ -84,92 +85,28 @@ func startUserModeNetworking(mc *vmconfigs.MachineConfig, provider vmconfigs.VMP } func startNetworking(mc *vmconfigs.MachineConfig, provider vmconfigs.VMProvider) (string, machine.APIForwardingState, error) { - var ( - forwardingState machine.APIForwardingState - forwardSock string - ) - dirs, err := machine.GetMachineDirs(provider.VMType()) - if err != nil { - return "", 0, err + // Provider has its own networking code path (e.g. WSL) + if provider.UseProviderNetworkSetup() { + return "", 0, provider.StartNetworking(mc, nil) } - hostSocket, err := dirs.DataDir.AppendToNewVMFile("podman.sock", nil) + + dirs, err := machine.GetMachineDirs(provider.VMType()) if err != nil { return "", 0, err } - linkSocketPath := filepath.Dir(dirs.DataDir.GetPath()) - linkSocket, err := define.NewMachineFile(filepath.Join(linkSocketPath, "podman.sock"), nil) + hostSocks, forwardSock, forwardingState, err := setupMachineSockets(mc.Name, dirs) if err != nil { return "", 0, err } - if mc.HostUser.UID != -1 { - forwardSock, forwardingState = setupAPIForwarding(hostSocket, linkSocket) - } - - if provider.UserModeNetworkEnabled(mc) { - if err := startUserModeNetworking(mc, provider, dirs, hostSocket); err != nil { - return "", 0, err - } + if err := startHostForwarder(mc, provider, dirs, hostSocks); err != nil { + return "", 0, err } return forwardSock, forwardingState, 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 - } - - 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 -} - // conductVMReadinessCheck checks to make sure the machine is in the proper state // and that SSH is up and running func conductVMReadinessCheck(mc *vmconfigs.MachineConfig, maxBackoffs int, backoff time.Duration, stateF func() (define.Status, error)) (connected bool, sshError error, err error) { @@ -191,7 +128,7 @@ func conductVMReadinessCheck(mc *vmconfigs.MachineConfig, maxBackoffs int, backo // 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 { + 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 } 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..d13d4a0036 --- /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) { + 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) { + 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/vmconfigs/config.go b/pkg/machine/vmconfigs/config.go index a5449b56cd..8ddaef8137 100644 --- a/pkg/machine/vmconfigs/config.go +++ b/pkg/machine/vmconfigs/config.go @@ -119,13 +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/wsl/declares.go b/pkg/machine/wsl/declares.go index 1c59d5f421..a8a8179008 100644 --- a/pkg/machine/wsl/declares.go +++ b/pkg/machine/wsl/declares.go @@ -1,3 +1,5 @@ +//go:build windows + package wsl const ( diff --git a/pkg/machine/wsl/fedora.go b/pkg/machine/wsl/fedora.go index 859d4221fa..4e4fb9a5b3 100644 --- a/pkg/machine/wsl/fedora.go +++ b/pkg/machine/wsl/fedora.go @@ -3,7 +3,6 @@ package wsl import ( "errors" "fmt" - "github.com/sirupsen/logrus" "io" "net/http" "net/url" @@ -13,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" ) diff --git a/pkg/machine/wsl/machine.go b/pkg/machine/wsl/machine.go index e92b39c868..01eb0df2de 100644 --- a/pkg/machine/wsl/machine.go +++ b/pkg/machine/wsl/machine.go @@ -56,10 +56,11 @@ func getConfigPathExt(name string, extension string) (string, error) { // TODO like provisionWSL, i think this needs to be pushed to use common // paths and types where possible func unprovisionWSL(mc *vmconfigs.MachineConfig) error { - if err := terminateDist(mc.Name); err != nil { + dist := machine.ToDist(mc.Name) + if err := terminateDist(dist); err != nil { logrus.Error(err) } - if err := unregisterDist(mc.Name); err != nil { + if err := unregisterDist(dist); err != nil { logrus.Error(err) } @@ -87,17 +88,18 @@ func provisionWSLDist(name string, imagePath string, prompt string) (string, err return "", fmt.Errorf("could not create wsldist directory: %w", err) } + dist := machine.ToDist(name) fmt.Println(prompt) - if err = runCmdPassThrough("wsl", "--import", name, distTarget, imagePath, "--version", "2"); err != nil { + if err = runCmdPassThrough("wsl", "--import", dist, distTarget, imagePath, "--version", "2"); err != nil { return "", fmt.Errorf("the WSL import of guest OS failed: %w", err) } // Fixes newuidmap - if err = wslInvoke(name, "rpm", "--restore", "shadow-utils"); err != nil { + if err = wslInvoke(dist, "rpm", "--restore", "shadow-utils"); err != nil { return "", fmt.Errorf("package permissions restore of shadow-utils on guest OS failed: %w", err) } - return name, nil + return dist, nil } func createKeys(mc *vmconfigs.MachineConfig, dist string) error { @@ -139,21 +141,21 @@ func configureSystem(mc *vmconfigs.MachineConfig, dist string) error { return fmt.Errorf("could not configure SSH port for guest OS: %w", err) } - if err := wslPipe(withUser(configServices, user), mc.Name, "sh"); err != nil { + if err := wslPipe(withUser(configServices, user), dist, "sh"); err != nil { return fmt.Errorf("could not configure systemd settings for guest OS: %w", err) } - if err := wslPipe(sudoers, mc.Name, "sh", "-c", "cat >> /etc/sudoers"); err != nil { + if err := wslPipe(sudoers, dist, "sh", "-c", "cat >> /etc/sudoers"); err != nil { return fmt.Errorf("could not add wheel to sudoers: %w", err) } - if err := wslPipe(overrideSysusers, mc.Name, "sh", "-c", + if err := wslPipe(overrideSysusers, dist, "sh", "-c", "cat > /etc/systemd/system/systemd-sysusers.service.d/override.conf"); err != nil { return fmt.Errorf("could not generate systemd-sysusers override for guest OS: %w", err) } lingerCmd := withUser("cat > /home/[USER]/.config/systemd/[USER]/linger-example.service", user) - if err := wslPipe(lingerService, mc.Name, "sh", "-c", lingerCmd); err != nil { + if err := wslPipe(lingerService, dist, "sh", "-c", lingerCmd); err != nil { return fmt.Errorf("could not generate linger service for guest OS: %w", err) } @@ -714,14 +716,15 @@ func unregisterDist(dist string) error { } func isRunning(name string) (bool, error) { - wsl, err := isWSLRunning(name) + dist := machine.ToDist(name) + wsl, err := isWSLRunning(dist) if err != nil { return false, err } sysd := false if wsl { - sysd, err = isSystemdRunning(name) + sysd, err = isSystemdRunning(dist) if err != nil { return false, err @@ -746,10 +749,11 @@ func getDiskSize(name string) uint64 { } func getCPUs(name string) (uint64, error) { - if run, _ := isWSLRunning(name); !run { + dist := machine.ToDist(name) + if run, _ := isWSLRunning(dist); !run { return 0, nil } - cmd := exec.Command("wsl", "-u", "root", "-d", name, "nproc") + cmd := exec.Command("wsl", "-u", "root", "-d", dist, "nproc") out, err := cmd.StdoutPipe() if err != nil { return 0, err @@ -769,10 +773,11 @@ func getCPUs(name string) (uint64, error) { } func getMem(name string) (uint64, error) { - if run, _ := isWSLRunning(name); !run { + dist := machine.ToDist(name) + if run, _ := isWSLRunning(dist); !run { return 0, nil } - cmd := exec.Command("wsl", "-u", "root", "-d", name, "cat", "/proc/meminfo") + cmd := exec.Command("wsl", "-u", "root", "-d", dist, "cat", "/proc/meminfo") out, err := cmd.StdoutPipe() if err != nil { return 0, err diff --git a/pkg/machine/wsl/stubber.go b/pkg/machine/wsl/stubber.go index 25347678cf..a3693d9c99 100644 --- a/pkg/machine/wsl/stubber.go +++ b/pkg/machine/wsl/stubber.go @@ -120,7 +120,7 @@ func (w WSLStubber) Remove(mc *vmconfigs.MachineConfig) ([]string, func() error, // below if we wanted to hard error on the wsl unregister // of the vm wslRemoveFunc := func() error { - if err := runCmdPassThrough("wsl", "--unregister", mc.Name); err != nil { + if err := runCmdPassThrough("wsl", "--unregister", machine.ToDist(mc.Name)); err != nil { logrus.Error(err) } return machine.ReleaseMachinePort(mc.SSH.Port) @@ -178,24 +178,19 @@ func (w WSLStubber) SetProviderAttrs(mc *vmconfigs.MachineConfig, opts define.Se return errors.New("changing disk size not supported for WSL machines") } - // TODO This needs to be plumbed in for set as well - //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 - // } + 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 } @@ -211,26 +206,34 @@ func (w WSLStubber) UserModeNetworkEnabled(mc *vmconfigs.MachineConfig) bool { return mc.WSLHypervisor.UserModeNetworking } -func (w WSLStubber) PostStartNetworking(mc *vmconfigs.MachineConfig) error { - if mc.WSLHypervisor.UserModeNetworking { - 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, false) +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(mc.Name, useProxy, false); err != nil { + if err := configureProxy(dist, useProxy, false); err != nil { return nil, nil, err } @@ -243,25 +246,11 @@ func (w WSLStubber) StartVM(mc *vmconfigs.MachineConfig) (func() error, func() e // } // } - err := wslInvoke(mc.Name, "/root/bootstrap") + err := wslInvoke(dist, "/root/bootstrap") if err != nil { err = fmt.Errorf("the WSL bootstrap script failed: %w", err) } - // TODO we dont show this for any other provider. perhaps we should ? and if - // so, we need to move it up the stack - //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) - //} - readyFunc := func() error { return nil } @@ -284,11 +273,16 @@ func (w WSLStubber) StopVM(mc *vmconfigs.MachineConfig, hardStop bool) error { var ( err error ) - // by this time, state has been verified to be running and a request - // to stop is fair game 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()) @@ -298,13 +292,13 @@ func (w WSLStubber) StopVM(mc *vmconfigs.MachineConfig, hardStop bool) error { fmt.Fprintf(os.Stderr, "Could not stop API forwarding service (win-sshproxy.exe): %s\n", err.Error()) } - cmd := exec.Command("wsl", "-u", "root", "-d", mc.Name, "sh") + 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", mc.Name, "/usr/local/bin/enterns", "systemctl", "exit", "0") + 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) } @@ -313,7 +307,7 @@ func (w WSLStubber) StopVM(mc *vmconfigs.MachineConfig, hardStop bool) error { return err } - return terminateDist(mc.Name) + return terminateDist(dist) } func (w WSLStubber) StopHostNetworking(mc *vmconfigs.MachineConfig, vmType define.VMType) error { diff --git a/pkg/machine/wsl/usermodenet.go b/pkg/machine/wsl/usermodenet.go index 320bdda418..1c7ef4eeda 100644 --- a/pkg/machine/wsl/usermodenet.go +++ b/pkg/machine/wsl/usermodenet.go @@ -99,7 +99,7 @@ func startUserModeNetworking(mc *vmconfigs.MachineConfig) error { } } - if err := createUserModeResolvConf(mc.Name); err != nil { + if err := createUserModeResolvConf(machine.ToDist(mc.Name)); err != nil { return err } @@ -255,7 +255,7 @@ func addUserModeNetEntry(mc *vmconfigs.MachineConfig) error { return err } - path := filepath.Join(entriesDir, mc.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) @@ -270,7 +270,7 @@ func removeUserModeNetEntry(name string) error { return err } - path := filepath.Join(entriesDir, name) + path := filepath.Join(entriesDir, machine.ToDist(name)) return os.Remove(path) } 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 0e8e37f9d2..094cbf7814 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