github.com/khulnasoft/cli@v0.0.0-20240402070845-01bcad7beefa/cli-plugins/socket/socket.go (about)

     1  package socket
     2  
     3  import (
     4  	"crypto/rand"
     5  	"encoding/hex"
     6  	"errors"
     7  	"io"
     8  	"net"
     9  	"os"
    10  	"runtime"
    11  	"sync"
    12  )
    13  
    14  // EnvKey represents the well-known environment variable used to pass the
    15  // plugin being executed the socket name it should listen on to coordinate with
    16  // the host CLI.
    17  const EnvKey = "DOCKER_CLI_PLUGIN_SOCKET"
    18  
    19  // NewPluginServer creates a plugin server that listens on a new Unix domain
    20  // socket. h is called for each new connection to the socket in a goroutine.
    21  func NewPluginServer(h func(net.Conn)) (*PluginServer, error) {
    22  	// Listen on a Unix socket, with the address being platform-dependent.
    23  	// When a non-abstract address is used, Go will unlink(2) the socket
    24  	// for us once the listener is closed, as documented in
    25  	// [net.UnixListener.SetUnlinkOnClose].
    26  	l, err := net.ListenUnix("unix", &net.UnixAddr{
    27  		Name: socketName("docker_cli_" + randomID()),
    28  		Net:  "unix",
    29  	})
    30  	if err != nil {
    31  		return nil, err
    32  	}
    33  
    34  	if h == nil {
    35  		h = func(net.Conn) {}
    36  	}
    37  
    38  	pl := &PluginServer{
    39  		l: l,
    40  		h: h,
    41  	}
    42  
    43  	go func() {
    44  		defer pl.Close()
    45  		for {
    46  			err := pl.accept()
    47  			if err != nil {
    48  				return
    49  			}
    50  		}
    51  	}()
    52  
    53  	return pl, nil
    54  }
    55  
    56  type PluginServer struct {
    57  	mu     sync.Mutex
    58  	conns  []net.Conn
    59  	l      *net.UnixListener
    60  	h      func(net.Conn)
    61  	closed bool
    62  }
    63  
    64  func (pl *PluginServer) accept() error {
    65  	conn, err := pl.l.Accept()
    66  	if err != nil {
    67  		return err
    68  	}
    69  
    70  	pl.mu.Lock()
    71  	defer pl.mu.Unlock()
    72  
    73  	if pl.closed {
    74  		// Handle potential race between Close and accept.
    75  		conn.Close()
    76  		return errors.New("plugin server is closed")
    77  	}
    78  
    79  	pl.conns = append(pl.conns, conn)
    80  
    81  	go pl.h(conn)
    82  	return nil
    83  }
    84  
    85  // Addr returns the [net.Addr] of the underlying [net.Listener].
    86  func (pl *PluginServer) Addr() net.Addr {
    87  	return pl.l.Addr()
    88  }
    89  
    90  // Close ensures that the server is no longer accepting new connections and
    91  // closes all existing connections. Existing connections will receive [io.EOF].
    92  //
    93  // The error value is that of the underlying [net.Listner.Close] call.
    94  func (pl *PluginServer) Close() error {
    95  	// Close connections first to ensure the connections get io.EOF instead
    96  	// of a connection reset.
    97  	pl.closeAllConns()
    98  
    99  	// Try to ensure that any active connections have a chance to receive
   100  	// io.EOF.
   101  	runtime.Gosched()
   102  
   103  	return pl.l.Close()
   104  }
   105  
   106  func (pl *PluginServer) closeAllConns() {
   107  	pl.mu.Lock()
   108  	defer pl.mu.Unlock()
   109  
   110  	// Prevent new connections from being accepted.
   111  	pl.closed = true
   112  
   113  	for _, conn := range pl.conns {
   114  		conn.Close()
   115  	}
   116  
   117  	pl.conns = nil
   118  }
   119  
   120  func randomID() string {
   121  	b := make([]byte, 16)
   122  	if _, err := rand.Read(b); err != nil {
   123  		panic(err) // This shouldn't happen
   124  	}
   125  	return hex.EncodeToString(b)
   126  }
   127  
   128  // ConnectAndWait connects to the socket passed via well-known env var,
   129  // if present, and attempts to read from it until it receives an EOF, at which
   130  // point cb is called.
   131  func ConnectAndWait(cb func()) {
   132  	socketAddr, ok := os.LookupEnv(EnvKey)
   133  	if !ok {
   134  		// if a plugin compiled against a more recent version of docker/cli
   135  		// is executed by an older CLI binary, ignore missing environment
   136  		// variable and behave as usual
   137  		return
   138  	}
   139  	addr, err := net.ResolveUnixAddr("unix", socketAddr)
   140  	if err != nil {
   141  		return
   142  	}
   143  	conn, err := net.DialUnix("unix", nil, addr)
   144  	if err != nil {
   145  		return
   146  	}
   147  
   148  	go func() {
   149  		b := make([]byte, 1)
   150  		for {
   151  			_, err := conn.Read(b)
   152  			if errors.Is(err, io.EOF) {
   153  				cb()
   154  				return
   155  			}
   156  		}
   157  	}()
   158  }