| // Copyright 2020 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package serial |
| |
| import ( |
| "bufio" |
| "context" |
| "errors" |
| "io" |
| "io/ioutil" |
| "net" |
| "os" |
| "time" |
| |
| "go.fuchsia.dev/fuchsia/tools/lib/logger" |
| ) |
| |
| // ErrNetClosing comes from the stdlib, but is not exported by the stdlib, see |
| // https://golang.org/issues/4373. There is a test to assert this is still a |
| // sound match for the behavior we're matching against. |
| var ErrNetClosing = errors.New("use of closed network connection") |
| |
| func IsErrNetClosing(err error) bool { |
| if e, ok := err.(*net.OpError); ok { |
| return e.Err.Error() == ErrNetClosing.Error() |
| } |
| return false |
| } |
| |
| // Server proxies all i/o to/from a serial port via another io.ReadWriter. |
| // Start and Stop may be pairwise called any number of times. |
| type Server struct { |
| ServerOptions |
| serial io.ReadWriteCloser |
| } |
| |
| // ServerOptions provide options that parametrize the server's behavior. |
| type ServerOptions struct { |
| // Logger if set to a non-nil value will receive log output for internal |
| // errors that are incidental to program logic, but may be relevant for |
| // diagnosis of general behaviors, such as errors in IO with the unix socket |
| // clients. |
| Logger *logger.Logger |
| |
| // AuxiliaryOutput is an optional serial output sink. It will be closed before |
| // server.Run returns. It is dup'd for each incoming socket. |
| AuxiliaryOutput *os.File |
| |
| // StartAtEnd instructs each connection to begin streaming at the end |
| // of the aux file. |
| StartAtEnd bool |
| } |
| |
| // NewServer returns a new server that lives atop the given 'serial' port. |
| func NewServer(serial io.ReadWriteCloser, opts ServerOptions) *Server { |
| s := &Server{ |
| ServerOptions: opts, |
| serial: serial, |
| } |
| return s |
| } |
| |
| // Run begins the server and blocks until the context signals done or an error |
| // is encountered reading from serial. While running, all serial i/o is |
| // forwarded to and from the any connection accepted by the listener. |
| // The listener is closed by the time Run returns. |
| func (s *Server) Run(ctx context.Context, listener net.Listener) error { |
| ctx, cancel := context.WithCancel(ctx) |
| defer cancel() |
| |
| if s.AuxiliaryOutput == nil { |
| f, err := ioutil.TempFile("", "tools-serial-aux-output") |
| if err != nil { |
| return err |
| } |
| s.AuxiliaryOutput = f |
| defer os.Remove(s.AuxiliaryOutput.Name()) |
| } |
| |
| go func() { |
| for { |
| conn, err := listener.Accept() |
| if err != nil { |
| // if the listener was closed, we don't care. |
| if IsErrNetClosing(err) { |
| return |
| } |
| s.errorf("serial: accept: %s", err) |
| return |
| } |
| |
| // Start a loop that reads whole lines from conn, and writes whole lines |
| // to the serial port. This is not complete, it would be better to have a |
| // proper line discipline for the serial port, however there are multiple |
| // writers to the port, this being one of them. We can best-effort |
| // non-conflicting writes by at least attempting to write lines as an |
| // atomic unit, though this is still fallible. |
| go func() { |
| b := bufio.NewReader(conn) |
| for { |
| l, err := b.ReadString('\n') |
| if err != nil { |
| s.errorf("serial: conn read: %s", err) |
| return |
| } |
| _, err = io.WriteString(s.serial, l) |
| if err != nil { |
| s.errorf("serial: write: %s", err) |
| return |
| } |
| } |
| }() |
| |
| go func() { |
| defer conn.Close() |
| |
| f, err := os.Open(s.AuxiliaryOutput.Name()) |
| if err != nil { |
| s.errorf("serial: %s", err) |
| } |
| defer f.Close() |
| |
| if s.StartAtEnd { |
| _, err = f.Seek(0, io.SeekEnd) |
| if err != nil { |
| s.errorf("serial: seek: %s", err) |
| return |
| } |
| } |
| |
| // keep copying until conn is closed |
| buf := make([]byte, 4096) |
| for { |
| // files on unix always return readable, even if select'd so |
| // there's no great strategy for "tailing". It's possible to relax |
| // some of this to a stat loop to check for appends, and/or to use |
| // filesystem watcher APIs, but in this context, this rate is both |
| // easy enough on CPU and fast enough for keeping up with serial. |
| select { |
| case <-ctx.Done(): |
| return |
| case <-time.After(50 * time.Millisecond): |
| _, err := io.CopyBuffer(conn, f, buf) |
| // io.Copy returns nil on io.EOF, which is useful here, as that's the |
| // case where we read the end of the file |
| if err == nil { |
| continue |
| } |
| return |
| } |
| } |
| }() |
| } |
| }() |
| |
| errs := make(chan error) |
| go func() { |
| buf := make([]byte, 4096) |
| for { |
| _, err := io.CopyBuffer(s.AuxiliaryOutput, s.serial, buf) |
| if err != nil { |
| errs <- err |
| return |
| } |
| } |
| }() |
| |
| var err error |
| select { |
| case <-ctx.Done(): |
| // close the serial port and wait for the copy goroutine to finish, so that |
| // we are confident that auxoutput has received all of the data up to the |
| // close. |
| s.serial.Close() |
| <-errs |
| case err = <-errs: |
| s.serial.Close() |
| cancel() |
| } |
| |
| listener.Close() |
| return err |
| } |
| |
| func (s *Server) errorf(format string, args ...interface{}) { |
| if s.Logger != nil { |
| s.Logger.Errorf(format, args...) |
| } |
| } |