Complete WSL implementation, refactor a few areas

Also addresses a number of issues:
- StopHostNetworking isn't plumbed, win-sshproxy leaks on hyperv
- Wait api and print output doesn't work properly on Windows
- API forwarding doesn't work on WSL
- Terminal corruption with after start/stop on Windows
- Gvproxy is forcefully killed vs gracefully quit
- Switching rootful/rootless does not update /var/run/docker.sock on the guest
- File already closed error on init
- HyperV backend is publishing Unix sockets when it should be named pipes
- User-mode networking doesn't always work
- Stop state outside of lock boundaries
- WSL blocks parallel machined (should be supported)

[NO NEW TESTS NEEDED]

Signed-off-by: Jason T. Greene <jason.greene@redhat.com>
This commit is contained in:
Jason T. Greene
2024-02-07 14:49:47 -06:00
parent d7cb66492b
commit 487219d809
44 changed files with 1710 additions and 276 deletions

View File

@ -128,7 +128,6 @@ func setMachine(cmd *cobra.Command, args []string) error {
setOpts.DiskSize = &newDiskSizeGB setOpts.DiskSize = &newDiskSizeGB
} }
if cmd.Flags().Changed("user-mode-networking") { if cmd.Flags().Changed("user-mode-networking") {
// TODO This needs help
setOpts.UserModeNetworking = &setFlags.UserModeNetworking setOpts.UserModeNetworking = &setFlags.UserModeNetworking
} }
if cmd.Flags().Changed("usb") { if cmd.Flags().Changed("usb") {

1
go.mod
View File

@ -19,6 +19,7 @@ require (
github.com/containers/ocicrypt v1.1.9 github.com/containers/ocicrypt v1.1.9
github.com/containers/psgo v1.9.0 github.com/containers/psgo v1.9.0
github.com/containers/storage v1.52.1-0.20240202181245-1419a5980565 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/go-systemd/v22 v22.5.1-0.20231103132048-7d375ecc2b09
github.com/coreos/stream-metadata-go v0.4.4 github.com/coreos/stream-metadata-go v0.4.4
github.com/crc-org/crc/v2 v2.32.0 github.com/crc-org/crc/v2 v2.32.0

2
go.sum
View File

@ -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/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 h1:Gcirfx2DNoayB/+ypLgl5+ABzIPPDAoncs1qgZHHQHE=
github.com/containers/storage v1.52.1-0.20240202181245-1419a5980565/go.mod h1:2E/QBqWVcJXwumP7nVUrampwRNL4XKjHL/aQya7ZdhI= 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 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo=
github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4= 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= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f h1:JOrtw2xFKzlg+cbHpyrpLDmnN1HqhBfnX7WDiW7eG2c=

View File

@ -40,6 +40,14 @@ type AppleHVStubber struct {
} }
func (a AppleHVStubber) UserModeNetworkEnabled(_ *vmconfigs.MachineConfig) bool { func (a AppleHVStubber) UserModeNetworkEnabled(_ *vmconfigs.MachineConfig) bool {
return true
}
func (a AppleHVStubber) UseProviderNetworkSetup() bool {
return false
}
func (a AppleHVStubber) RequireExclusiveActive() bool {
return true return true
} }
@ -319,7 +327,7 @@ func (a AppleHVStubber) PrepareIgnition(_ *vmconfigs.MachineConfig, _ *ignition.
return nil, nil return nil, nil
} }
func (a AppleHVStubber) PostStartNetworking(mc *vmconfigs.MachineConfig) error { func (a AppleHVStubber) PostStartNetworking(mc *vmconfigs.MachineConfig, noInfo bool) error {
return nil return nil
} }

View File

@ -32,8 +32,8 @@ func Decompress(localPath *define.VMFile, uncompressedPath string) error {
return err return err
} }
defer func() { defer func() {
if err := uncompressedFileWriter.Close(); err != nil { if err := uncompressedFileWriter.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
logrus.Errorf("unable to to close decompressed file %s: %q", uncompressedPath, err) logrus.Warnf("unable to close decompressed file %s: %q", uncompressedPath, err)
} }
}() }()
sourceFile, err := localPath.Read() sourceFile, err := localPath.Read()

View File

@ -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) { func WaitAndPingAPI(sock string) {
client := http.Client{ client := http.Client{
Transport: &http.Transport{ Transport: &http.Transport{
DialContext: func(context.Context, string, string) (net.Conn, error) { DialContext: func(context.Context, string, string) (net.Conn, error) {
con, err := net.DialTimeout("unix", sock, apiUpTimeout) con, err := dialSocket(sock, apiUpTimeout)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -2,7 +2,6 @@ package e2e_test
import ( import (
"fmt" "fmt"
"github.com/containers/podman/v4/pkg/machine/wsl"
"io" "io"
url2 "net/url" url2 "net/url"
"os" "os"
@ -13,6 +12,8 @@ import (
"testing" "testing"
"time" "time"
"github.com/containers/podman/v5/pkg/machine/wsl"
"github.com/containers/podman/v5/pkg/machine" "github.com/containers/podman/v5/pkg/machine"
"github.com/containers/podman/v5/pkg/machine/compression" "github.com/containers/podman/v5/pkg/machine/compression"
"github.com/containers/podman/v5/pkg/machine/define" "github.com/containers/podman/v5/pkg/machine/define"

View File

@ -1,16 +1,12 @@
package machine package machine
import ( import (
"errors"
"fmt" "fmt"
"runtime"
"strconv" "strconv"
"syscall"
"time" "time"
"github.com/containers/podman/v5/pkg/machine/define" "github.com/containers/podman/v5/pkg/machine/define"
psutil "github.com/shirou/gopsutil/v3/process" psutil "github.com/shirou/gopsutil/v3/process"
"github.com/sirupsen/logrus"
) )
const ( const (
@ -39,49 +35,6 @@ func backoffForProcess(p *psutil.Process) error {
return fmt.Errorf("process %d has not ended", p.Pid) 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 // CleanupGVProxy reads the --pid-file for gvproxy attempts to stop it
func CleanupGVProxy(f define.VMFile) error { func CleanupGVProxy(f define.VMFile) error {
gvPid, err := f.Read() gvPid, err := f.Read()

View File

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

View File

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

View File

@ -33,6 +33,14 @@ func (h HyperVStubber) UserModeNetworkEnabled(mc *vmconfigs.MachineConfig) bool
return true 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 { func (h HyperVStubber) CreateVM(opts define.CreateVMOpts, mc *vmconfigs.MachineConfig, builder *ignition.IgnitionBuilder) error {
var ( var (
err error err error
@ -374,7 +382,7 @@ func (h HyperVStubber) PrepareIgnition(mc *vmconfigs.MachineConfig, ignBuilder *
return &ignOpts, nil return &ignOpts, nil
} }
func (h HyperVStubber) PostStartNetworking(mc *vmconfigs.MachineConfig) error { func (h HyperVStubber) PostStartNetworking(mc *vmconfigs.MachineConfig, noInfo bool) error {
var ( var (
err error err error
executable string executable string
@ -383,25 +391,6 @@ func (h HyperVStubber) PostStartNetworking(mc *vmconfigs.MachineConfig) error {
defer callbackFuncs.CleanIfErr(&err) defer callbackFuncs.CleanIfErr(&err)
go callbackFuncs.CleanOnSignal() 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 { if len(mc.Mounts) != 0 {
var ( var (
dirs *define.MachineDirs dirs *define.MachineDirs

View File

@ -6,6 +6,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"strings"
"github.com/containers/podman/v5/pkg/machine/connection" "github.com/containers/podman/v5/pkg/machine/connection"
"github.com/containers/storage/pkg/ioutils" "github.com/containers/storage/pkg/ioutils"
@ -46,19 +47,6 @@ func WaitAPIAndPrintInfo(forwardState APIForwardingState, name, helper, forwardS
WaitAndPingAPI(forwardSock) WaitAndPingAPI(forwardSock)
if !noInfo { 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) fmt.Printf("API forwarding listening on: %s\n", forwardSock)
if forwardState == DockerGlobal { if forwardState == DockerGlobal {
fmt.Printf("Docker API clients default to this address. You do not need to set DOCKER_HOST.\n\n") 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 sudo %s install
podman machine stop%[2]s; podman machine start%[2]s podman machine stop%[2]s; podman machine start%[2]s
` `
fmt.Printf(fmtString, helper, suffix) fmt.Printf(fmtString, helper, suffix)
} }
case MachineLocal: 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 fmtString = `You can %sconnect Docker API clients by setting DOCKER_HOST using the
following command in your terminal session: following command in your terminal session:
export DOCKER_HOST='unix://%s' %s'
` `
prefix := ""
fmt.Printf(fmtString, stillString, forwardSock) 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 // SetRootful modifies the machine's default connection to be either rootful or
// rootless // rootless
func SetRootful(rootful bool, name, rootfulName string) error { func SetRootful(rootful bool, name, rootfulName string) error {

View File

@ -3,7 +3,10 @@
package machine package machine
import ( import (
"context"
"errors" "errors"
"fmt"
"net"
"strings" "strings"
) )
@ -32,3 +35,11 @@ func ParseVolumeFromPath(v string) (source, target, options string, readonly boo
} }
return 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)
}

View File

@ -3,7 +3,9 @@
package machine package machine
import ( import (
"context"
"fmt" "fmt"
"net"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@ -12,16 +14,17 @@ import (
"time" "time"
"github.com/containers/podman/v5/pkg/machine/define" "github.com/containers/podman/v5/pkg/machine/define"
winio "github.com/Microsoft/go-winio"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
const ( const (
pipePrefix = "npipe:////./pipe/" NamedPipePrefix = "npipe:////./pipe/"
globalPipe = "docker_engine" GlobalNamedPipe = "docker_engine"
winSShProxy = "win-sshproxy.exe" winSShProxy = "win-sshproxy.exe"
winSshProxyTid = "win-sshproxy.tid" winSshProxyTid = "win-sshproxy.tid"
rootfulSock = "/run/podman/podman.sock" rootfulSock = "/run/podman/podman.sock"
rootlessSock = "/run/user/1000/podman/podman.sock" rootlessSock = "/run/user/1000/podman/podman.sock"
) )
const WM_QUIT = 0x12 //nolint const WM_QUIT = 0x12 //nolint
@ -71,6 +74,11 @@ func WaitPipeExists(pipeName string, retries int, checkFailure func() error) err
return 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) { func LaunchWinProxy(opts WinProxyOpts, noInfo bool) {
globalName, pipeName, err := launchWinProxy(opts) globalName, pipeName, err := launchWinProxy(opts)
if !noInfo { if !noInfo {
@ -102,7 +110,7 @@ func launchWinProxy(opts WinProxyOpts) (bool, string, error) {
} }
globalName := false globalName := false
if PipeNameAvailable(globalPipe) { if PipeNameAvailable(GlobalNamedPipe) {
globalName = true 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) 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 waitPipe := machinePipe
if globalName { if globalName {
args = append(args, pipePrefix+globalPipe, dest, opts.IdentityPath) args = append(args, NamedPipePrefix+GlobalNamedPipe, dest, opts.IdentityPath)
waitPipe = globalPipe waitPipe = GlobalNamedPipe
} }
cmd := exec.Command(command, args...) cmd := exec.Command(command, args...)
logrus.Debugf("winssh command: %s %v", 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 { if err := cmd.Start(); err != nil {
return globalName, "", err 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) active, exitCode := GetProcessState(cmd.Process.Pid)
if !active { if !active {
return fmt.Errorf("win-sshproxy.exe failed to start, exit code: %d (see windows event logs)", exitCode) 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 return name
} }
func GetEnvSetString(env string, val string) string {
return fmt.Sprintf("$Env:%s=\"%s\"", env, val)
}

View File

@ -34,6 +34,14 @@ func (q QEMUStubber) UserModeNetworkEnabled(*vmconfigs.MachineConfig) bool {
return true return true
} }
func (q QEMUStubber) UseProviderNetworkSetup() bool {
return false
}
func (q QEMUStubber) RequireExclusiveActive() bool {
return true
}
func (q *QEMUStubber) setQEMUCommandLine(mc *vmconfigs.MachineConfig) error { func (q *QEMUStubber) setQEMUCommandLine(mc *vmconfigs.MachineConfig) error {
qemuBinary, err := findQEMUBinary() qemuBinary, err := findQEMUBinary()
if err != nil { if err != nil {
@ -73,7 +81,7 @@ func (q *QEMUStubber) setQEMUCommandLine(mc *vmconfigs.MachineConfig) error {
return nil 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) monitor, err := command.NewQMPMonitor(opts.Name, opts.Dirs.RuntimeDir)
if err != nil { if err != nil {
return err return err
@ -327,7 +335,7 @@ func (q *QEMUStubber) MountType() vmconfigs.VolumeMountType {
return vmconfigs.NineP return vmconfigs.NineP
} }
func (q *QEMUStubber) PostStartNetworking(mc *vmconfigs.MachineConfig) error { func (q *QEMUStubber) PostStartNetworking(mc *vmconfigs.MachineConfig, noInfo bool) error {
return nil return nil
} }

View File

@ -256,6 +256,11 @@ func VMExists(name string, vmstubbers []vmconfigs.VMProvider) (*vmconfigs.Machin
// CheckExclusiveActiveVM checks if any of the machines are already running // CheckExclusiveActiveVM checks if any of the machines are already running
func CheckExclusiveActiveVM(provider vmconfigs.VMProvider, mc *vmconfigs.MachineConfig) error { 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 // Check if any other machines are running; if so, we error
localMachines, err := getMCsOverProviders([]vmconfigs.VMProvider{provider}) localMachines, err := getMCsOverProviders([]vmconfigs.VMProvider{provider})
if err != nil { if err != nil {
@ -330,7 +335,7 @@ func Stop(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, dirs *machineDef
} }
// Stop GvProxy and remove PID file // Stop GvProxy and remove PID file
if mp.UserModeNetworkEnabled(mc) { if !mp.UseProviderNetworkSetup() {
gvproxyPidFile, err := dirs.RuntimeDir.AppendToNewVMFile("gvproxy.pid", nil) gvproxyPidFile, err := dirs.RuntimeDir.AppendToNewVMFile("gvproxy.pid", nil)
if err != nil { if err != nil {
return err 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 { if err != nil {
return err return err
} }
@ -408,15 +417,6 @@ func Start(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, _ *machineDefin
return err 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) // update the podman/docker socket service if the host user has been modified at all (UID or Rootful)
if mc.HostUser.Modified { if mc.HostUser.Modified {
if machine.UpdatePodmanDockerSockService(mc) == nil { 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 return nil
} }

View File

@ -2,9 +2,7 @@ package shim
import ( import (
"fmt" "fmt"
"io/fs"
"net" "net"
"os"
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
@ -23,7 +21,7 @@ const (
dockerConnectTimeout = 5 * time.Second 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 forwardUser := mc.SSH.RemoteUsername
// TODO should this go up the stack higher or // 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.SSHPort = mc.SSH.Port
cmd.AddForwardSock(hostSocket.GetPath()) // Windows providers listen on multiple sockets since they do not involve links
cmd.AddForwardDest(guestSock) for _, hostSock := range hostSocks {
cmd.AddForwardUser(forwardUser) cmd.AddForwardSock(hostSock)
cmd.AddForwardIdentity(mc.SSH.IdentityPath) cmd.AddForwardDest(guestSock)
cmd.AddForwardUser(forwardUser)
cmd.AddForwardIdentity(mc.SSH.IdentityPath)
}
if logrus.IsLevelEnabled(logrus.DebugLevel) { if logrus.IsLevelEnabled(logrus.DebugLevel) {
cmd.Debug = true 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) { func startNetworking(mc *vmconfigs.MachineConfig, provider vmconfigs.VMProvider) (string, machine.APIForwardingState, error) {
var ( // Provider has its own networking code path (e.g. WSL)
forwardingState machine.APIForwardingState if provider.UseProviderNetworkSetup() {
forwardSock string return "", 0, provider.StartNetworking(mc, nil)
) }
dirs, err := machine.GetMachineDirs(provider.VMType()) dirs, err := machine.GetMachineDirs(provider.VMType())
if err != nil { if err != nil {
return "", 0, err return "", 0, err
} }
hostSocket, err := dirs.DataDir.AppendToNewVMFile("podman.sock", nil)
hostSocks, forwardSock, forwardingState, err := setupMachineSockets(mc.Name, dirs)
if err != nil { if err != nil {
return "", 0, err return "", 0, err
} }
linkSocketPath := filepath.Dir(dirs.DataDir.GetPath()) if err := startHostForwarder(mc, provider, dirs, hostSocks); err != nil {
linkSocket, err := define.NewMachineFile(filepath.Join(linkSocketPath, "podman.sock"), nil)
if err != nil {
return "", 0, err 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
}
}
return forwardSock, forwardingState, nil 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 // conductVMReadinessCheck checks to make sure the machine is in the proper state
// and that SSH is up and running // 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) { 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 // CoreOS users have reported the same observation but
// the underlying source of the issue remains unknown. // 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) logrus.Debugf("SSH readiness check for machine failed: %v", sshError)
continue continue
} }

View File

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

View File

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

View File

@ -2,7 +2,6 @@ package machine
import ( import (
"fmt" "fmt"
"os"
"os/exec" "os/exec"
"strconv" "strconv"
@ -13,24 +12,38 @@ import (
// and a port // and a port
// TODO This should probably be taught about an machineconfig to reduce input // TODO This should probably be taught about an machineconfig to reduce input
func CommonSSH(username, identityPath, name string, sshPort int, inputArgs []string) error { 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" sshDestination := username + "@localhost"
port := strconv.Itoa(sshPort) port := strconv.Itoa(sshPort)
interactive := true
args := []string{"-i", identityPath, "-p", port, sshDestination, args := []string{"-i", identityPath, "-p", port, sshDestination,
"-o", "IdentitiesOnly=yes", "-o", "IdentitiesOnly=yes",
"-o", "StrictHostKeyChecking=no", "-o", "LogLevel=ERROR", "-o", "SetEnv=LC_ALL="} "-o", "StrictHostKeyChecking=no", "-o", "LogLevel=ERROR", "-o", "SetEnv=LC_ALL="}
if len(inputArgs) > 0 { if len(inputArgs) > 0 {
interactive = false
args = append(args, inputArgs...) args = append(args, inputArgs...)
} else { } else {
// ensure we have a tty
args = append(args, "-t")
fmt.Printf("Connecting to vm %s. To close connection, use `~.` or `exit`\n", name) fmt.Printf("Connecting to vm %s. To close connection, use `~.` or `exit`\n", name)
} }
cmd := exec.Command("ssh", args...) cmd := exec.Command("ssh", args...)
logrus.Debugf("Executing: ssh %v\n", args) logrus.Debugf("Executing: ssh %v\n", args)
cmd.Stdout = os.Stdout if !silent {
cmd.Stderr = os.Stderr if err := setupIOPassthrough(cmd, interactive); err != nil {
cmd.Stdin = os.Stdin return err
}
}
return cmd.Run() return cmd.Run()
} }

16
pkg/machine/ssh_unix.go Normal file
View File

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

View File

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

View File

@ -119,13 +119,15 @@ type VMProvider interface { //nolint:interfacebloat
RemoveAndCleanMachines(dirs *define.MachineDirs) error RemoveAndCleanMachines(dirs *define.MachineDirs) error
SetProviderAttrs(mc *MachineConfig, opts define.SetOptions) error SetProviderAttrs(mc *MachineConfig, opts define.SetOptions) error
StartNetworking(mc *MachineConfig, cmd *gvproxy.GvproxyCommand) 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) StartVM(mc *MachineConfig) (func() error, func() error, error)
State(mc *MachineConfig, bypass bool) (define.Status, error) State(mc *MachineConfig, bypass bool) (define.Status, error)
StopVM(mc *MachineConfig, hardStop bool) error StopVM(mc *MachineConfig, hardStop bool) error
StopHostNetworking(mc *MachineConfig, vmType define.VMType) error StopHostNetworking(mc *MachineConfig, vmType define.VMType) error
VMType() define.VMType VMType() define.VMType
UserModeNetworkEnabled(mc *MachineConfig) bool UserModeNetworkEnabled(mc *MachineConfig) bool
UseProviderNetworkSetup() bool
RequireExclusiveActive() bool
} }
// HostUser describes the host user // HostUser describes the host user

View File

@ -1,3 +1,5 @@
//go:build windows
package wsl package wsl
const ( const (

View File

@ -3,7 +3,6 @@ package wsl
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/sirupsen/logrus"
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
@ -13,6 +12,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/sirupsen/logrus"
"github.com/containers/podman/v5/pkg/machine" "github.com/containers/podman/v5/pkg/machine"
"github.com/containers/podman/v5/pkg/machine/define" "github.com/containers/podman/v5/pkg/machine/define"
) )

View File

@ -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 // TODO like provisionWSL, i think this needs to be pushed to use common
// paths and types where possible // paths and types where possible
func unprovisionWSL(mc *vmconfigs.MachineConfig) error { 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) logrus.Error(err)
} }
if err := unregisterDist(mc.Name); err != nil { if err := unregisterDist(dist); err != nil {
logrus.Error(err) 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) return "", fmt.Errorf("could not create wsldist directory: %w", err)
} }
dist := machine.ToDist(name)
fmt.Println(prompt) 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) return "", fmt.Errorf("the WSL import of guest OS failed: %w", err)
} }
// Fixes newuidmap // 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 "", 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 { 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) 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) 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) 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 { "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) 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) 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) 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) { func isRunning(name string) (bool, error) {
wsl, err := isWSLRunning(name) dist := machine.ToDist(name)
wsl, err := isWSLRunning(dist)
if err != nil { if err != nil {
return false, err return false, err
} }
sysd := false sysd := false
if wsl { if wsl {
sysd, err = isSystemdRunning(name) sysd, err = isSystemdRunning(dist)
if err != nil { if err != nil {
return false, err return false, err
@ -746,10 +749,11 @@ func getDiskSize(name string) uint64 {
} }
func getCPUs(name string) (uint64, error) { func getCPUs(name string) (uint64, error) {
if run, _ := isWSLRunning(name); !run { dist := machine.ToDist(name)
if run, _ := isWSLRunning(dist); !run {
return 0, nil 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() out, err := cmd.StdoutPipe()
if err != nil { if err != nil {
return 0, err return 0, err
@ -769,10 +773,11 @@ func getCPUs(name string) (uint64, error) {
} }
func getMem(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 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() out, err := cmd.StdoutPipe()
if err != nil { if err != nil {
return 0, err return 0, err

View File

@ -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 // below if we wanted to hard error on the wsl unregister
// of the vm // of the vm
wslRemoveFunc := func() error { 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) logrus.Error(err)
} }
return machine.ReleaseMachinePort(mc.SSH.Port) 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") 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 && mc.WSLHypervisor.UserModeNetworking != *opts.UserModeNetworking {
//if opts.UserModeNetworking != nil && *opts.UserModeNetworking != v.UserModeNetworking { if running, _ := isRunning(mc.Name); running {
// update := true return errors.New("user-mode networking can only be changed when the machine is not running")
// }
// if v.isRunning() {
// update = false dist := machine.ToDist(mc.Name)
// setErrors = append(setErrors, fmt.Errorf("user-mode networking can only be changed when the machine is not running")) if err := changeDistUserModeNetworking(dist, mc.SSH.RemoteUsername, mc.ImagePath.GetPath(), *opts.UserModeNetworking); err != nil {
// } else { return fmt.Errorf("failure changing state of user-mode networking setting", err)
// dist := toDist(v.Name) }
// if err := changeDistUserModeNetworking(dist, v.RemoteUsername, v.ImagePath, *opts.UserModeNetworking); err != nil {
// update = false mc.WSLHypervisor.UserModeNetworking = *opts.UserModeNetworking
// setErrors = append(setErrors, err) }
// }
// }
//
// if update {
// v.UserModeNetworking = *opts.UserModeNetworking
// }
return nil return nil
} }
@ -211,26 +206,34 @@ func (w WSLStubber) UserModeNetworkEnabled(mc *vmconfigs.MachineConfig) bool {
return mc.WSLHypervisor.UserModeNetworking return mc.WSLHypervisor.UserModeNetworking
} }
func (w WSLStubber) PostStartNetworking(mc *vmconfigs.MachineConfig) error { func (w WSLStubber) UseProviderNetworkSetup() bool {
if mc.WSLHypervisor.UserModeNetworking { return true
winProxyOpts := machine.WinProxyOpts{ }
Name: mc.Name,
IdentityPath: mc.SSH.IdentityPath, func (w WSLStubber) RequireExclusiveActive() bool {
Port: mc.SSH.Port, return false
RemoteUsername: mc.SSH.RemoteUsername, }
Rootful: mc.HostUser.Rootful,
VMType: w.VMType(), func (w WSLStubber) PostStartNetworking(mc *vmconfigs.MachineConfig, noInfo bool) error {
} winProxyOpts := machine.WinProxyOpts{
machine.LaunchWinProxy(winProxyOpts, false) 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 return nil
} }
func (w WSLStubber) StartVM(mc *vmconfigs.MachineConfig) (func() error, func() error, error) { func (w WSLStubber) StartVM(mc *vmconfigs.MachineConfig) (func() error, func() error, error) {
useProxy := setupWslProxyEnv() useProxy := setupWslProxyEnv()
dist := machine.ToDist(mc.Name)
// TODO Quiet is hard set to false: follow up // 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 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 { if err != nil {
err = fmt.Errorf("the WSL bootstrap script failed: %w", err) 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 { readyFunc := func() error {
return nil return nil
} }
@ -284,11 +273,16 @@ func (w WSLStubber) StopVM(mc *vmconfigs.MachineConfig, hardStop bool) error {
var ( var (
err error err error
) )
// by this time, state has been verified to be running and a request
// to stop is fair game
mc.Lock() mc.Lock()
defer mc.Unlock() 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 // Stop user-mode networking if enabled
if err := stopUserModeNetworking(mc); err != nil { if err := stopUserModeNetworking(mc); err != nil {
fmt.Fprintf(os.Stderr, "Could not cleanly stop user-mode networking: %s\n", err.Error()) 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()) 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) cmd.Stdin = strings.NewReader(waitTerm)
if err = cmd.Start(); err != nil { if err = cmd.Start(); err != nil {
return fmt.Errorf("executing wait command: %w", err) 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 { if err = exitCmd.Run(); err != nil {
return fmt.Errorf("stopping sysd: %w", err) return fmt.Errorf("stopping sysd: %w", err)
} }
@ -313,7 +307,7 @@ func (w WSLStubber) StopVM(mc *vmconfigs.MachineConfig, hardStop bool) error {
return err return err
} }
return terminateDist(mc.Name) return terminateDist(dist)
} }
func (w WSLStubber) StopHostNetworking(mc *vmconfigs.MachineConfig, vmType define.VMType) error { func (w WSLStubber) StopHostNetworking(mc *vmconfigs.MachineConfig, vmType define.VMType) error {

View File

@ -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 return err
} }
@ -255,7 +255,7 @@ func addUserModeNetEntry(mc *vmconfigs.MachineConfig) error {
return err 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) file, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
if err != nil { if err != nil {
return fmt.Errorf("could not add user-mode networking registration: %w", err) return fmt.Errorf("could not add user-mode networking registration: %w", err)
@ -270,7 +270,7 @@ func removeUserModeNetEntry(name string) error {
return err return err
} }
path := filepath.Join(entriesDir, name) path := filepath.Join(entriesDir, machine.ToDist(name))
return os.Remove(path) return os.Remove(path)
} }

202
vendor/github.com/containers/winquit/LICENSE generated vendored Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

135
vendor/github.com/containers/winquit/pkg/winquit/doc.go generated vendored Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
//go:build !windows
// +build !windows
package win32

View File

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

View File

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

View File

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

View File

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

4
vendor/modules.txt vendored
View File

@ -401,6 +401,10 @@ github.com/containers/storage/pkg/tarlog
github.com/containers/storage/pkg/truncindex github.com/containers/storage/pkg/truncindex
github.com/containers/storage/pkg/unshare github.com/containers/storage/pkg/unshare
github.com/containers/storage/types 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 # github.com/coreos/go-oidc/v3 v3.9.0
## explicit; go 1.19 ## explicit; go 1.19
github.com/coreos/go-oidc/v3/oidc github.com/coreos/go-oidc/v3/oidc