go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/logdog/client/butler/streamserver/streamserver.go (about) 1 // Copyright 2015 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package streamserver 16 17 import ( 18 "context" 19 "fmt" 20 "io" 21 "net" 22 "sync" 23 "sync/atomic" 24 25 "go.chromium.org/luci/common/logging" 26 "go.chromium.org/luci/common/runtime/paniccatcher" 27 "go.chromium.org/luci/logdog/api/logpb" 28 "go.chromium.org/luci/logdog/client/butlerlib/streamproto" 29 ) 30 31 type listener interface { 32 io.Closer 33 Accept() (io.ReadCloser, error) 34 Addr() net.Addr 35 } 36 37 func mkListener(l net.Listener) listener { 38 return &accepterImpl{l} 39 } 40 41 type accepterImpl struct { 42 net.Listener 43 } 44 45 func (a *accepterImpl) Accept() (io.ReadCloser, error) { 46 conn, err := a.Listener.Accept() 47 if err != nil { 48 return nil, err 49 } 50 return conn.(io.ReadCloser), nil 51 } 52 53 // streamParams are parameters representing a negotiated stream ready to 54 // deliver. 55 type streamParams struct { 56 // The stream's ReadCloser connection. 57 rc io.ReadCloser 58 // Negotiated stream descriptor. 59 descriptor *logpb.LogStreamDescriptor 60 } 61 62 // StreamServer implements a Logdog Butler listener. 63 // 64 // This holds a named pipe listener (either a unix domain socket or windows 65 // Named Pipe, depending on the platform). Local processes on the machine may 66 // connect to this named pipe to stream data to Logdog. 67 type StreamServer struct { 68 log logging.Logger 69 70 // address is the string returned by the Address method. 71 address string 72 73 // gen is a generator function that is called to produce the stream server's 74 // Listener. On success, it returns the instantiated Listener, which will be 75 // closed when the stream server is closed. 76 gen func() (listener, error) 77 l listener 78 laddr string 79 80 streamParamsC chan *streamParams 81 closedC chan struct{} 82 acceptFinishedC chan struct{} 83 84 // closed is an atomically-protected integer. If its value is 1, the stream 85 // server has been closed. 86 closed int32 87 88 // discardC is a testing channel. If not nil, failed client connections will 89 // be written here. 90 discardC chan *streamClient 91 } 92 93 func (s *StreamServer) String() string { 94 return fmt.Sprintf("%T(%s)", s, s.laddr) 95 } 96 97 // Address returns a string that can be used by the "streamclient" package to 98 // return a client for this StreamServer. 99 // 100 // Full package is: 101 // go.chromium.org/luci/logdog/butlerlib/streamclient 102 func (s *StreamServer) Address() string { 103 return s.address 104 } 105 106 // Listen performs initial connection and setup and runs a goroutine to listen 107 // for new connections. 108 func (s *StreamServer) Listen() error { 109 // Create a listener (OS-specific). 110 var err error 111 s.l, err = s.gen() 112 if err != nil { 113 return err 114 } 115 116 s.laddr = s.l.Addr().String() 117 s.streamParamsC = make(chan *streamParams) 118 s.closedC = make(chan struct{}) 119 s.acceptFinishedC = make(chan struct{}) 120 121 // Poll the Listener for new connections in a separate goroutine. This will 122 // terminate when the server is Close()d. 123 go s.serve() 124 return nil 125 } 126 127 // Next blocks, returning a new Stream when one is available. If the stream 128 // server has closed, this will return nil. 129 func (s *StreamServer) Next() (io.ReadCloser, *logpb.LogStreamDescriptor) { 130 if streamParams, ok := <-s.streamParamsC; ok { 131 return streamParams.rc, streamParams.descriptor 132 } 133 return nil, nil 134 } 135 136 // Close terminates the stream server, cleaning up resources. 137 func (s *StreamServer) Close() { 138 if s.l == nil { 139 panic("server is not currently serving") 140 } 141 142 // Mark that we've been closed. 143 atomic.StoreInt32(&s.closed, 1) 144 145 // Close our Listener. This will cause our 'Accept' goroutine to terminate. 146 s.l.Close() 147 close(s.closedC) 148 <-s.acceptFinishedC 149 150 // Close our streamParamsC to signal that we're closed. Any blocking Next will 151 // drain the channel, then return with nil. 152 close(s.streamParamsC) 153 s.l = nil 154 } 155 156 // Continuously pulls connections from the supplied Listener and returns them as 157 // connections to streamParamsC for consumption by Next(). 158 func (s *StreamServer) serve() { 159 defer close(s.acceptFinishedC) 160 161 nextID := 0 162 clientWG := sync.WaitGroup{} 163 for { 164 s.log.Debugf("Beginning Accept() loop cycle.") 165 conn, err := s.l.Accept() 166 if err != nil { 167 if atomic.LoadInt32(&s.closed) != 0 { 168 s.log.Debugf("Error during Accept() encountered (closed): %s", err) 169 } else { 170 s.log.Errorf("Error during Accept(): %s", err) 171 } 172 break 173 } 174 175 // Spawn a goroutine to handle this connection. This goroutine will take 176 // ownership of the connection, closing it as appropriate. 177 client := &streamClient{ 178 log: s.log, 179 closedC: s.closedC, 180 id: nextID, 181 conn: conn, 182 } 183 184 clientWG.Add(1) 185 go func() { 186 defer clientWG.Done() 187 188 var params *streamParams 189 paniccatcher.Do(func() { 190 var err error 191 params, err = client.handle() 192 if err != nil { 193 s.log.Errorf("Failed to negotitate stream client: %s", err) 194 params = nil 195 } 196 }, func(p *paniccatcher.Panic) { 197 s.log.Errorf("Panic during client handshake:\n%s\n%s", p.Reason, p.Stack) 198 params = nil 199 }) 200 201 // Did the negotiation fail? 202 if params != nil { 203 // Punt the client to our Next function. If we close while waiting, close 204 // the Client. 205 select { 206 case s.streamParamsC <- params: 207 break 208 209 case <-s.closedC: 210 s.log.Warningf("Closed with client in hand. Cleaning up client.") 211 params.rc.Close() 212 params = nil 213 } 214 } 215 216 // (Testing) write failed connections to discardC if configured. 217 if params == nil && s.discardC != nil { 218 s.discardC <- client 219 } 220 }() 221 nextID++ 222 } 223 224 // Wait for client connections to finish. 225 clientWG.Wait() 226 s.log.Infof("Exiting serve loop. Served %d connections", nextID) 227 } 228 229 // streamClient manages a single client connection. 230 type streamClient struct { 231 log logging.Logger 232 233 closedC chan struct{} // Signal channel to indicate that the server has closed. 234 id int // Client ID, used for debugging correlation. 235 conn io.ReadCloser // The underlying client connection. 236 237 // decoupleMu is used to ensure that decoupleConn is called at most one time. 238 decoupleMu sync.Mutex 239 } 240 241 // handle initializes the supplied connection. On success, it will prepare 242 // the connection as a Butler stream and pass its parameters through the 243 // supplied channel. 244 // 245 // On failure, the connection will be closed. 246 // 247 // This method returns the negotiated stream parameters, or nil if the 248 // negotiation failed and the client was closed. 249 func (c *streamClient) handle() (*streamParams, error) { 250 c.log.Infof("Received new connection.") 251 252 // Close the connection as a failsafe. If we have already decoupled it, this 253 // will end up being a no-op. 254 defer c.closeConn() 255 256 // Perform our handshake. We pass the connection explicitly into this method 257 // because it can get decoupled during operation. 258 p, err := c.handshake() 259 if err != nil { 260 return nil, err 261 } 262 263 // Successful handshake. Forward our properties. 264 return &streamParams{c.decoupleConn(), p}, nil 265 } 266 267 // handshake handles the handshaking and registration of a single connection. If 268 // the connection successfully handshakes, it will be registered as a stream and 269 // supplied to the local streamParamsC; otherwise, it will be closed. 270 // 271 // The client connection opens with a handshake protocol. Once complete, the 272 // connection itself becomes the stream. 273 func (c *streamClient) handshake() (*logpb.LogStreamDescriptor, error) { 274 c.log.Infof("Beginning handshake.") 275 flags := &streamproto.Flags{} 276 if err := flags.FromHandshake(c.conn); err != nil { 277 return nil, err 278 } 279 return flags.Descriptor(), nil 280 } 281 282 // Closes the underlying connection. 283 func (c *streamClient) closeConn() { 284 conn := c.decoupleConn() 285 if conn != nil { 286 if err := conn.Close(); err != nil { 287 c.log.Warningf("Error on connection close: %s", err) 288 } 289 } 290 } 291 292 // Decouples the active connection, returning it and setting the connection to 293 // nil. 294 func (c *streamClient) decoupleConn() (conn io.ReadCloser) { 295 c.decoupleMu.Lock() 296 defer c.decoupleMu.Unlock() 297 298 conn, c.conn = c.conn, nil 299 return 300 } 301 302 // New creates a new StreamServer. 303 // 304 // This has a platform-specific implementation. 305 // 306 // On Mac/Linux, this makes a Unix Domain Socket based server, and `path` is 307 // required to be the absolute path to a filesystem location which is suitable 308 // for a domain socket. The location will be removed prior to, and after, use. 309 // 310 // On Windows, this makes a Named Pipe based server. `path` must be a valid UNC 311 // path component, and will be used to listen on the named pipe 312 // "\\.\$path.$PID.$UNIQUE" where $PID is the process id of the current process 313 // and $UNIQUE is a monotonically increasing value within this process' memory. 314 // 315 // `path` may be empty and a unique name will be chosen for you. 316 func New(ctx context.Context, path string) (*StreamServer, error) { 317 return newStreamServer(ctx, path) 318 }