Skip to content

Instantly share code, notes, and snippets.

@raylee
Last active January 6, 2021 14:34
Show Gist options
  • Save raylee/ef44aac10dc1fc0068f5073210f62e17 to your computer and use it in GitHub Desktop.
Save raylee/ef44aac10dc1fc0068f5073210f62e17 to your computer and use it in GitHub Desktop.
wrapper for https://github.com/google/goexpect which gives it a simpler interface, plus a multiWriteCloser implementation for logging.
// Interact wraps google/goexpect with a simpler interface
package main
import (
"io"
"log"
"regexp"
"time"
expect "github.com/google/goexpect"
)
type Interact struct {
ex *expect.GExpect
done chan struct{}
timeout time.Duration
}
// ctrl converts an A-Z character to its ASCII control equivalent if it exists, or
// the original rune if it doesn't.
func ctrl(r rune) rune {
if r >= 'A' && r <= 'Z' {
return rune(r - 'A' + 1)
}
if r >= 'a' && r <= 'z' {
return rune(r - 'a' + 1)
}
return r
}
// Control sends a control character to the remote.
func (i *Interact) Control(r rune) {
i.ex.Send(string(ctrl(r)))
}
// Send a string to the remote.
func (i *Interact) Send(s string) {
i.ex.Send(s)
}
// SendLine appends a line terminator when sending the string s.
func (i *Interact) SendLine(s string) {
i.ex.Send(s + "\n")
}
// Expect will wait for the string s.
func (i *Interact) Expect(s string) {
re := regexp.MustCompile(s)
i.ex.Expect(re, i.timeout)
}
// ExpectRE will wait for the regular expression re.
func (i *Interact) ExpectRE(re *regexp.Regexp) {
i.ex.Expect(re, i.timeout)
}
// Close shuts down the expect machinery.
func (i *Interact) Close() {
close(i.done)
i.ex.Close()
}
// NewInteract starts goexpect on s, with a logger for the IO and a default timeout for the operations.
func NewInteract(s io.ReadWriteCloser, logger io.WriteCloser, timeout time.Duration) Interact {
done := make(chan struct{})
streamControl := &expect.GenOptions{
In: s,
Out: s,
Wait: func() error { <-done; return nil },
Close: func() error { return s.Close() },
Check: func() bool { return true },
}
ex, _, err := expect.SpawnGeneric(
streamControl,
-1, // timeout
expect.Tee(logger),
expect.CheckDuration(100*time.Millisecond),
expect.SendTimeout(timeout),
)
if err != nil {
log.Fatal(err)
}
// wrap the vendor's package with a simpler interface
return Interact{ex, done, timeout}
}
package main
import "io"
// multiWriteCloser adds the io.Closer interface to the standard library's
// io.MultiWriter.
type multiWriteCloser struct {
writers []io.WriteCloser
io.Writer
}
// Close closes all underlying streams. Satisfies the io.Closer and
// io.WriteCloser interfaces.
func (t *multiWriteCloser) Close() error {
var err error
for _, w := range t.writers {
e := w.Close()
if err != nil {
err = e
}
}
return err
}
// MultiWriteCloser creates a writer that duplicates its writes to all the
// provided writers, similar to the Unix tee(1) command. Close closes
// all streams.
//
// Each write is written to each listed writer, one at a time.
// If a listed writer returns an error, that overall write operation
// stops and returns the error; it does not continue down the list.
func MultiWriteCloser(writers ...io.WriteCloser) io.WriteCloser {
allClosers := make([]io.WriteCloser, 0, len(writers))
allWriters := make([]io.Writer, 0, len(writers))
for _, w := range writers {
if mw, ok := w.(*multiWriteCloser); ok {
allClosers = append(allClosers, mw.writers...)
} else {
allClosers = append(allClosers, w)
}
allWriters = append(allWriters, w.(io.Writer))
}
return &multiWriteCloser{allClosers, io.MultiWriter(allWriters...)}
}
@raylee
Copy link
Author

raylee commented Oct 3, 2020

Usage is something like the below, which controls a serial port:

// TeeLogger opens a file which logs to both standard out and the given filename.
func TeeLogger(fn string) io.WriteCloser {
	logfile, err := os.OpenFile(fn, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		log.Fatal(err)
	}
	return MultiWriteCloser(logfile, os.Stdout)
}

// openSerial connects to the device over the USB serial adaptor and wraps the IO
// in an 'expect' style scripting package.
// import "github.com/tarm/serial"
func openSerial(device string, baud int) Interact {
	s, err := serial.OpenPort(&serial.Config{Name: device, Baud: baud})
	if err != nil {
		log.Fatal(err)
	}
	return NewInteract(s, Logger(), timeout)
}

func main() {
	flag.Parse()
	interact := openSerial(*dev, *baud)
	defer interact.Close()

	interact.Control('c')

	for _, c := range resetCmds {
		interact.SendLine(c)
		interact.Expect(prompt)
	}

	...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment