Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 1 addition & 9 deletions pkg/machine/apple/apple.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,7 @@ import (

const applehvMACAddress = "5a:94:ef:e4:0c:ee"

var (
gvProxyWaitBackoff = 500 * time.Millisecond
gvProxyMaxBackoffAttempts = 6
ignitionSocketName = "ignition.sock"
)
var ignitionSocketName = "ignition.sock"

// ResizeDisk uses os truncate to resize (only larger) a raw disk. the input size
// is assumed GiB
Expand Down Expand Up @@ -77,10 +73,6 @@ func StartGenericAppleVM(mc *vmconfigs.MachineConfig, cmdBinary string, bootload
if err != nil {
return nil, nil, err
}
// Wait on gvproxy to be running and aware
if err := sockets.WaitForSocketWithBackoffs(gvProxyMaxBackoffAttempts, gvProxyWaitBackoff, gvproxySocket.GetPath(), "gvproxy"); err != nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are just two callers of this function which you removed so WaitForSocketWithBackoffs should be removed as well IMO

return nil, nil, err
}
netDevice.SetUnixSocketPath(gvproxySocket.GetPath())

// create a one-time virtual machine for starting because we dont want all this information in the
Expand Down
5 changes: 4 additions & 1 deletion pkg/machine/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ import (
"go.podman.io/podman/v6/pkg/machine/vmconfigs"
)

const apiUpTimeout = 20 * time.Second
const (
apiUpTimeout = 20 * time.Second
GvProxyReadyTimeout = 5 * time.Second
)

var ForwarderBinaryName = "gvproxy"

Expand Down
14 changes: 12 additions & 2 deletions pkg/machine/gvproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"io/fs"
"path/filepath"
"strconv"
"time"

Expand Down Expand Up @@ -33,7 +34,8 @@ func readPIDFileWithRetry(f define.VMFile) ([]byte, error) {
return f.Read()
}

// CleanupGVProxy reads the --pid-file for gvproxy attempts to stop it
// CleanupGVProxy reads the --pid-file for gvproxy, stops the process,
// removes the PID file, and cleans up the notification socket.
func CleanupGVProxy(f define.VMFile) error {
gvPid, err := readPIDFileWithRetry(f)
if err != nil {
Expand All @@ -51,5 +53,13 @@ func CleanupGVProxy(f define.VMFile) error {
if err := waitOnProcess(proxyPid); err != nil {
return err
}
return removeGVProxyPIDFile(f)
if err := removeGVProxyPIDFile(f); err != nil {
return err
}

// Remove notification socket after gvproxy has exited
runtimeDir := filepath.Dir(f.GetPath())
CleanupGvProxyNotifySocket(runtimeDir)

return nil
}
160 changes: 160 additions & 0 deletions pkg/machine/gvproxy_notify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package machine

import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"os"
"path/filepath"

gvProxyTypes "github.com/containers/gvisor-tap-vsock/pkg/types"
"github.com/sirupsen/logrus"
)

const notifySocketName = "gvproxy-notify.sock"

// GvProxyNotifier listens on a unix socket for JSON notification messages
// from gvproxy.
type GvProxyNotifier struct {
listener *net.UnixListener
socketPath string
readyCh chan struct{}
errorCh chan error
}

// SocketPath returns the path to the notification unix socket.
func (n *GvProxyNotifier) SocketPath() string {
return n.socketPath
}

// NewGvProxyNotifier creates a notification listener socket in the given runtime directory.
//
// The socket begins accepting connections at the kernel level immediately upon creation
// (via net.Listen), so gvproxy can be started and connect before Start() is called without
// losing messages. The kernel backlog buffers both the connection and any data sent on it
// until Accept()/Read() consume them.
//
// The notifier is only used during machine start. Close() stops the listener but
// intentionally leaves the socket file on disk because gvproxy remains running after
// start completes and may still dial the socket for later notifications. The socket
// file is removed by CleanupGvProxyNotifySocket, which is called from CleanupGVProxy
// during machine stop after gvproxy has exited. Any stale socket from a previous run
// is removed at the top of this constructor before creating a new listener.
func NewGvProxyNotifier(runtimeDir string) (*GvProxyNotifier, error) {
socketPath := filepath.Join(runtimeDir, notifySocketName)

if err := os.Remove(socketPath); err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("removing old notification socket: %w", err)
}

listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: socketPath, Net: "unix"})
if err != nil {
return nil, fmt.Errorf("creating notification listener: %w", err)
}

// Prevent Close() from removing the socket file. gvproxy may still
// dial it for notifications after the listener is closed. The file is
// removed by CleanupGvProxyNotifySocket during machine stop.
listener.SetUnlinkOnClose(false)

return &GvProxyNotifier{
listener: listener,
socketPath: socketPath,
readyCh: make(chan struct{}, 1),
errorCh: make(chan error, 1),
}, nil
}

// CleanupGvProxyNotifySocket removes the notification socket file from the given runtime directory.
//
// This is called from CleanupGVProxy during machine stop, at which point the notifier
// (which only runs during machine start) has long since closed its listener.
func CleanupGvProxyNotifySocket(runtimeDir string) {
socketPath := filepath.Join(runtimeDir, notifySocketName)
if err := os.Remove(socketPath); err != nil && !errors.Is(err, os.ErrNotExist) {
logrus.Debugf("failed to remove gvproxy notification socket: %v", err)
}
}

// Close stops the notifier listener.
//
// The socket file is intentionally left on disk because gvproxy may still dial it for later notifications
// (connection_established, connection_closed).
// The file is cleaned up by CleanupGvProxyNotifySocket after gvproxy exits.
func (n *GvProxyNotifier) Close() {
n.listener.Close()
}

// Start begins accepting connections and processing notifications.
//
// It blocks until the context is canceled or the listener is closed,
// intended to be run as a goroutine.
func (n *GvProxyNotifier) Start(ctx context.Context) {
for {
conn, err := n.listener.Accept()
if err != nil {
if ctx.Err() != nil || errors.Is(err, net.ErrClosed) {
return
}
logrus.Errorf("notification listener accept error: %v", err)
return
}
n.handleConnection(ctx, conn)
}
}

func (n *GvProxyNotifier) handleConnection(ctx context.Context, conn net.Conn) {
defer conn.Close()
decoder := json.NewDecoder(conn)

for {
if ctx.Err() != nil {
return
}

var msg gvProxyTypes.NotificationMessage
if err := decoder.Decode(&msg); err != nil {
if ctx.Err() != nil {
return
}
logrus.Debugf("notification decode error: %v", err)
return
}

logrus.Debugf("gvproxy notification received: type=%s mac=%s", msg.NotificationType, msg.MacAddress)

switch msg.NotificationType {
case gvProxyTypes.Ready:
select {
case n.readyCh <- struct{}{}:
default:
}
case gvProxyTypes.ConnectionEstablished:
logrus.Debugf("gvproxy: VM connected (mac=%s)", msg.MacAddress)
case gvProxyTypes.HypervisorError:
select {
case n.errorCh <- fmt.Errorf("gvproxy reported hypervisor error"):
default:
}
case gvProxyTypes.ConnectionClosed:
select {
case n.errorCh <- fmt.Errorf("gvproxy: VM disconnected unexpectedly (mac=%s)", msg.MacAddress):
default:
}
}
}
}

// WaitReady blocks until the "ready" notification is received or the context expires.
func (n *GvProxyNotifier) WaitReady(ctx context.Context) error {
select {
case <-n.readyCh:
return nil
case err := <-n.errorCh:
return err
case <-ctx.Done():
return fmt.Errorf("timeout waiting for gvproxy ready notification: %w", ctx.Err())
}
}
95 changes: 95 additions & 0 deletions pkg/machine/gvproxy_notify_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//go:build amd64 || arm64

package machine

import (
"context"
"encoding/json"
"errors"
"net"
"os"
"testing"
"time"

gvproxyTypes "github.com/containers/gvisor-tap-vsock/pkg/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestGvProxyNotifier_Ready(t *testing.T) {
dir := t.TempDir()
notifier, err := NewGvProxyNotifier(dir)
require.NoError(t, err)
defer notifier.Close()

go notifier.Start(t.Context())

conn, err := net.Dial("unix", notifier.SocketPath())
require.NoError(t, err)
defer conn.Close()

err = json.NewEncoder(conn).Encode(gvproxyTypes.NotificationMessage{
NotificationType: gvproxyTypes.Ready,
})
require.NoError(t, err)

waitCtx, waitCancel := context.WithTimeout(t.Context(), 2*time.Second)
defer waitCancel()
err = notifier.WaitReady(waitCtx)
assert.NoError(t, err)
}

func TestGvProxyNotifier_HypervisorError(t *testing.T) {
dir := t.TempDir()
notifier, err := NewGvProxyNotifier(dir)
require.NoError(t, err)
defer notifier.Close()

go notifier.Start(t.Context())

conn, err := net.Dial("unix", notifier.SocketPath())
require.NoError(t, err)
defer conn.Close()

err = json.NewEncoder(conn).Encode(gvproxyTypes.NotificationMessage{
NotificationType: gvproxyTypes.HypervisorError,
})
require.NoError(t, err)

waitCtx, waitCancel := context.WithTimeout(t.Context(), 2*time.Second)
defer waitCancel()
err = notifier.WaitReady(waitCtx)
assert.Error(t, err)
assert.Contains(t, err.Error(), "hypervisor error")
}

func TestGvProxyNotifier_Timeout(t *testing.T) {
dir := t.TempDir()
notifier, err := NewGvProxyNotifier(dir)
require.NoError(t, err)
defer notifier.Close()

go notifier.Start(t.Context())

waitCtx, waitCancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
defer waitCancel()
err = notifier.WaitReady(waitCtx)
assert.Error(t, err)
assert.ErrorIs(t, err, context.DeadlineExceeded)
}

func TestCleanupGvProxyNotifySocket(t *testing.T) {
dir := t.TempDir()

notifier, err := NewGvProxyNotifier(dir)
require.NoError(t, err)
socketPath := notifier.SocketPath()
notifier.Close()

_, err = os.Stat(socketPath)
assert.NoError(t, err, "socket file should still exist after Close()")

CleanupGvProxyNotifySocket(dir)
_, err = os.Stat(socketPath)
assert.True(t, errors.Is(err, os.ErrNotExist), "socket file should be gone after cleanup")
}
15 changes: 0 additions & 15 deletions pkg/machine/qemu/stubber.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,6 @@ type QEMUStubber struct {
virtiofsHelpers []virtiofsdHelperCmd
}

var (
gvProxyWaitBackoff = 500 * time.Millisecond
gvProxyMaxBackoffAttempts = 6
)

func (q *QEMUStubber) UserModeNetworkEnabled(*vmconfigs.MachineConfig) bool {
return true
}
Expand Down Expand Up @@ -155,16 +150,6 @@ func (q *QEMUStubber) StartVM(mc *vmconfigs.MachineConfig) (func() error, func()
return nil, nil, err
}

gvProxySock, err := mc.GVProxySocket()
if err != nil {
return nil, nil, err
}

// Wait on gvproxy to be running and aware
if err := sockets.WaitForSocketWithBackoffs(gvProxyMaxBackoffAttempts, gvProxyWaitBackoff, gvProxySock.GetPath(), "gvproxy"); err != nil {
return nil, nil, err
}

dnr, dnw, err := machine.GetDevNullFiles()
if err != nil {
return nil, nil, err
Expand Down
20 changes: 19 additions & 1 deletion pkg/machine/shim/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package shim

import (
"bufio"
"context"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -615,7 +616,7 @@ func Start(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, opts machine.St
}

// start gvproxy and set up the API socket forwarding
forwardSocketPath, forwardingState, err := startNetworking(mc, mp)
forwardSocketPath, forwardingState, gvProxyNotifier, err := startNetworking(mc, mp)
if err != nil {
return err
}
Expand All @@ -629,6 +630,23 @@ func Start(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, opts machine.St
}
callbackFuncs.Add(cleanGv)

// Wait for gvproxy to signal readiness via the notification socket.
if gvProxyNotifier != nil {
notifyCtx, notifyCancel := context.WithCancel(context.Background())
go gvProxyNotifier.Start(notifyCtx)
defer func() {
notifyCancel()
gvProxyNotifier.Close()
}()

gvProxyReadyCtx, gvProxyReadyCancel := context.WithTimeout(context.Background(), machine.GvProxyReadyTimeout)
defer gvProxyReadyCancel()
if err := gvProxyNotifier.WaitReady(gvProxyReadyCtx); err != nil {
return fmt.Errorf("waiting for gvproxy readiness: %w", err)
}
logrus.Debug("gvproxy ready notification received")
}

// if there are generic things that need to be done, a preStart function could be added here
// should it be extensive

Expand Down
Loading
Loading