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 }