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 }