From 3c9ba66295db4fbbabaf113f7351463ab6e18cc0 Mon Sep 17 00:00:00 2001 From: Ambrose Chua Date: Tue, 16 Jun 2020 15:35:55 +0800 Subject: [PATCH] Initial working serial port writer --- .gitignore | 1 + conf.go | 93 +++++++++++++++++++++++++++++++++++++++++++++ file.go | 35 +++++++++++++++++ go.mod | 9 +++++ go.sum | 15 ++++++++ main.go | 109 +++++++++++++++++++++++++++++++++++++++++++++++++++++ serial.go | 64 +++++++++++++++++++++++++++++++ util.go | 39 +++++++++++++++++++ 8 files changed, 365 insertions(+) create mode 100644 .gitignore create mode 100644 conf.go create mode 100644 file.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 serial.go create mode 100644 util.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..095d898 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +apply diff --git a/conf.go b/conf.go new file mode 100644 index 0000000..b6ed697 --- /dev/null +++ b/conf.go @@ -0,0 +1,93 @@ +package main + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io" + "time" +) + +type Device interface { + ReadTimeout(time.Duration) ([]byte, error) + WriteLine([]byte) error + Close() error +} + +var ErrorDeviceOperationNotSupported = errors.New("device operation not supported") + +type ConfigurationLine interface { + Apply(Device) error +} + +type ConfigurationDirective struct { + Name string + Argument string +} + +type ConfigurationSimple []byte + +func (d ConfigurationDirective) Apply(dev Device) error { + if d.Name == "sleep" { + seconds := 0 + _, err := fmt.Sscanf(string(d.Argument), "%d", &seconds) + if err != nil { + return fmt.Errorf("bad sleep arguments: %w", err) + } + time.Sleep(time.Duration(seconds) * time.Second) + } + return nil +} + +func (s ConfigurationSimple) Apply(dev Device) error { + err := dev.WriteLine([]byte(s)) + if err != nil { + return err + } + response, err := dev.ReadTimeout(200 * time.Millisecond) + if errors.Is(err, ErrorDeviceOperationNotSupported) { + } else if err != nil { + return err + } + fmt.Printf("%s", response) + return nil +} + +type ConfigurationReader bufio.Reader + +func NewConfigurationReader(r io.Reader) *ConfigurationReader { + return (*ConfigurationReader)(bufio.NewReader(r)) +} + +func (c *ConfigurationReader) Read() (ConfigurationLine, error) { + line, err := (*bufio.Reader)(c).ReadBytes('\n') + if err != nil { + return nil, err + } + line = bytes.TrimSuffix(line, []byte{'\n'}) + + if len(line) > 1 && line[0] == '!' && line[1] == ' ' { + // Comment + return ConfigurationSimple(string(line)), nil + } else if len(line) > 0 && line[0] == '!' { + // Directive + line = line[1:] + + firstSep := bytes.IndexByte(line, ' ') + if firstSep < 0 { + return ConfigurationDirective{ + Name: string(line), + }, nil + } + + directive := line[:firstSep] + rest := line[firstSep+1:] + return ConfigurationDirective{ + Name: string(directive), + Argument: string(rest), + }, nil + } else { + return ConfigurationSimple(line), nil + } +} diff --git a/file.go b/file.go new file mode 100644 index 0000000..ac52c8e --- /dev/null +++ b/file.go @@ -0,0 +1,35 @@ +package main + +import ( + "fmt" + "os" + "time" +) + +type DeviceFile struct { + file *os.File +} + +func NewDeviceFile(name string) (DeviceFile, error) { + file, err := os.OpenFile(name, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return DeviceFile{}, err + } + return DeviceFile{ + file: file, + }, nil +} + +func (f DeviceFile) ReadTimeout(timeout time.Duration) ([]byte, error) { + return nil, fmt.Errorf("%w: Read", ErrorDeviceOperationNotSupported) +} + +func (f DeviceFile) WriteLine(buf []byte) error { + buf = append(buf, '\n') + f.file.Write(buf) + return nil +} + +func (f DeviceFile) Close() error { + return f.file.Close() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1adb441 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module git.makerforce.io/dump/sit/cs2203/confs/apply + +go 1.14 + +require ( + github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 + go.bug.st/serial v1.1.0 + golang.org/x/sys v0.0.0-20200523222454-059865788121 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..79d5939 --- /dev/null +++ b/go.sum @@ -0,0 +1,15 @@ +github.com/creack/goselect v0.1.1 h1:tiSSgKE1eJtxs1h/VgGQWuXUP0YS4CDIFMp6vaI1ls0= +github.com/creack/goselect v0.1.1/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +go.bug.st/serial v1.1.0 h1:O0EHZw8ZdhmTAikak5ZY/8vyKCpFxZYgqZw1bGegxU8= +go.bug.st/serial v1.1.0/go.mod h1:rpXPISGjuNjPTRTcMlxi9lN6LoIPxd1ixVjBd8aSk/Q= +golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121 h1:rITEj+UZHYC927n8GT97eC3zrpzXdb/voyeOuVKS46o= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main.go b/main.go new file mode 100644 index 0000000..a25c344 --- /dev/null +++ b/main.go @@ -0,0 +1,109 @@ +/* +apply interacts with a serial port to configure a Cisco product using files. + +This tool does not attempt to understand the proprietary donkey format that is +the Cisco CLI. Instead, it provides some directives around it to make life +easier when crafting configuration files. + +Basic usage is very simple: Pass a file containing Cisco commands and it will +write it out to serial port. It also prints the output back to stdout for +visual inspection of success. + +apply also contains some additional features to help you in handling files. The +main feature is directives that perform certain actions. These are expressed +within a line beginning with "!". + +Directives + + !sleep + +Sleep a fixed number of seconds. + + !assert + +Ensure that in the next 1 second, output from the serial port contains . + +*/ +package main + +import ( + "errors" + "flag" + "fmt" + "io" + "os" + "time" +) + +var Usage = func() { + fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [-port PORT] [-writefile] FILE...\n", os.Args[0]) + flag.PrintDefaults() +} + +func main() { + port := flag.String("port", "/dev/ttyUSB0", "Serial port to write to") + writefile := flag.Bool("writefile", false, "Treat PORT as file, and write to file") + flag.Parse() + confs := flag.Args() + + if len(confs) < 1 { + Usage() + return + } + + var device Device + var err error + if *writefile { + device, err = NewDeviceFile(*port) + } else { + device, err = NewDeviceSerial(*port) + } + if err != nil { + panic(err) + } + defer device.Close() + + for _, conf := range confs { + file, err := os.Open(conf) + if err != nil { + panic(fmt.Errorf("file %s: %w", conf, err)) + } + + reader := NewConfigurationReader(file) + lineNum := 0 + + var line ConfigurationLine + for { + lineNum++ + + line, err = reader.Read() + if err != nil { + break + } + if _, ok := line.(ConfigurationSimple); !ok { + fmt.Printf("directive: %v\n", line) + } + + err = line.Apply(device) + if err != nil { + break + } + } + + if err != nil && err != io.EOF { + panic(fmt.Errorf("file %s line %d: %w", conf, lineNum, err)) + } + + // Attempt to read rest of output + response, err := device.ReadTimeout(1000 * time.Millisecond) + if errors.Is(err, ErrorDeviceOperationNotSupported) { + } else if err != nil { + panic(fmt.Errorf("device: %w", err)) + } + fmt.Printf("%s", response) + + if err := file.Close(); err != nil { + panic(fmt.Errorf("file %s: %w", conf, err)) + } + } +} diff --git a/serial.go b/serial.go new file mode 100644 index 0000000..f00edd5 --- /dev/null +++ b/serial.go @@ -0,0 +1,64 @@ +package main + +import ( + "time" + + "go.bug.st/serial" +) + +type DeviceSerial struct { + port serial.Port + lines chan []byte + lineErr chan error +} + +func NewDeviceSerial(name string) (DeviceSerial, error) { + serialMode := &serial.Mode{ + BaudRate: 9600, + DataBits: 8, + Parity: serial.NoParity, + StopBits: serial.OneStopBit, + } + port, err := serial.Open(name, serialMode) + if err != nil { + return DeviceSerial{}, err + } + lines, lineErr := lineChannel(port) + return DeviceSerial{ + port: port, + lines: lines, + lineErr: lineErr, + }, nil +} + +func (s DeviceSerial) ReadTimeout(timeout time.Duration) ([]byte, error) { + var err error + buf := make([]byte, 0) + t := timerChannel(timeout) + for { + // Read lines until timeout + select { + case line := <-s.lines: + buf = append(buf, line...) + case err = <-s.lineErr: + return buf, err + case <-t: + return buf, err + } + } + return nil, nil +} + +func (s DeviceSerial) WriteLine(buf []byte) error { + // TODO: handle n + buf = append(buf, '\r') + _, err := s.port.Write(buf) + if err != nil { + return err + } + return nil +} + +func (s DeviceSerial) Close() error { + return s.port.Close() +} diff --git a/util.go b/util.go new file mode 100644 index 0000000..9cd9176 --- /dev/null +++ b/util.go @@ -0,0 +1,39 @@ +package main + +import ( + "bufio" + "io" + "time" +) + +func lineChannel(r io.Reader) (chan []byte, chan error) { + lineChan := make(chan []byte, 100) + errChan := make(chan error) + + reader := bufio.NewReader(r) + go func() { + for { + line, err := reader.ReadBytes('\n') + lineChan <- line + + if err != nil { + errChan <- err + break + } + } + close(lineChan) + }() + + return lineChan, errChan +} + +func timerChannel(t time.Duration) chan bool { + timerChan := make(chan bool) + + go func() { + time.Sleep(t) + timerChan <- true + }() + + return timerChan +}