github.com/kolbycrouch/elvish@v0.14.1-0.20210614162631-215b9ac1c423/pkg/eval/port.go (about)

     1  package eval
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"sync"
     9  
    10  	"src.elv.sh/pkg/eval/vals"
    11  	"src.elv.sh/pkg/strutil"
    12  )
    13  
    14  // Port conveys data stream. It always consists of a byte band and a channel band.
    15  type Port struct {
    16  	File      *os.File
    17  	Chan      chan interface{}
    18  	closeFile bool
    19  	closeChan bool
    20  }
    21  
    22  // Returns a copy of the Port with the Close* flags unset.
    23  func (p *Port) fork() *Port {
    24  	return &Port{p.File, p.Chan, false, false}
    25  }
    26  
    27  // Closes a Port.
    28  func (p *Port) close() {
    29  	if p == nil {
    30  		return
    31  	}
    32  	if p.closeFile {
    33  		p.File.Close()
    34  	}
    35  	if p.closeChan {
    36  		close(p.Chan)
    37  	}
    38  }
    39  
    40  var (
    41  	// ClosedChan is a closed channel, suitable as a placeholder input channel.
    42  	ClosedChan = getClosedChan()
    43  	// BlackholeChan is a channel that absorbs all values written to it,
    44  	// suitable as a placeholder output channel.
    45  	BlackholeChan = getBlackholeChan()
    46  	// DevNull is /dev/null, suitable as a placeholder file for either input or
    47  	// output.
    48  	DevNull = getDevNull()
    49  
    50  	// DummyInputPort is a port made up from DevNull and ClosedChan, suitable as
    51  	// a placeholder input port.
    52  	DummyInputPort = &Port{File: DevNull, Chan: ClosedChan}
    53  	// DummyOutputPort is a port made up from DevNull and BlackholeChan,
    54  	// suitable as a placeholder output port.
    55  	DummyOutputPort = &Port{File: DevNull, Chan: BlackholeChan}
    56  )
    57  
    58  func getClosedChan() chan interface{} {
    59  	ch := make(chan interface{})
    60  	close(ch)
    61  	return ch
    62  }
    63  
    64  func getBlackholeChan() chan interface{} {
    65  	ch := make(chan interface{})
    66  	go func() {
    67  		for range ch {
    68  		}
    69  	}()
    70  	return ch
    71  }
    72  
    73  func getDevNull() *os.File {
    74  	f, err := os.Open(os.DevNull)
    75  	if err != nil {
    76  		fmt.Fprintf(os.Stderr,
    77  			"cannot open %s, shell might not function normally\n", os.DevNull)
    78  	}
    79  	return f
    80  }
    81  
    82  // PipePort returns an output *Port whose value and byte components are both
    83  // piped. The supplied functions are called on a separate goroutine with the
    84  // read ends of the value and byte components of the port. It also returns a
    85  // function to clean up the port and wait for the callbacks to finish.
    86  func PipePort(vCb func(<-chan interface{}), bCb func(*os.File)) (*Port, func(), error) {
    87  	r, w, err := os.Pipe()
    88  	if err != nil {
    89  		return nil, nil, err
    90  	}
    91  	ch := make(chan interface{}, outputCaptureBufferSize)
    92  
    93  	var wg sync.WaitGroup
    94  	wg.Add(2)
    95  	go func() {
    96  		defer wg.Done()
    97  		vCb(ch)
    98  	}()
    99  	go func() {
   100  		defer wg.Done()
   101  		defer r.Close()
   102  		bCb(r)
   103  	}()
   104  
   105  	port := &Port{Chan: ch, closeChan: true, File: w, closeFile: true}
   106  	done := func() {
   107  		port.close()
   108  		wg.Wait()
   109  	}
   110  	return port, done, nil
   111  }
   112  
   113  // CapturePort returns an output *Port whose value and byte components are
   114  // both connected to an internal pipe that saves the output. It also returns a
   115  // function to call to obtain the captured output.
   116  func CapturePort() (*Port, func() []interface{}, error) {
   117  	vs := []interface{}{}
   118  	var m sync.Mutex
   119  	port, done, err := PipePort(
   120  		func(ch <-chan interface{}) {
   121  			for v := range ch {
   122  				m.Lock()
   123  				vs = append(vs, v)
   124  				m.Unlock()
   125  			}
   126  		},
   127  		func(r *os.File) {
   128  			buffered := bufio.NewReader(r)
   129  			for {
   130  				line, err := buffered.ReadString('\n')
   131  				if line != "" {
   132  					v := strutil.ChopLineEnding(line)
   133  					m.Lock()
   134  					vs = append(vs, v)
   135  					m.Unlock()
   136  				}
   137  				if err != nil {
   138  					if err != io.EOF {
   139  						logger.Println("error on reading:", err)
   140  					}
   141  					break
   142  				}
   143  			}
   144  		})
   145  	if err != nil {
   146  		return nil, nil, err
   147  	}
   148  	return port, func() []interface{} {
   149  		done()
   150  		return vs
   151  	}, nil
   152  }
   153  
   154  // StringCapturePort is like CapturePort, but processes value outputs by
   155  // stringifying them and prepending an output marker.
   156  func StringCapturePort() (*Port, func() []string, error) {
   157  	var lines []string
   158  	var mu sync.Mutex
   159  	addLine := func(line string) {
   160  		mu.Lock()
   161  		defer mu.Unlock()
   162  		lines = append(lines, line)
   163  	}
   164  	port, done, err := PipePort(
   165  		func(ch <-chan interface{}) {
   166  			for v := range ch {
   167  				addLine("▶ " + vals.ToString(v))
   168  			}
   169  		},
   170  		func(r *os.File) {
   171  			bufr := bufio.NewReader(r)
   172  			for {
   173  				line, err := bufr.ReadString('\n')
   174  				if err != nil {
   175  					if err != io.EOF {
   176  						addLine("i/o error: " + err.Error())
   177  					}
   178  					break
   179  				}
   180  				addLine(strutil.ChopLineEnding(line))
   181  			}
   182  		})
   183  	if err != nil {
   184  		return nil, nil, err
   185  	}
   186  	return port, func() []string {
   187  		done()
   188  		return lines
   189  	}, nil
   190  }
   191  
   192  // Buffer size for the channel to use in FilePort. The value has been chosen
   193  // arbitrarily.
   194  const filePortChanSize = 32
   195  
   196  // FilePort returns an output *Port where the byte component is the file itself,
   197  // and the value component is converted to an internal channel that writes
   198  // each value to the file, prepending with a prefix. It also returns a cleanup
   199  // function, which should be called when the *Port is no longer needed.
   200  func FilePort(f *os.File, valuePrefix string) (*Port, func()) {
   201  	ch := make(chan interface{}, filePortChanSize)
   202  	relayDone := make(chan struct{})
   203  	go func() {
   204  		for v := range ch {
   205  			f.WriteString(valuePrefix)
   206  			f.WriteString(vals.Repr(v, vals.NoPretty))
   207  			f.WriteString("\n")
   208  		}
   209  		close(relayDone)
   210  	}()
   211  	return &Port{File: f, Chan: ch}, func() {
   212  		close(ch)
   213  		<-relayDone
   214  	}
   215  }
   216  
   217  // PortsFromStdFiles is a shorthand for calling PortsFromFiles with os.Stdin,
   218  // os.Stdout and os.Stderr.
   219  func PortsFromStdFiles(prefix string) ([]*Port, func()) {
   220  	return PortsFromFiles([3]*os.File{os.Stdin, os.Stdout, os.Stderr}, prefix)
   221  }
   222  
   223  // PortsFromFiles builds 3 ports from 3 files. It also returns a function that
   224  // should be called when the ports are no longer needed.
   225  func PortsFromFiles(files [3]*os.File, prefix string) ([]*Port, func()) {
   226  	port1, cleanup1 := FilePort(files[1], prefix)
   227  	port2, cleanup2 := FilePort(files[2], prefix)
   228  	return []*Port{{File: files[0], Chan: ClosedChan}, port1, port2}, func() {
   229  		cleanup1()
   230  		cleanup2()
   231  	}
   232  }