mirror of
https://github.com/containers/podman.git
synced 2025-05-17 06:59:07 +08:00
275 lines
6.7 KiB
Go
275 lines
6.7 KiB
Go
//go:build windows
|
|
|
|
package machine
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
winio "github.com/Microsoft/go-winio"
|
|
"github.com/containers/podman/v5/pkg/machine/define"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
const (
|
|
NamedPipePrefix = "npipe:////./pipe/"
|
|
GlobalNamedPipe = "docker_engine"
|
|
winSSHProxy = "win-sshproxy.exe"
|
|
winSSHProxyTid = "win-sshproxy.tid"
|
|
rootfulSock = "/run/podman/podman.sock"
|
|
rootlessSock = "/run/user/1000/podman/podman.sock"
|
|
|
|
// machine wait is longer since we must hard fail
|
|
MachineNameWait = 5 * time.Second
|
|
GlobalNameWait = 250 * time.Millisecond
|
|
)
|
|
|
|
//nolint:stylecheck
|
|
const WM_QUIT = 0x12
|
|
|
|
type WinProxyOpts struct {
|
|
Name string
|
|
IdentityPath string
|
|
Port int
|
|
RemoteUsername string
|
|
Rootful bool
|
|
VMType define.VMType
|
|
}
|
|
|
|
func GetProcessState(pid int) (active bool, exitCode int) {
|
|
const da = syscall.STANDARD_RIGHTS_READ | syscall.PROCESS_QUERY_INFORMATION | syscall.SYNCHRONIZE
|
|
handle, err := syscall.OpenProcess(da, false, uint32(pid))
|
|
if err != nil {
|
|
logrus.Debugf("Error retrieving process %d: %v", pid, err)
|
|
return false, int(syscall.ERROR_PROC_NOT_FOUND)
|
|
}
|
|
|
|
var code uint32
|
|
if err := syscall.GetExitCodeProcess(handle, &code); err != nil {
|
|
logrus.Errorf("Error retrieving process %d exit code: %v", pid, err)
|
|
}
|
|
return code == 259, int(code)
|
|
}
|
|
|
|
func PipeNameAvailable(pipeName string, maxWait time.Duration) bool {
|
|
const interval = 250 * time.Millisecond
|
|
var wait time.Duration
|
|
for {
|
|
_, err := os.Stat(`\\.\pipe\` + pipeName)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return true
|
|
}
|
|
if wait >= maxWait {
|
|
return false
|
|
}
|
|
time.Sleep(interval)
|
|
wait += interval
|
|
}
|
|
}
|
|
|
|
func WaitPipeExists(pipeName string, retries int, checkFailure func() error) error {
|
|
var err error
|
|
for i := 0; i < retries; i++ {
|
|
_, err = os.Stat(`\\.\pipe\` + pipeName)
|
|
if err == nil {
|
|
break
|
|
}
|
|
if fail := checkFailure(); fail != nil {
|
|
return fail
|
|
}
|
|
time.Sleep(250 * time.Millisecond)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func DialNamedPipe(ctx context.Context, path string) (net.Conn, error) {
|
|
path = strings.ReplaceAll(path, "/", "\\")
|
|
return winio.DialPipeContext(ctx, path)
|
|
}
|
|
|
|
func LaunchWinProxy(opts WinProxyOpts, noInfo bool) {
|
|
globalName, pipeName, err := launchWinProxy(opts)
|
|
if !noInfo {
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "API forwarding for Docker API clients is not available due to the following startup failures.")
|
|
fmt.Fprintf(os.Stderr, "\t%s\n", err.Error())
|
|
fmt.Fprintln(os.Stderr, "\nPodman clients are still able to connect.")
|
|
} else {
|
|
fmt.Printf("API forwarding listening on: %s\n", pipeName)
|
|
if globalName {
|
|
fmt.Printf("\nDocker API clients default to this address. You do not need to set DOCKER_HOST.\n")
|
|
} else {
|
|
fmt.Printf("\nAnother process was listening on the default Docker API pipe address.\n")
|
|
fmt.Printf("You can still connect Docker API clients by setting DOCKER HOST using the\n")
|
|
fmt.Printf("following powershell command in your terminal session:\n")
|
|
fmt.Printf("\n\t$Env:DOCKER_HOST = '%s'\n", pipeName)
|
|
fmt.Printf("\nOr in a classic CMD prompt:\n")
|
|
fmt.Printf("\n\tset DOCKER_HOST=%s\n", pipeName)
|
|
fmt.Printf("\nAlternatively, terminate the other process and restart podman machine.\n")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func launchWinProxy(opts WinProxyOpts) (bool, string, error) {
|
|
machinePipe := ToDist(opts.Name)
|
|
if !PipeNameAvailable(machinePipe, MachineNameWait) {
|
|
return false, "", fmt.Errorf("could not start api proxy since expected pipe is not available: %s", machinePipe)
|
|
}
|
|
|
|
globalName := false
|
|
if PipeNameAvailable(GlobalNamedPipe, GlobalNameWait) {
|
|
globalName = true
|
|
}
|
|
|
|
command, err := FindExecutablePeer(winSSHProxy)
|
|
if err != nil {
|
|
return globalName, "", err
|
|
}
|
|
|
|
stateDir, err := GetWinProxyStateDir(opts.Name, opts.VMType)
|
|
if err != nil {
|
|
return globalName, "", err
|
|
}
|
|
|
|
destSock := rootlessSock
|
|
forwardUser := opts.RemoteUsername
|
|
|
|
if opts.Rootful {
|
|
destSock = rootfulSock
|
|
forwardUser = "root"
|
|
}
|
|
|
|
dest := fmt.Sprintf("ssh://%s@localhost:%d%s", forwardUser, opts.Port, destSock)
|
|
args := []string{opts.Name, stateDir, NamedPipePrefix + machinePipe, dest, opts.IdentityPath}
|
|
waitPipe := machinePipe
|
|
if globalName {
|
|
args = append(args, NamedPipePrefix+GlobalNamedPipe, dest, opts.IdentityPath)
|
|
waitPipe = GlobalNamedPipe
|
|
}
|
|
|
|
cmd := exec.Command(command, args...)
|
|
logrus.Debugf("winssh command: %s %v", command, args)
|
|
if err := cmd.Start(); err != nil {
|
|
return globalName, "", err
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func StopWinProxy(name string, vmtype define.VMType) error {
|
|
pid, tid, tidFile, err := readWinProxyTid(name, vmtype)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
proc, err := os.FindProcess(int(pid))
|
|
if err != nil {
|
|
//nolint:nilerr
|
|
return nil
|
|
}
|
|
sendQuit(tid)
|
|
_ = waitTimeout(proc, 20*time.Second)
|
|
_ = os.Remove(tidFile)
|
|
|
|
return nil
|
|
}
|
|
|
|
func readWinProxyTid(name string, vmtype define.VMType) (uint32, uint32, string, error) {
|
|
stateDir, err := GetWinProxyStateDir(name, vmtype)
|
|
if err != nil {
|
|
return 0, 0, "", err
|
|
}
|
|
|
|
tidFile := filepath.Join(stateDir, winSSHProxyTid)
|
|
contents, err := os.ReadFile(tidFile)
|
|
if err != nil {
|
|
return 0, 0, "", err
|
|
}
|
|
|
|
var pid, tid uint32
|
|
fmt.Sscanf(string(contents), "%d:%d", &pid, &tid)
|
|
return pid, tid, tidFile, nil
|
|
}
|
|
|
|
func waitTimeout(proc *os.Process, timeout time.Duration) bool {
|
|
done := make(chan bool)
|
|
go func() {
|
|
_, _ = proc.Wait()
|
|
done <- true
|
|
}()
|
|
ret := false
|
|
select {
|
|
case <-time.After(timeout):
|
|
_ = proc.Kill()
|
|
<-done
|
|
case <-done:
|
|
ret = true
|
|
break
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
func sendQuit(tid uint32) {
|
|
user32 := syscall.NewLazyDLL("user32.dll")
|
|
postMessage := user32.NewProc("PostThreadMessageW")
|
|
//nolint:dogsled
|
|
_, _, _ = postMessage.Call(uintptr(tid), WM_QUIT, 0, 0)
|
|
}
|
|
|
|
func FindExecutablePeer(name string) (string, error) {
|
|
exe, err := os.Executable()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
exe, err = filepath.EvalSymlinks(exe)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return filepath.Join(filepath.Dir(exe), name), nil
|
|
}
|
|
|
|
func GetWinProxyStateDir(name string, vmtype define.VMType) (string, error) {
|
|
dir, err := GetDataDir(vmtype)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
stateDir := filepath.Join(dir, name)
|
|
if err = os.MkdirAll(stateDir, 0755); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return stateDir, nil
|
|
}
|
|
|
|
func ToDist(name string) string {
|
|
if !strings.HasPrefix(name, "podman") {
|
|
name = "podman-" + name
|
|
}
|
|
return name
|
|
}
|
|
|
|
func GetEnvSetString(env string, val string) string {
|
|
return fmt.Sprintf("$Env:%s=\"%s\"", env, val)
|
|
}
|