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  }