src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/eval/port.go (about) 1 package eval 2 3 import ( 4 "bufio" 5 "errors" 6 "fmt" 7 "io" 8 "os" 9 "sync" 10 "sync/atomic" 11 12 "src.elv.sh/pkg/eval/errs" 13 "src.elv.sh/pkg/eval/vals" 14 "src.elv.sh/pkg/strutil" 15 ) 16 17 // Port conveys data stream. It always consists of a byte band and a channel band. 18 type Port struct { 19 File *os.File 20 Chan chan any 21 closeFile bool 22 closeChan bool 23 24 // The following two fields are populated as an additional control mechanism 25 // for output ports. When no more value should be send on Chan, sendError is 26 // populated and sendStop is closed. This is used for both detection of 27 // reader termination (see readerGone below) and closed ports. 28 sendStop chan struct{} 29 sendError *error 30 31 // Only populated in output ports writing to another command in a pipeline. 32 // When the reading end of the pipe exits, it stores true in readerGone. 33 // This is used to check if an external command killed by SIGPIPE is caused 34 // by the termination of the reader of the pipe. 35 readerGone *atomic.Bool 36 } 37 38 // ErrPortDoesNotSupportValueOutput is thrown when writing to a port that does 39 // not support value output. 40 var ErrPortDoesNotSupportValueOutput = errors.New("port does not support value output") 41 42 // A closed channel, suitable as a value for Port.sendStop when there is no 43 // reader to start with. 44 var closedSendStop = make(chan struct{}) 45 46 func init() { close(closedSendStop) } 47 48 // Returns a copy of the Port with the Close* flags unset. 49 func (p *Port) fork() *Port { 50 return &Port{p.File, p.Chan, false, false, p.sendStop, p.sendError, p.readerGone} 51 } 52 53 // Closes a Port. 54 func (p *Port) close() { 55 if p == nil { 56 return 57 } 58 if p.closeFile { 59 p.File.Close() 60 } 61 if p.closeChan { 62 close(p.Chan) 63 } 64 } 65 66 var ( 67 // ClosedChan is a closed channel, suitable as a placeholder input channel. 68 ClosedChan = getClosedChan() 69 // BlackholeChan is a channel that absorbs all values written to it, 70 // suitable as a placeholder output channel. 71 BlackholeChan = getBlackholeChan() 72 // DevNull is /dev/null, suitable as a placeholder file for either input or 73 // output. 74 DevNull = getDevNull() 75 76 // DummyInputPort is a port made up from DevNull and ClosedChan, suitable as 77 // a placeholder input port. 78 DummyInputPort = &Port{File: DevNull, Chan: ClosedChan} 79 // DummyOutputPort is a port made up from DevNull and BlackholeChan, 80 // suitable as a placeholder output port. 81 DummyOutputPort = &Port{File: DevNull, Chan: BlackholeChan} 82 83 // DummyPorts contains 3 dummy ports, suitable as stdin, stdout and stderr. 84 DummyPorts = []*Port{DummyInputPort, DummyOutputPort, DummyOutputPort} 85 ) 86 87 func getClosedChan() chan any { 88 ch := make(chan any) 89 close(ch) 90 return ch 91 } 92 93 func getBlackholeChan() chan any { 94 ch := make(chan any) 95 go func() { 96 for range ch { 97 } 98 }() 99 return ch 100 } 101 102 func getDevNull() *os.File { 103 f, err := os.Open(os.DevNull) 104 if err != nil { 105 fmt.Fprintf(os.Stderr, 106 "cannot open %s, shell might not function normally\n", os.DevNull) 107 } 108 return f 109 } 110 111 // PipePort returns an output *Port whose value and byte components are both 112 // piped. The supplied functions are called on a separate goroutine with the 113 // read ends of the value and byte components of the port. It also returns a 114 // function to clean up the port and wait for the callbacks to finish. 115 func PipePort(vCb func(<-chan any), bCb func(*os.File)) (*Port, func(), error) { 116 r, w, err := os.Pipe() 117 if err != nil { 118 return nil, nil, err 119 } 120 ch := make(chan any, outputCaptureBufferSize) 121 122 var wg sync.WaitGroup 123 wg.Add(2) 124 go func() { 125 defer wg.Done() 126 vCb(ch) 127 }() 128 go func() { 129 defer wg.Done() 130 defer r.Close() 131 bCb(r) 132 }() 133 134 port := &Port{Chan: ch, closeChan: true, File: w, closeFile: true} 135 done := func() { 136 port.close() 137 wg.Wait() 138 } 139 return port, done, nil 140 } 141 142 // CapturePort returns an output [*Port] whose value and byte components are 143 // saved separately. It also returns a function to call to obtain the captured 144 // output. 145 func CapturePort() (*Port, func() ([]any, []byte), error) { 146 var values []any 147 var bytes []byte 148 port, done, err := PipePort( 149 func(ch <-chan any) { 150 for v := range ch { 151 values = append(values, v) 152 } 153 }, 154 func(r *os.File) { 155 var err error 156 bytes, err = io.ReadAll(r) 157 if err != nil && err != io.EOF { 158 logger.Println("error on reading:", err) 159 } 160 }, 161 ) 162 if err != nil { 163 return nil, nil, err 164 } 165 return port, func() ([]any, []byte) { 166 done() 167 return values, bytes 168 }, nil 169 } 170 171 // ValueCapturePort returns an output [*Port] whose value and byte components 172 // are saved, with bytes saved one string value per line. It also returns a 173 // function to call to obtain the captured output. 174 func ValueCapturePort() (*Port, func() []any, error) { 175 vs := []any{} 176 var m sync.Mutex 177 port, done, err := PipePort( 178 func(ch <-chan any) { 179 for v := range ch { 180 m.Lock() 181 vs = append(vs, v) 182 m.Unlock() 183 } 184 }, 185 func(r *os.File) { 186 buffered := bufio.NewReader(r) 187 for { 188 line, err := buffered.ReadString('\n') 189 if line != "" { 190 v := strutil.ChopLineEnding(line) 191 m.Lock() 192 vs = append(vs, v) 193 m.Unlock() 194 } 195 if err != nil { 196 if err != io.EOF { 197 logger.Println("error on reading:", err) 198 } 199 break 200 } 201 } 202 }) 203 if err != nil { 204 return nil, nil, err 205 } 206 return port, func() []any { 207 done() 208 return vs 209 }, nil 210 } 211 212 // StringCapturePort is like [ValueCapturePort], but converts value outputs by 213 // stringifying them and prepending an output marker. 214 func StringCapturePort() (*Port, func() []string, error) { 215 var lines []string 216 var mu sync.Mutex 217 addLine := func(line string) { 218 mu.Lock() 219 defer mu.Unlock() 220 lines = append(lines, line) 221 } 222 port, done, err := PipePort( 223 func(ch <-chan any) { 224 for v := range ch { 225 addLine("▶ " + vals.ToString(v)) 226 } 227 }, 228 func(r *os.File) { 229 bufr := bufio.NewReader(r) 230 for { 231 line, err := bufr.ReadString('\n') 232 if err != nil { 233 if err != io.EOF { 234 addLine("i/o error: " + err.Error()) 235 } 236 break 237 } 238 addLine(strutil.ChopLineEnding(line)) 239 } 240 }) 241 if err != nil { 242 return nil, nil, err 243 } 244 return port, func() []string { 245 done() 246 return lines 247 }, nil 248 } 249 250 // Buffer size for the channel to use in FilePort. The value has been chosen 251 // arbitrarily. 252 const filePortChanSize = 32 253 254 // FilePort returns an output *Port where the byte component is the file itself, 255 // and the value component is converted to an internal channel that writes 256 // each value to the file, prepending with a prefix. It also returns a cleanup 257 // function, which should be called when the *Port is no longer needed. 258 func FilePort(f *os.File, valuePrefix string) (*Port, func()) { 259 ch := make(chan any, filePortChanSize) 260 relayDone := make(chan struct{}) 261 go func() { 262 for v := range ch { 263 f.WriteString(valuePrefix) 264 f.WriteString(vals.ReprPlain(v)) 265 f.WriteString("\n") 266 } 267 close(relayDone) 268 }() 269 return &Port{File: f, Chan: ch}, func() { 270 close(ch) 271 <-relayDone 272 } 273 } 274 275 // PortsFromStdFiles is a shorthand for calling PortsFromFiles with os.Stdin, 276 // os.Stdout and os.Stderr. 277 func PortsFromStdFiles(prefix string) ([]*Port, func()) { 278 return PortsFromFiles([3]*os.File{os.Stdin, os.Stdout, os.Stderr}, prefix) 279 } 280 281 // PortsFromFiles builds 3 ports from 3 files. It also returns a function that 282 // should be called when the ports are no longer needed. 283 func PortsFromFiles(files [3]*os.File, prefix string) ([]*Port, func()) { 284 port1, cleanup1 := FilePort(files[1], prefix) 285 port2, cleanup2 := FilePort(files[2], prefix) 286 return []*Port{{File: files[0], Chan: ClosedChan}, port1, port2}, func() { 287 cleanup1() 288 cleanup2() 289 } 290 } 291 292 // ValueOutput defines the interface through which builtin commands access the 293 // value output. 294 // 295 // The value output is backed by two channels, one for writing output, another 296 // for the back-chanel signal that the reader of the channel has gone. 297 type ValueOutput interface { 298 // Outputs a value. Returns errs.ReaderGone if the reader is gone. 299 Put(v any) error 300 } 301 302 type valueOutput struct { 303 data chan<- any 304 sendStop <-chan struct{} 305 sendError *error 306 } 307 308 func (vo valueOutput) Put(v any) error { 309 select { 310 case vo.data <- v: 311 return nil 312 case <-vo.sendStop: 313 return *vo.sendError 314 } 315 } 316 317 // ByteOutput defines the interface through which builtin commands access the 318 // byte output. 319 // 320 // It is a thin wrapper around the underlying *os.File value, only exposing 321 // the necessary methods for writing bytes and strings, and converting any 322 // syscall.EPIPE errors to errs.ReaderGone. 323 type ByteOutput interface { 324 io.Writer 325 io.StringWriter 326 } 327 328 type byteOutput struct { 329 f *os.File 330 } 331 332 func (bo byteOutput) Write(p []byte) (int, error) { 333 n, err := bo.f.Write(p) 334 return n, convertReaderGone(err) 335 } 336 337 func (bo byteOutput) WriteString(s string) (int, error) { 338 n, err := bo.f.WriteString(s) 339 return n, convertReaderGone(err) 340 } 341 342 func convertReaderGone(err error) error { 343 if pathErr, ok := err.(*os.PathError); ok { 344 if pathErr.Err == epipe { 345 return errs.ReaderGone{} 346 } 347 } 348 return err 349 }