go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/logdog/client/butlerlib/streamclient/client.go (about)

     1  // Copyright 2019 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 streamclient
    16  
    17  import (
    18  	"context"
    19  	"io"
    20  	"strings"
    21  	"time"
    22  
    23  	"go.chromium.org/luci/common/clock"
    24  	"go.chromium.org/luci/common/clock/clockflag"
    25  	"go.chromium.org/luci/common/errors"
    26  	"go.chromium.org/luci/logdog/api/logpb"
    27  	"go.chromium.org/luci/logdog/client/butlerlib/streamproto"
    28  	"go.chromium.org/luci/logdog/common/types"
    29  )
    30  
    31  // This is populated via init() functions in this package.
    32  var protocolRegistry = map[string]dialFactory{}
    33  
    34  // dialFactory takes an implementation-specific address (e.g. `localhost:1234`
    35  // or `/path/to/fifo`), and returns a `dialer` function which can be invoked to
    36  // open a new, raw, connection to the butler.
    37  type dialFactory func(addr string) (dialer, error)
    38  
    39  // dialer is called by Client for every new stream created, and is expected to:
    40  //   - open a connection (as appropriate) to the dialer's destination
    41  //   - "handshake" with the opened connection (if needed)
    42  //   - return an appropriate writer type around the connection.
    43  type dialer interface {
    44  	// if forProcess is true, this must do its best to return an *os.File.
    45  	//
    46  	// If the implementation fails to do so, then it will cause "os/exec" to
    47  	// allocate an extra goroutine and copy loop when this stream is attached to
    48  	// a command's stdout/stderr.
    49  	DialStream(forProcess bool, f streamproto.Flags) (io.WriteCloser, error)
    50  	DialDgramStream(f streamproto.Flags) (DatagramStream, error)
    51  }
    52  
    53  // Client is a client to a local LogDog Butler.
    54  //
    55  // The methods here allow you to open a stream (text, binary or datagram) which
    56  // you can then use to send data to LogDog.
    57  type Client struct {
    58  	dial dialer
    59  
    60  	ns types.StreamName
    61  }
    62  
    63  // New instantiates a new Client instance. This type of instance will be parsed
    64  // from the supplied path string, which takes the form:
    65  //
    66  //	<protocol>:<protocol-specific-spec>
    67  //
    68  // # Supported protocols
    69  //
    70  // Below is the list of all supported protocols:
    71  //
    72  //	unix:/path/to/socket (POSIX only)
    73  //
    74  // Connects to a UNIX domain socket at "/path/to/socket".
    75  // This is the preferred protocol for Linux/Mac.
    76  //
    77  //	net.pipe:name (Windows only)
    78  //
    79  // Connects to a local Windows named pipe "\\.\pipe\name". This is the preferred
    80  // protocol for Windows.
    81  //
    82  //	null (All platforms)
    83  //
    84  // Sinks all connections and writes into a null data sink. Useful for tests, or
    85  // for running programs which use logdog but you don't care about their logdog
    86  // outputs.
    87  //
    88  //	fake:$id
    89  //
    90  // Connects to an in-memory Fake created in this package by calling NewFake.
    91  // The string `fake:$id` can be obtained by calling the Fake's StreamServerPath
    92  // method.
    93  func New(path string, namespace types.StreamName) (*Client, error) {
    94  	parts := strings.SplitN(path, ":", 2)
    95  	protocol, value := parts[0], ""
    96  	if len(parts) == 2 {
    97  		value = parts[1]
    98  	}
    99  
   100  	if f, ok := protocolRegistry[protocol]; ok {
   101  		dial, err := f(value)
   102  		if err != nil {
   103  			return nil, errors.Annotate(err, "opening path %q", path).Err()
   104  		}
   105  		return &Client{dial, namespace}, nil
   106  	}
   107  	return nil, errors.Reason("no protocol registered for [%s]", parts[0]).Err()
   108  }
   109  
   110  func (c *Client) mkOptions(ctx context.Context, name types.StreamName, opts []Option) (ret options, err error) {
   111  	ret.desc, ret.forProcess = RenderOptions(opts...)
   112  	ret.desc.Name = streamproto.StreamNameFlag(c.ns.Concat(name))
   113  	if time.Time(ret.desc.Timestamp).IsZero() {
   114  		ret.desc.Timestamp = clockflag.Time(clock.Now(ctx))
   115  	}
   116  	if ret.desc.ContentType == "" {
   117  		ret.desc.ContentType = string(ret.desc.Type.DefaultContentType())
   118  	}
   119  
   120  	if err = ret.desc.Descriptor().Validate(false); err != nil {
   121  		err = errors.Annotate(err, "invalid stream descriptor").Err()
   122  	}
   123  	return
   124  }
   125  
   126  // NewStream returns a new open stream to the butler.
   127  //
   128  // By default this is text-based (line-oriented); pass Binary for a binary stream.
   129  //
   130  // Text streams look for newlines to delimit log chunks.
   131  // Binary streams use fixed size chunks to delimit log chunks.
   132  func (c *Client) NewStream(ctx context.Context, name types.StreamName, opts ...Option) (io.WriteCloser, error) {
   133  	fullOpts, err := c.mkOptions(ctx, name, opts)
   134  	if err != nil {
   135  		return nil, err
   136  	}
   137  	ret, err := c.dial.DialStream(fullOpts.forProcess, fullOpts.desc)
   138  	return ret, errors.Annotate(err, "attempting to connect stream %q", name).Err()
   139  }
   140  
   141  // NewDatagramStream returns a new datagram stream to the butler.
   142  //
   143  // Datagram streams allow you to send messages without having to demark the
   144  // separation between messages.
   145  //
   146  // NOTE: It is an error to pass ForProcess as an Option (see documentation on
   147  // ForProcess for more detail).
   148  func (c *Client) NewDatagramStream(ctx context.Context, name types.StreamName, opts ...Option) (DatagramStream, error) {
   149  	newOpts := make([]Option, 0, len(opts)+1)
   150  	newOpts = append(newOpts, opts...)
   151  	newOpts = append(newOpts, func(o *options) {
   152  		o.desc.Type = streamproto.StreamType(logpb.StreamType_DATAGRAM)
   153  	})
   154  
   155  	fullOpts, err := c.mkOptions(ctx, name, newOpts)
   156  	if err != nil {
   157  		return nil, err
   158  	}
   159  	if fullOpts.forProcess {
   160  		return nil, errors.Reason("cannot specify ForProcess on a datagram stream").Err()
   161  	}
   162  	ret, err := c.dial.DialDgramStream(fullOpts.desc)
   163  	return ret, errors.Annotate(err, "attempting to connect datagram stream %q", name).Err()
   164  }
   165  
   166  // GetNamespace returns the LOGDOG_NAMESPACE value associated with this Client.
   167  //
   168  // Safe to call on a nil client; will return an empty StreamName.
   169  func (c *Client) GetNamespace() types.StreamName {
   170  	if c == nil {
   171  		return types.StreamName("")
   172  	}
   173  	return c.ns
   174  }