github.com/haraldrudell/parl@v0.4.176/mains/stdin-reader.go (about)

     1  /*
     2  © 2024–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/)
     3  ISC License
     4  */
     5  
     6  package mains
     7  
     8  import (
     9  	"fmt"
    10  	"io"
    11  	"os"
    12  	"sync/atomic"
    13  
    14  	"github.com/haraldrudell/parl"
    15  	"github.com/haraldrudell/parl/perrors"
    16  )
    17  
    18  // StdinReader is a reader wrapping the unclosable os.Stdin.Read
    19  //   - on error, the error is sent to addError and EOF is returned
    20  type StdinReader struct {
    21  	// optionl error submitting function
    22  	addError parl.AddError
    23  	// whether error has occured in [StdinReader.Read]
    24  	isClosed atomic.Bool
    25  	// optional value set to true on error
    26  	isError *atomic.Bool
    27  }
    28  
    29  var _ io.Reader = &StdinReader{}
    30  
    31  // NewStdinReader returns a reader that closes on error
    32  //   - addError is an optional function receiving errors occurring in [os.Stdin.Read].
    33  //     if missing, errors are printed to stderr
    34  //   - isError is an optional atomic set to true on first error
    35  func NewStdinReader(addError parl.AddError, isError *atomic.Bool) (reader *StdinReader) {
    36  	return &StdinReader{
    37  		addError: addError,
    38  		isError:  isError,
    39  	}
    40  }
    41  
    42  // Read reads from standard input
    43  //   - on error, the reader closes
    44  //   - errors are submitted separately or printed to stderr and not returned
    45  //   - the only error returned is [io.EOF]
    46  //   - [os.Stdin] cannot be closed so a blocking read cannot be canceled
    47  //   - if another process closes stdin, on the next keypress an error will result
    48  //   - on process exit, Read may hang until enter is pressed
    49  func (r *StdinReader) Read(p []byte) (n int, err error) {
    50  
    51  	// already closed case
    52  	if r.isClosed.Load() {
    53  		err = io.EOF
    54  		return
    55  	}
    56  
    57  	var isPanic bool
    58  
    59  	n, isPanic, err = r.read(p)
    60  
    61  	// no error case
    62  	if err == nil {
    63  		return
    64  	}
    65  
    66  	// store error condition in object
    67  	r.isClosed.Store(true)
    68  	// if isError present, note error has occurred
    69  	if r.isError != nil {
    70  		r.isError.Store(true)
    71  	}
    72  
    73  	// do not submit or print EOF error
    74  	//	- indication is r.isError true
    75  	if err == io.EOF {
    76  		return
    77  	}
    78  
    79  	// if another process closes stdin:
    80  	// os.StdinRead error:
    81  	// “read /dev/stdin: input/output error [*fs.PathError]
    82  	// input/output error [syscall.Errno]”
    83  	// isPanic: false
    84  
    85  	// if addError present, submit error to it
    86  	if r.addError != nil {
    87  		err = perrors.ErrorfPF("os.Stdin.Read error: “%w” isPanic: %t",
    88  			err, isPanic,
    89  		)
    90  		r.addError(err)
    91  		err = io.EOF
    92  		return
    93  	}
    94  
    95  	fmt.Fprintf(os.Stderr, "os.Stdin.Read error: “%s” isPanic: %t",
    96  		perrors.Long(err),
    97  		isPanic,
    98  	)
    99  	err = io.EOF
   100  
   101  	return
   102  }
   103  
   104  // read invokes [os.Stdin.Read] capturing panic
   105  func (r *StdinReader) read(p []byte) (n int, isPanic bool, err error) {
   106  	defer parl.RecoverErr(func() parl.DA { return parl.A() }, &err, &isPanic)
   107  
   108  	n, err = os.Stdin.Read(p)
   109  
   110  	return
   111  }