blob: 9aa88fab81ef8a15273232559e9f7410af86a3b7 [file] [log] [blame]
// 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"
"log"
"net"
"os"
"time"
)
// 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 *log.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.logf("error: 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.logf("error: serial: conn read: %s", err)
return
}
_, err = io.WriteString(s.serial, l)
if err != nil {
s.logf("error: serial: write: %s", err)
return
}
}
}()
go func() {
defer conn.Close()
f, err := os.Open(s.AuxiliaryOutput.Name())
if err != nil {
s.logf("error: serial: %s", err)
}
defer f.Close()
if s.StartAtEnd {
_, err = f.Seek(0, io.SeekEnd)
if err != nil {
s.logf("error: 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) logf(format string, args ...interface{}) {
if s.Logger != nil {
s.Logger.Printf(format, args...)
}
}