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 }