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  }