From 0e4004fbbf4556defe0f907d43d066e1c049ff7f Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Fri, 16 Jul 2021 15:06:21 +0200 Subject: [PATCH 01/20] Made this repo a library --- Taskfile.yml | 8 +- discovery_server.go | 317 +++++++++++++++++++++ args.go => dummy-discovery/args/args.go | 16 +- dummy-discovery/main.go | 134 +++++++++ main.go | 362 ------------------------ version/version.go | 24 -- 6 files changed, 464 insertions(+), 397 deletions(-) create mode 100644 discovery_server.go rename args.go => dummy-discovery/args/args.go (79%) create mode 100644 dummy-discovery/main.go delete mode 100644 main.go delete mode 100644 version/version.go diff --git a/Taskfile.yml b/Taskfile.yml index 2acbb6e..cf8a145 100755 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -2,9 +2,9 @@ version: "3" tasks: build: - desc: Build the project + desc: Build the dummy-discovery client example cmds: - - go build -v -ldflags '{{.LDFLAGS}}' + - go build -o dist/dummy -v -ldflags '{{.LDFLAGS}}' ./dummy-discovery vars: PROJECT_NAME: "dummy-discovery" @@ -14,5 +14,5 @@ vars: TIMESTAMP: sh: echo "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" LDFLAGS: > - -X github.com/arduino/dummy-discovery/version.Tag={{.VERSION}} - -X github.com/arduino/dummy-discovery/version.Timestamp={{.TIMESTAMP}} + -X github.com/arduino/dummy-discovery/dummy-discovery/args.Tag={{.VERSION}} + -X github.com/arduino/dummy-discovery/dummy-discovery/args.Timestamp={{.TIMESTAMP}} diff --git a/discovery_server.go b/discovery_server.go new file mode 100644 index 0000000..ccb8eaf --- /dev/null +++ b/discovery_server.go @@ -0,0 +1,317 @@ +// +// This file is part of dummy-discovery. +// +// Copyright 2021 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to modify or +// otherwise use the software for commercial activities involving the Arduino +// software without disclosing the source code of your own applications. To purchase +// a commercial license, send an email to license@arduino.cc. +// + +package discovery + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + "regexp" + "strconv" + "strings" + "sync" + + "github.com/arduino/go-properties-orderedmap" +) + +type Port struct { + Address string `json:"address"` + AddressLabel string `json:"label,omitempty"` + Protocol string `json:"protocol,omitempty"` + ProtocolLabel string `json:"protocolLabel,omitempty"` + Properties *properties.Map `json:"properties,omitempty"` +} + +type EventCallback func(event string, port *Port) + +type Discovery interface { + Hello(userAgent string, protocolVersion int) error + Start() error + Stop() error + List() ([]*Port, error) + StartSync(eventCB EventCallback) (chan<- bool, error) +} + +type DiscoveryServer struct { + impl Discovery + out io.Writer + outMutex sync.Mutex + userAgent string + reqProtocolVersion int + initialized bool + started bool + syncStarted bool + syncCloseChan chan<- bool +} + +func NewDiscoveryServer(impl Discovery) *DiscoveryServer { + return &DiscoveryServer{ + impl: impl, + } +} + +func (d *DiscoveryServer) Run(in io.Reader, out io.Writer) error { + d.out = out + reader := bufio.NewReader(in) + for { + fullCmd, err := reader.ReadString('\n') + if err != nil { + d.output(&genericMessageJSON{ + EventType: "command_error", + Error: true, + Message: err.Error(), + }) + return err + } + split := strings.Split(fullCmd, " ") + cmd := strings.ToUpper(strings.TrimSpace(split[0])) + + if !d.initialized && cmd != "HELLO" { + d.output(&genericMessageJSON{ + EventType: "command_error", + Error: true, + Message: fmt.Sprintf("First command must be HELLO, but got '%s'", cmd), + }) + continue + } + + switch cmd { + case "HELLO": + if d.initialized { + d.output(&genericMessageJSON{ + EventType: "hello", + Error: true, + Message: "HELLO already called", + }) + continue + } + re := regexp.MustCompile(`(\d+) "([^"]+)"`) + matches := re.FindStringSubmatch(fullCmd[6:]) + if len(matches) != 3 { + d.output(&genericMessageJSON{ + EventType: "hello", + Error: true, + Message: "Invalid HELLO command", + }) + continue + } + d.userAgent = matches[2] + if v, err := strconv.ParseInt(matches[1], 10, 64); err != nil { + d.output(&genericMessageJSON{ + EventType: "hello", + Error: true, + Message: "Invalid protocol version: " + matches[2], + }) + continue + } else { + d.reqProtocolVersion = int(v) + } + if err := d.impl.Hello(d.userAgent, 1); err != nil { + d.output(&genericMessageJSON{ + EventType: "hello", + Error: true, + Message: err.Error(), + }) + continue + } + d.output(&genericMessageJSON{ + EventType: "hello", + ProtocolVersion: 1, // Protocol version 1 is the only supported for now... + Message: "OK", + }) + d.initialized = true + + case "START": + if d.started { + d.output(&genericMessageJSON{ + EventType: "start", + Error: true, + Message: "Discovery already STARTed", + }) + continue + } + if d.syncStarted { + d.output(&genericMessageJSON{ + EventType: "start", + Error: true, + Message: "Discovery already START_SYNCed, cannot START", + }) + continue + } + if err := d.impl.Start(); err != nil { + d.output(&genericMessageJSON{ + EventType: "start", + Error: true, + Message: "Cannot START: " + err.Error(), + }) + continue + } + d.started = true + d.output(&genericMessageJSON{ + EventType: "start", + Message: "OK", + }) + + case "LIST": + if !d.started { + d.output(&genericMessageJSON{ + EventType: "list", + Error: true, + Message: "Discovery not STARTed", + }) + continue + } + if d.syncStarted { + d.output(&genericMessageJSON{ + EventType: "list", + Error: true, + Message: "discovery already START_SYNCed, LIST not allowed", + }) + continue + } + if ports, err := d.impl.List(); err != nil { + d.output(&genericMessageJSON{ + EventType: "list", + Error: true, + Message: "LIST error: " + err.Error(), + }) + continue + } else { + type listOutputJSON struct { + EventType string `json:"eventType"` + Ports []*Port `json:"ports"` + } + d.output(&listOutputJSON{ + EventType: "list", + Ports: ports, + }) + } + + case "START_SYNC": + if d.syncStarted { + d.output(&genericMessageJSON{ + EventType: "start_sync", + Error: true, + Message: "Discovery already START_SYNCed", + }) + continue + } + if d.started { + d.output(&genericMessageJSON{ + EventType: "start_sync", + Error: true, + Message: "Discovery already STARTed, cannot START_SYNC", + }) + continue + } + if c, err := d.impl.StartSync(d.syncEvent); err != nil { + d.output(&genericMessageJSON{ + EventType: "start_sync", + Error: true, + Message: "Cannot START_SYNC: " + err.Error(), + }) + continue + } else { + d.syncCloseChan = c + d.syncStarted = true + d.output(&genericMessageJSON{ + EventType: "start_sync", + Message: "OK", + }) + } + + case "STOP": + if !d.syncStarted && !d.started { + d.output(&genericMessageJSON{ + EventType: "stop", + Error: true, + Message: "Discovery already STOPped", + }) + continue + } + if err := d.impl.Stop(); err != nil { + d.output(&genericMessageJSON{ + EventType: "stop", + Error: true, + Message: "Cannot STOP: " + err.Error(), + }) + continue + } + if d.started { + d.started = false + } + if d.syncStarted { + d.syncCloseChan <- true + close(d.syncCloseChan) + d.syncStarted = false + } + d.output(&genericMessageJSON{ + EventType: "stop", + Message: "OK", + }) + + case "QUIT": + d.output(&genericMessageJSON{ + EventType: "quit", + Message: "OK", + }) + return nil + + default: + d.output(&genericMessageJSON{ + EventType: "command_error", + Error: true, + Message: fmt.Sprintf("Command %s not supported", cmd), + }) + } + } +} + +func (d *DiscoveryServer) syncEvent(event string, port *Port) { + type syncOutputJSON struct { + EventType string `json:"eventType"` + Port *Port `json:"port"` + } + d.output(&syncOutputJSON{ + EventType: event, + Port: port, + }) +} + +type genericMessageJSON struct { + EventType string `json:"eventType"` + Message string `json:"message"` + Error bool `json:"error,omitempty"` + ProtocolVersion int `json:"protocolVersion,omitempty"` +} + +func (d *DiscoveryServer) output(msg interface{}) { + data, err := json.MarshalIndent(msg, "", " ") + if err != nil { + d.output(&genericMessageJSON{ + EventType: "command_error", + Error: true, + Message: err.Error(), + }) + } else { + d.outMutex.Lock() + fmt.Println(string(data)) + d.outMutex.Unlock() + } +} diff --git a/args.go b/dummy-discovery/args/args.go similarity index 79% rename from args.go rename to dummy-discovery/args/args.go index f01c4ff..7480264 100644 --- a/args.go +++ b/dummy-discovery/args/args.go @@ -15,25 +15,27 @@ // a commercial license, send an email to license@arduino.cc. // -package main +package args import ( "fmt" "os" ) -var args struct { - showVersion bool -} +// Tag is the current git tag +var Tag = "snapshot" + +// Timestamp is the current timestamp +var Timestamp = "unknown" -func parseArgs() { +func ParseArgs() { for _, arg := range os.Args[1:] { if arg == "" { continue } if arg == "-v" || arg == "--version" { - args.showVersion = true - continue + fmt.Printf("serial-discovery %s (build timestamp: %s)\n", Tag, Timestamp) + os.Exit(0) } fmt.Fprintf(os.Stderr, "invalid argument: %s\n", arg) os.Exit(1) diff --git a/dummy-discovery/main.go b/dummy-discovery/main.go new file mode 100644 index 0000000..730bafe --- /dev/null +++ b/dummy-discovery/main.go @@ -0,0 +1,134 @@ +// +// This file is part of dummy-discovery. +// +// Copyright 2021 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to modify or +// otherwise use the software for commercial activities involving the Arduino +// software without disclosing the source code of your own applications. To purchase +// a commercial license, send an email to license@arduino.cc. +// + +package main + +import ( + "errors" + "fmt" + "os" + "time" + + discovery "github.com/arduino/dummy-discovery" + "github.com/arduino/dummy-discovery/dummy-discovery/args" + "github.com/arduino/go-properties-orderedmap" +) + +type DummyDiscovery struct { + startSyncCount int + listCount int +} + +func main() { + args.ParseArgs() + dummyDiscovery := &DummyDiscovery{} + server := discovery.NewDiscoveryServer(dummyDiscovery) + if err := server.Run(os.Stdin, os.Stdout); err != nil { + os.Exit(1) + } +} + +func (d *DummyDiscovery) Hello(userAgent string, protocol int) error { + return nil +} + +func (d *DummyDiscovery) List() ([]*discovery.Port, error) { + d.listCount++ + if d.listCount%5 == 0 { + return nil, errors.New("could not list every 5 times") + } + return []*discovery.Port{ + CreateDummyPort(), + }, nil +} + +func (d *DummyDiscovery) Start() error { + return nil +} + +func (d *DummyDiscovery) Stop() error { + return nil +} + +func (d *DummyDiscovery) StartSync(eventCB discovery.EventCallback) (chan<- bool, error) { + d.startSyncCount++ + if d.startSyncCount%5 == 0 { + return nil, errors.New("could not start_sync every 5 times") + } + + c := make(chan bool) + + // Run synchronous event emitter + go func() { + var closeChan <-chan bool = c + + // Ouput initial port state + eventCB("add", CreateDummyPort()) + eventCB("add", CreateDummyPort()) + + // Start sending events + for { + // if err != nil { + // output(&genericMessageJSON{ + // EventType: "start_sync", + // Error: true, + // Message: fmt.Sprintf("error decoding START_SYNC event: %s", err), + // }) + // return + // } + + select { + case <-closeChan: + return + case <-time.After(2 * time.Second): + } + + port := CreateDummyPort() + eventCB("add", port) + + select { + case <-closeChan: + return + case <-time.After(2 * time.Second): + } + + eventCB("remove", &discovery.Port{ + Address: port.Address, + Protocol: port.Protocol, + }) + } + }() + + return c, nil +} + +var dummyCounter = 0 + +func CreateDummyPort() *discovery.Port { + dummyCounter++ + return &discovery.Port{ + Address: fmt.Sprintf("%d", dummyCounter), + AddressLabel: "Dummy upload port", + Protocol: "dummy", + ProtocolLabel: "Dummy protocol", + Properties: properties.NewFromHashmap(map[string]string{ + "vid": "0x2341", + "pid": "0x0041", + "mac": fmt.Sprintf("%d", dummyCounter*384782), + }), + } +} diff --git a/main.go b/main.go deleted file mode 100644 index c149901..0000000 --- a/main.go +++ /dev/null @@ -1,362 +0,0 @@ -// -// This file is part of dummy-discovery. -// -// Copyright 2021 ARDUINO SA (http://www.arduino.cc/) -// -// This software is released under the GNU General Public License version 3, -// which covers the main part of arduino-cli. -// The terms of this license can be found at: -// https://www.gnu.org/licenses/gpl-3.0.en.html -// -// You can be released from the requirements of the above licenses by purchasing -// a commercial license. Buying such a license is mandatory if you want to modify or -// otherwise use the software for commercial activities involving the Arduino -// software without disclosing the source code of your own applications. To purchase -// a commercial license, send an email to license@arduino.cc. -// - -package main - -import ( - "bufio" - "encoding/json" - "fmt" - "os" - "regexp" - "strconv" - "strings" - "sync" - "time" - - "github.com/arduino/dummy-discovery/version" - "github.com/arduino/go-properties-orderedmap" -) - -var initialized = false -var started = false -var syncStarted = false -var syncCloseChan chan<- bool - -func main() { - parseArgs() - if args.showVersion { - fmt.Printf("serial-discovery %s (build timestamp: %s)\n", version.Tag, version.Timestamp) - return - } - - reader := bufio.NewReader(os.Stdin) - for { - fullCmd, err := reader.ReadString('\n') - if err != nil { - output(&genericMessageJSON{ - EventType: "command_error", - Error: true, - Message: err.Error(), - }) - os.Exit(1) - } - split := strings.Split(fullCmd, " ") - cmd := strings.ToUpper(strings.TrimSpace(split[0])) - - if !initialized && cmd != "HELLO" { - output(&genericMessageJSON{ - EventType: "command_error", - Error: true, - Message: fmt.Sprintf("First command must be HELLO, but got '%s'", cmd), - }) - continue - } - - switch cmd { - case "HELLO": - if initialized { - output(&genericMessageJSON{ - EventType: "hello", - Error: true, - Message: "HELLO already called", - }) - } - re := regexp.MustCompile(`(\d+) "([^"]+)"`) - matches := re.FindStringSubmatch(fullCmd[6:]) - if len(matches) != 3 { - output(&genericMessageJSON{ - EventType: "hello", - Error: true, - Message: "Invalid HELLO command", - }) - continue - } - _ /* userAgent */ = matches[2] - _ /* reqProtocolVersion */, err := strconv.ParseUint(matches[1], 10, 64) - if err != nil { - output(&genericMessageJSON{ - EventType: "hello", - Error: true, - Message: "Invalid protocol version: " + matches[2], - }) - continue - } - output(&genericMessageJSON{ - EventType: "hello", - ProtocolVersion: 1, // Protocol version 1 is the only supported for now... - Message: "OK", - }) - initialized = true - - case "START": - if started { - output(&genericMessageJSON{ - EventType: "start", - Error: true, - Message: "already STARTed", - }) - continue - } - if syncStarted { - output(&genericMessageJSON{ - EventType: "start", - Error: true, - Message: "discovery already START_SYNCed, cannot START", - }) - continue - } - output(&genericMessageJSON{ - EventType: "start", - Message: "OK", - }) - started = true - - case "LIST": - if !started { - output(&genericMessageJSON{ - EventType: "list", - Error: true, - Message: "discovery not STARTed", - }) - continue - } - if syncStarted { // TODO: Report in RFC that in "events mode" LIST is not allowed - output(&genericMessageJSON{ - EventType: "list", - Error: true, - Message: "discovery already START_SYNCed, LIST not allowed", - }) - continue - } - outputList() - - case "START_SYNC": - startSync() - - case "STOP": - if !syncStarted && !started { - output(&genericMessageJSON{ - EventType: "stop", - Error: true, - Message: "already STOPped", - }) - continue - } - if started { - started = false - } - if syncStarted { - syncCloseChan <- true - close(syncCloseChan) - syncCloseChan = nil - syncStarted = false - } - output(&genericMessageJSON{ - EventType: "stop", - Message: "OK", - }) - - case "QUIT": - output(&genericMessageJSON{ - EventType: "quit", - Message: "OK", - }) - os.Exit(0) - - default: - output(&genericMessageJSON{ - EventType: "command_error", - Error: true, - Message: fmt.Sprintf("Command %s not supported", cmd), - }) - } - } -} - -type boardPortJSON struct { - Address string `json:"address"` - Label string `json:"label,omitempty"` - Protocol string `json:"protocol,omitempty"` - ProtocolLabel string `json:"protocolLabel,omitempty"` - Properties *properties.Map `json:"properties,omitempty"` -} - -type listOutputJSON struct { - EventType string `json:"eventType"` - Ports []*boardPortJSON `json:"ports"` -} - -type syncOutputJSON struct { - EventType string `json:"eventType"` - Port *boardPortJSON `json:"port"` -} - -var startSyncCount = 0 - -func startSync() { - if syncStarted { - output(&genericMessageJSON{ - EventType: "start_sync", - Error: true, - Message: "discovery already START_SYNCed", - }) - return - } - if started { - output(&genericMessageJSON{ - EventType: "start_sync", - Error: true, - Message: "discovery already STARTed, cannot START_SYNC", - }) - return - } - - startSyncCount++ - if startSyncCount%5 == 0 { - output(&genericMessageJSON{ - EventType: "start_sync", - Error: true, - Message: "could not start_sync every 5 times", - }) - return - } - - c := make(chan bool) - - syncCloseChan = c - syncStarted = true - output(&genericMessageJSON{ - EventType: "start_sync", - Message: "OK", - }) - - // Run synchronous event emitter - go func() { - var closeChan <-chan bool = c - - // Ouput initial port state - output(&syncOutputJSON{ - EventType: "add", - Port: CreateDummyPort(), - }) - output(&syncOutputJSON{ - EventType: "add", - Port: CreateDummyPort(), - }) - - // Start sending events - for { - // if err != nil { - // output(&genericMessageJSON{ - // EventType: "start_sync", - // Error: true, - // Message: fmt.Sprintf("error decoding START_SYNC event: %s", err), - // }) - // return - // } - - select { - case <-closeChan: - return - case <-time.After(2 * time.Second): - } - - port := CreateDummyPort() - output(&syncOutputJSON{ - EventType: "add", - Port: port, - }) - - select { - case <-closeChan: - return - case <-time.After(2 * time.Second): - } - - output(&syncOutputJSON{ - EventType: "remove", - Port: &boardPortJSON{ - Address: port.Address, - Protocol: port.Protocol, - }, - }) - } - }() -} - -var listCount = 0 - -func outputList() { - listCount++ - if listCount%5 == 0 { - output(&genericMessageJSON{ - EventType: "list", - Error: true, - Message: "could not list every 5 times", - }) - return - } - - portJSON := CreateDummyPort() - portsJSON := []*boardPortJSON{portJSON} - output(&listOutputJSON{ - EventType: "list", - Ports: portsJSON, - }) -} - -type genericMessageJSON struct { - EventType string `json:"eventType"` - Message string `json:"message"` - Error bool `json:"error,omitempty"` - ProtocolVersion int `json:"protocolVersion,omitempty"` -} - -var dummyCounter = 0 - -func CreateDummyPort() *boardPortJSON { - dummyCounter++ - return &boardPortJSON{ - Address: fmt.Sprintf("%d", dummyCounter), - Label: "Dummy upload port", - Protocol: "dummy", - ProtocolLabel: "Dummy protocol", - Properties: properties.NewFromHashmap(map[string]string{ - "vid": "0x2341", - "pid": "0x0041", - "mac": fmt.Sprintf("%d", dummyCounter*384782), - }), - } -} - -func output(msg interface{}) { - d, err := json.MarshalIndent(msg, "", " ") - if err != nil { - output(&genericMessageJSON{ - EventType: "command_error", - Error: true, - Message: err.Error(), - }) - } else { - stdoutMutex.Lock() - fmt.Println(string(d)) - stdoutMutex.Unlock() - } -} - -var stdoutMutex sync.Mutex diff --git a/version/version.go b/version/version.go deleted file mode 100644 index e597cd6..0000000 --- a/version/version.go +++ /dev/null @@ -1,24 +0,0 @@ -// -// This file is part of dummy-discovery. -// -// Copyright 2021 ARDUINO SA (http://www.arduino.cc/) -// -// This software is released under the GNU General Public License version 3, -// which covers the main part of arduino-cli. -// The terms of this license can be found at: -// https://www.gnu.org/licenses/gpl-3.0.en.html -// -// You can be released from the requirements of the above licenses by purchasing -// a commercial license. Buying such a license is mandatory if you want to modify or -// otherwise use the software for commercial activities involving the Arduino -// software without disclosing the source code of your own applications. To purchase -// a commercial license, send an email to license@arduino.cc. -// - -package version - -// Tag is the current git tag -var Tag = "snapshot" - -// Timestamp is the current timestamp -var Timestamp = "unknown" From c18cb8615b1cd2ce7ee2720b5ad0839f8e805127 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Fri, 16 Jul 2021 16:15:04 +0200 Subject: [PATCH 02/20] Renamed build task --- .github/workflows/test.yaml | 2 +- Taskfile.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index fa6ce08..3351113 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -26,4 +26,4 @@ jobs: go-version: "1.16" - name: Build - run: task build + run: task build-dummy-discovery diff --git a/Taskfile.yml b/Taskfile.yml index cf8a145..50aaca8 100755 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -1,10 +1,10 @@ version: "3" tasks: - build: + build-dummy-discovery: desc: Build the dummy-discovery client example cmds: - - go build -o dist/dummy -v -ldflags '{{.LDFLAGS}}' ./dummy-discovery + - go build -o dist/dummy-discovery -v -ldflags '{{.LDFLAGS}}' ./dummy-discovery vars: PROJECT_NAME: "dummy-discovery" From 74365e0d3e0f1a39d5354e21848b72a654a860e5 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Fri, 16 Jul 2021 16:22:27 +0200 Subject: [PATCH 03/20] Output json in the correct stream... --- discovery_server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discovery_server.go b/discovery_server.go index ccb8eaf..8e4a4c0 100644 --- a/discovery_server.go +++ b/discovery_server.go @@ -311,7 +311,7 @@ func (d *DiscoveryServer) output(msg interface{}) { }) } else { d.outMutex.Lock() - fmt.Println(string(data)) + d.out.Write(data) d.outMutex.Unlock() } } From bd7845020d743e240154be524a8f01d511ac42d7 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Fri, 16 Jul 2021 17:02:31 +0200 Subject: [PATCH 04/20] Factored out some common operations --- discovery_server.go | 143 ++++++++++++-------------------------------- 1 file changed, 37 insertions(+), 106 deletions(-) diff --git a/discovery_server.go b/discovery_server.go index 8e4a4c0..d39278b 100644 --- a/discovery_server.go +++ b/discovery_server.go @@ -72,62 +72,38 @@ func (d *DiscoveryServer) Run(in io.Reader, out io.Writer) error { for { fullCmd, err := reader.ReadString('\n') if err != nil { - d.output(&genericMessageJSON{ - EventType: "command_error", - Error: true, - Message: err.Error(), - }) + d.outputError("command_error", err.Error()) return err } split := strings.Split(fullCmd, " ") cmd := strings.ToUpper(strings.TrimSpace(split[0])) if !d.initialized && cmd != "HELLO" { - d.output(&genericMessageJSON{ - EventType: "command_error", - Error: true, - Message: fmt.Sprintf("First command must be HELLO, but got '%s'", cmd), - }) + d.outputError("command_error", fmt.Sprintf("First command must be HELLO, but got '%s'", cmd)) continue } switch cmd { case "HELLO": if d.initialized { - d.output(&genericMessageJSON{ - EventType: "hello", - Error: true, - Message: "HELLO already called", - }) + d.outputError("hello", "HELLO already called") continue } re := regexp.MustCompile(`(\d+) "([^"]+)"`) matches := re.FindStringSubmatch(fullCmd[6:]) if len(matches) != 3 { - d.output(&genericMessageJSON{ - EventType: "hello", - Error: true, - Message: "Invalid HELLO command", - }) + d.outputError("hello", "Invalid HELLO command") continue } d.userAgent = matches[2] if v, err := strconv.ParseInt(matches[1], 10, 64); err != nil { - d.output(&genericMessageJSON{ - EventType: "hello", - Error: true, - Message: "Invalid protocol version: " + matches[2], - }) + d.outputError("hello", "Invalid protocol version: "+matches[2]) continue } else { d.reqProtocolVersion = int(v) } if err := d.impl.Hello(d.userAgent, 1); err != nil { - d.output(&genericMessageJSON{ - EventType: "hello", - Error: true, - Message: err.Error(), - }) + d.outputError("hello", err.Error()) continue } d.output(&genericMessageJSON{ @@ -139,58 +115,31 @@ func (d *DiscoveryServer) Run(in io.Reader, out io.Writer) error { case "START": if d.started { - d.output(&genericMessageJSON{ - EventType: "start", - Error: true, - Message: "Discovery already STARTed", - }) + d.outputError("start", "Discovery already STARTed") continue } if d.syncStarted { - d.output(&genericMessageJSON{ - EventType: "start", - Error: true, - Message: "Discovery already START_SYNCed, cannot START", - }) + d.outputError("start", "Discovery already START_SYNCed, cannot START") continue } if err := d.impl.Start(); err != nil { - d.output(&genericMessageJSON{ - EventType: "start", - Error: true, - Message: "Cannot START: " + err.Error(), - }) + d.outputError("start", "Cannot START: "+err.Error()) continue } d.started = true - d.output(&genericMessageJSON{ - EventType: "start", - Message: "OK", - }) + d.outputOk("start") case "LIST": if !d.started { - d.output(&genericMessageJSON{ - EventType: "list", - Error: true, - Message: "Discovery not STARTed", - }) + d.outputError("list", "Discovery not STARTed") continue } if d.syncStarted { - d.output(&genericMessageJSON{ - EventType: "list", - Error: true, - Message: "discovery already START_SYNCed, LIST not allowed", - }) + d.outputError("list", "discovery already START_SYNCed, LIST not allowed") continue } if ports, err := d.impl.List(); err != nil { - d.output(&genericMessageJSON{ - EventType: "list", - Error: true, - Message: "LIST error: " + err.Error(), - }) + d.outputError("list", "LIST error: "+err.Error()) continue } else { type listOutputJSON struct { @@ -205,52 +154,29 @@ func (d *DiscoveryServer) Run(in io.Reader, out io.Writer) error { case "START_SYNC": if d.syncStarted { - d.output(&genericMessageJSON{ - EventType: "start_sync", - Error: true, - Message: "Discovery already START_SYNCed", - }) + d.outputError("start_sync", "Discovery already START_SYNCed") continue } if d.started { - d.output(&genericMessageJSON{ - EventType: "start_sync", - Error: true, - Message: "Discovery already STARTed, cannot START_SYNC", - }) + d.outputError("start_sync", "Discovery already STARTed, cannot START_SYNC") continue } if c, err := d.impl.StartSync(d.syncEvent); err != nil { - d.output(&genericMessageJSON{ - EventType: "start_sync", - Error: true, - Message: "Cannot START_SYNC: " + err.Error(), - }) + d.outputError("start_sync", "Cannot START_SYNC: "+err.Error()) continue } else { d.syncCloseChan = c d.syncStarted = true - d.output(&genericMessageJSON{ - EventType: "start_sync", - Message: "OK", - }) + d.outputOk("start_sync") } case "STOP": if !d.syncStarted && !d.started { - d.output(&genericMessageJSON{ - EventType: "stop", - Error: true, - Message: "Discovery already STOPped", - }) + d.outputError("stop", "Discovery already STOPped") continue } if err := d.impl.Stop(); err != nil { - d.output(&genericMessageJSON{ - EventType: "stop", - Error: true, - Message: "Cannot STOP: " + err.Error(), - }) + d.outputError("stop", "Cannot STOP: "+err.Error()) continue } if d.started { @@ -261,24 +187,14 @@ func (d *DiscoveryServer) Run(in io.Reader, out io.Writer) error { close(d.syncCloseChan) d.syncStarted = false } - d.output(&genericMessageJSON{ - EventType: "stop", - Message: "OK", - }) + d.outputOk("stop") case "QUIT": - d.output(&genericMessageJSON{ - EventType: "quit", - Message: "OK", - }) + d.outputOk("quit") return nil default: - d.output(&genericMessageJSON{ - EventType: "command_error", - Error: true, - Message: fmt.Sprintf("Command %s not supported", cmd), - }) + d.outputError("command_error", fmt.Sprintf("Command %s not supported", cmd)) } } } @@ -301,6 +217,21 @@ type genericMessageJSON struct { ProtocolVersion int `json:"protocolVersion,omitempty"` } +func (d *DiscoveryServer) outputOk(event string) { + d.output(&genericMessageJSON{ + EventType: event, + Message: "OK", + }) +} + +func (d *DiscoveryServer) outputError(event, msg string) { + d.output(&genericMessageJSON{ + EventType: event, + Error: true, + Message: msg, + }) +} + func (d *DiscoveryServer) output(msg interface{}) { data, err := json.MarshalIndent(msg, "", " ") if err != nil { From 85d0f954cf3b8591548db6e1590c44f6beb2109a Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Fri, 16 Jul 2021 17:09:26 +0200 Subject: [PATCH 05/20] Moved command handlers in their own method --- discovery_server.go | 216 +++++++++++++++++++++++--------------------- 1 file changed, 115 insertions(+), 101 deletions(-) diff --git a/discovery_server.go b/discovery_server.go index d39278b..8f961bf 100644 --- a/discovery_server.go +++ b/discovery_server.go @@ -85,120 +85,134 @@ func (d *DiscoveryServer) Run(in io.Reader, out io.Writer) error { switch cmd { case "HELLO": - if d.initialized { - d.outputError("hello", "HELLO already called") - continue - } - re := regexp.MustCompile(`(\d+) "([^"]+)"`) - matches := re.FindStringSubmatch(fullCmd[6:]) - if len(matches) != 3 { - d.outputError("hello", "Invalid HELLO command") - continue - } - d.userAgent = matches[2] - if v, err := strconv.ParseInt(matches[1], 10, 64); err != nil { - d.outputError("hello", "Invalid protocol version: "+matches[2]) - continue - } else { - d.reqProtocolVersion = int(v) - } - if err := d.impl.Hello(d.userAgent, 1); err != nil { - d.outputError("hello", err.Error()) - continue - } - d.output(&genericMessageJSON{ - EventType: "hello", - ProtocolVersion: 1, // Protocol version 1 is the only supported for now... - Message: "OK", - }) - d.initialized = true - + d.hello(fullCmd[6:]) case "START": - if d.started { - d.outputError("start", "Discovery already STARTed") - continue - } - if d.syncStarted { - d.outputError("start", "Discovery already START_SYNCed, cannot START") - continue - } - if err := d.impl.Start(); err != nil { - d.outputError("start", "Cannot START: "+err.Error()) - continue - } - d.started = true - d.outputOk("start") - + d.start() case "LIST": - if !d.started { - d.outputError("list", "Discovery not STARTed") - continue - } - if d.syncStarted { - d.outputError("list", "discovery already START_SYNCed, LIST not allowed") - continue - } - if ports, err := d.impl.List(); err != nil { - d.outputError("list", "LIST error: "+err.Error()) - continue - } else { - type listOutputJSON struct { - EventType string `json:"eventType"` - Ports []*Port `json:"ports"` - } - d.output(&listOutputJSON{ - EventType: "list", - Ports: ports, - }) - } - + d.list() case "START_SYNC": - if d.syncStarted { - d.outputError("start_sync", "Discovery already START_SYNCed") - continue - } - if d.started { - d.outputError("start_sync", "Discovery already STARTed, cannot START_SYNC") - continue - } - if c, err := d.impl.StartSync(d.syncEvent); err != nil { - d.outputError("start_sync", "Cannot START_SYNC: "+err.Error()) - continue - } else { - d.syncCloseChan = c - d.syncStarted = true - d.outputOk("start_sync") - } - + d.startSync() case "STOP": - if !d.syncStarted && !d.started { - d.outputError("stop", "Discovery already STOPped") - continue - } - if err := d.impl.Stop(); err != nil { - d.outputError("stop", "Cannot STOP: "+err.Error()) - continue - } - if d.started { - d.started = false - } - if d.syncStarted { - d.syncCloseChan <- true - close(d.syncCloseChan) - d.syncStarted = false - } - d.outputOk("stop") - + d.stop() case "QUIT": d.outputOk("quit") return nil - default: d.outputError("command_error", fmt.Sprintf("Command %s not supported", cmd)) } } } +func (d *DiscoveryServer) hello(cmd string) { + if d.initialized { + d.outputError("hello", "HELLO already called") + return + } + re := regexp.MustCompile(`(\d+) "([^"]+)"`) + matches := re.FindStringSubmatch(cmd) + if len(matches) != 3 { + d.outputError("hello", "Invalid HELLO command") + return + } + d.userAgent = matches[2] + if v, err := strconv.ParseInt(matches[1], 10, 64); err != nil { + d.outputError("hello", "Invalid protocol version: "+matches[2]) + return + } else { + d.reqProtocolVersion = int(v) + } + if err := d.impl.Hello(d.userAgent, 1); err != nil { + d.outputError("hello", err.Error()) + return + } + d.output(&genericMessageJSON{ + EventType: "hello", + ProtocolVersion: 1, // Protocol version 1 is the only supported for now... + Message: "OK", + }) + d.initialized = true +} + +func (d *DiscoveryServer) start() { + if d.started { + d.outputError("start", "Discovery already STARTed") + return + } + if d.syncStarted { + d.outputError("start", "Discovery already START_SYNCed, cannot START") + return + } + if err := d.impl.Start(); err != nil { + d.outputError("start", "Cannot START: "+err.Error()) + return + } + d.started = true + d.outputOk("start") +} + +func (d *DiscoveryServer) list() { + if !d.started { + d.outputError("list", "Discovery not STARTed") + return + } + if d.syncStarted { + d.outputError("list", "discovery already START_SYNCed, LIST not allowed") + return + } + if ports, err := d.impl.List(); err != nil { + d.outputError("list", "LIST error: "+err.Error()) + return + } else { + type listOutputJSON struct { + EventType string `json:"eventType"` + Ports []*Port `json:"ports"` + } + d.output(&listOutputJSON{ + EventType: "list", + Ports: ports, + }) + } +} + +func (d *DiscoveryServer) startSync() { + if d.syncStarted { + d.outputError("start_sync", "Discovery already START_SYNCed") + return + } + if d.started { + d.outputError("start_sync", "Discovery already STARTed, cannot START_SYNC") + return + } + if c, err := d.impl.StartSync(d.syncEvent); err != nil { + d.outputError("start_sync", "Cannot START_SYNC: "+err.Error()) + return + } else { + d.syncCloseChan = c + d.syncStarted = true + d.outputOk("start_sync") + } +} + +func (d *DiscoveryServer) stop() { + if !d.syncStarted && !d.started { + d.outputError("stop", "Discovery already STOPped") + return + } + if err := d.impl.Stop(); err != nil { + d.outputError("stop", "Cannot STOP: "+err.Error()) + return + } + if d.started { + d.started = false + } + if d.syncStarted { + d.syncCloseChan <- true + close(d.syncCloseChan) + d.syncStarted = false + } + d.outputOk("stop") +} + func (d *DiscoveryServer) syncEvent(event string, port *Port) { type syncOutputJSON struct { EventType string `json:"eventType"` From 090c9aab706d6601e2529d783373e0efc137858a Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Fri, 16 Jul 2021 17:40:37 +0200 Subject: [PATCH 06/20] Added godoc comments --- discovery_server.go | 49 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/discovery_server.go b/discovery_server.go index 8f961bf..4823b59 100644 --- a/discovery_server.go +++ b/discovery_server.go @@ -15,6 +15,15 @@ // a commercial license, send an email to license@arduino.cc. // +// discovery is a library for handling the Arduino Pluggable-Discovery protocol +// (https://github.com/arduino/tooling-rfcs/blob/main/RFCs/0002-pluggable-discovery.md#pluggable-discovery-api-via-stdinstdout) +// +// The library implements the state machine and the parsing logic to communicate with a pluggable-discovery client. +// All the commands issued by the client are conveniently translated into function calls, in particular +// the Discovery interface are the only functions that must be implemented to get a fully working pluggable discovery +// using this library. +// +// A usage example is provided in the dummy-discovery package. package discovery import ( @@ -30,6 +39,7 @@ import ( "github.com/arduino/go-properties-orderedmap" ) +// Port is a descriptor for a board port type Port struct { Address string `json:"address"` AddressLabel string `json:"label,omitempty"` @@ -38,16 +48,39 @@ type Port struct { Properties *properties.Map `json:"properties,omitempty"` } -type EventCallback func(event string, port *Port) - +// Discovery is an interface that represents the business logic that +// a pluggable discovery must implement. The communication protocol +// is completely hidden and it's handled by a DiscoveryServer. type Discovery interface { + // Hello is called once at startup to provide the userAgent string + // and the protocolVersion negotiated with the client. Hello(userAgent string, protocolVersion int) error + + // Start is called to start the discovery internal subroutines. Start() error - Stop() error - List() ([]*Port, error) + + // List returns the list of the currently available ports. It works + // only after a Start. + List() (portList []*Port, err error) + + // StartSync is called to put the discovery in event mode. When the + // function returns the discovery must send port events ("add" or "remove") + // using the eventCB function. StartSync(eventCB EventCallback) (chan<- bool, error) + + // Stop stops the discovery internal subroutines. If the discovery is + // in event mode it must stop sending events through the eventCB previously + // set. + Stop() error } +// EventCallback is a callback function to call to transmit port +// metadata when the discovery is in "sync" mode and a new event +// is detected. +type EventCallback func(event string, port *Port) + +// A DiscoveryServer is a pluggable discovery protocol handler, +// it must be created using the NewDiscoveryServer function. type DiscoveryServer struct { impl Discovery out io.Writer @@ -60,12 +93,20 @@ type DiscoveryServer struct { syncCloseChan chan<- bool } +// NewDiscoveryServer creates a new discovery server backed by the +// provided pluggable discovery implementation. To start the server +// use the Run method. func NewDiscoveryServer(impl Discovery) *DiscoveryServer { return &DiscoveryServer{ impl: impl, } } +// Run starts the protocol handling loop on the given input and +// output stream, usually `os.Stdin` and `os.Stdout` are used. +// The function blocks until the `QUIT` command is received or +// the input stream is closed. In case of IO error the error is +// returned. func (d *DiscoveryServer) Run(in io.Reader, out io.Writer) error { d.out = out reader := bufio.NewReader(in) From 48b60e6c9804f67660f11247a40dfef6791b6f39 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Fri, 16 Jul 2021 17:46:52 +0200 Subject: [PATCH 07/20] We do not need the closing channel exposed, Stop() is sufficient --- discovery_server.go | 21 ++++++--------------- dummy-discovery/main.go | 12 +++++++++--- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/discovery_server.go b/discovery_server.go index 4823b59..7f9c6f0 100644 --- a/discovery_server.go +++ b/discovery_server.go @@ -66,7 +66,7 @@ type Discovery interface { // StartSync is called to put the discovery in event mode. When the // function returns the discovery must send port events ("add" or "remove") // using the eventCB function. - StartSync(eventCB EventCallback) (chan<- bool, error) + StartSync(eventCB EventCallback) error // Stop stops the discovery internal subroutines. If the discovery is // in event mode it must stop sending events through the eventCB previously @@ -90,7 +90,6 @@ type DiscoveryServer struct { initialized bool started bool syncStarted bool - syncCloseChan chan<- bool } // NewDiscoveryServer creates a new discovery server backed by the @@ -224,14 +223,12 @@ func (d *DiscoveryServer) startSync() { d.outputError("start_sync", "Discovery already STARTed, cannot START_SYNC") return } - if c, err := d.impl.StartSync(d.syncEvent); err != nil { + if err := d.impl.StartSync(d.syncEvent); err != nil { d.outputError("start_sync", "Cannot START_SYNC: "+err.Error()) return - } else { - d.syncCloseChan = c - d.syncStarted = true - d.outputOk("start_sync") } + d.syncStarted = true + d.outputOk("start_sync") } func (d *DiscoveryServer) stop() { @@ -243,14 +240,8 @@ func (d *DiscoveryServer) stop() { d.outputError("stop", "Cannot STOP: "+err.Error()) return } - if d.started { - d.started = false - } - if d.syncStarted { - d.syncCloseChan <- true - close(d.syncCloseChan) - d.syncStarted = false - } + d.started = false + d.syncStarted = false d.outputOk("stop") } diff --git a/dummy-discovery/main.go b/dummy-discovery/main.go index 730bafe..1e7b5be 100644 --- a/dummy-discovery/main.go +++ b/dummy-discovery/main.go @@ -31,6 +31,7 @@ import ( type DummyDiscovery struct { startSyncCount int listCount int + closeChan chan<- bool } func main() { @@ -61,16 +62,21 @@ func (d *DummyDiscovery) Start() error { } func (d *DummyDiscovery) Stop() error { + if d.closeChan != nil { + d.closeChan <- true + d.closeChan = nil + } return nil } -func (d *DummyDiscovery) StartSync(eventCB discovery.EventCallback) (chan<- bool, error) { +func (d *DummyDiscovery) StartSync(eventCB discovery.EventCallback) error { d.startSyncCount++ if d.startSyncCount%5 == 0 { - return nil, errors.New("could not start_sync every 5 times") + return errors.New("could not start_sync every 5 times") } c := make(chan bool) + d.closeChan = c // Run synchronous event emitter go func() { @@ -113,7 +119,7 @@ func (d *DummyDiscovery) StartSync(eventCB discovery.EventCallback) (chan<- bool } }() - return c, nil + return nil } var dummyCounter = 0 From a061d49a741497867d4ffbaaeea52fd3f535fab0 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Fri, 16 Jul 2021 17:48:49 +0200 Subject: [PATCH 08/20] Add a final \n on the json output for better readability --- discovery_server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discovery_server.go b/discovery_server.go index 7f9c6f0..ba7cb74 100644 --- a/discovery_server.go +++ b/discovery_server.go @@ -288,7 +288,7 @@ func (d *DiscoveryServer) output(msg interface{}) { }) } else { d.outMutex.Lock() - d.out.Write(data) + fmt.Fprintln(d.out, string(data)) d.outMutex.Unlock() } } From d1f301b7bd1c2e6ba49e3ee7dbc0860a1b928358 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Fri, 16 Jul 2021 17:58:04 +0200 Subject: [PATCH 09/20] Remove useless comment --- dummy-discovery/main.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/dummy-discovery/main.go b/dummy-discovery/main.go index 1e7b5be..a504340 100644 --- a/dummy-discovery/main.go +++ b/dummy-discovery/main.go @@ -88,15 +88,6 @@ func (d *DummyDiscovery) StartSync(eventCB discovery.EventCallback) error { // Start sending events for { - // if err != nil { - // output(&genericMessageJSON{ - // EventType: "start_sync", - // Error: true, - // Message: fmt.Sprintf("error decoding START_SYNC event: %s", err), - // }) - // return - // } - select { case <-closeChan: return From 21f28b6ec29c3fed843ff8e968cc14aa4aeaf10b Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Mon, 19 Jul 2021 11:52:09 +0200 Subject: [PATCH 10/20] Added Quit method --- discovery_server.go | 5 +++++ dummy-discovery/main.go | 2 ++ 2 files changed, 7 insertions(+) diff --git a/discovery_server.go b/discovery_server.go index ba7cb74..2efe6a4 100644 --- a/discovery_server.go +++ b/discovery_server.go @@ -72,6 +72,10 @@ type Discovery interface { // in event mode it must stop sending events through the eventCB previously // set. Stop() error + + // Quit is called just before the server terminates. This function can be + // used by the discovery as a last chance gracefully close resources. + Quit() } // EventCallback is a callback function to call to transmit port @@ -135,6 +139,7 @@ func (d *DiscoveryServer) Run(in io.Reader, out io.Writer) error { case "STOP": d.stop() case "QUIT": + d.impl.Quit() d.outputOk("quit") return nil default: diff --git a/dummy-discovery/main.go b/dummy-discovery/main.go index a504340..81e684c 100644 --- a/dummy-discovery/main.go +++ b/dummy-discovery/main.go @@ -47,6 +47,8 @@ func (d *DummyDiscovery) Hello(userAgent string, protocol int) error { return nil } +func (d *DummyDiscovery) Quit() {} + func (d *DummyDiscovery) List() ([]*discovery.Port, error) { d.listCount++ if d.listCount%5 == 0 { From a31ff4c7e44795603a1edf777e53824ba95ec06a Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Mon, 19 Jul 2021 12:03:49 +0200 Subject: [PATCH 11/20] added readme to dummy-discovery --- dummy-discovery/README.md | 265 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 265 insertions(+) create mode 100644 dummy-discovery/README.md diff --git a/dummy-discovery/README.md b/dummy-discovery/README.md new file mode 100644 index 0000000..7d83eb0 --- /dev/null +++ b/dummy-discovery/README.md @@ -0,0 +1,265 @@ +# Arduino pluggable discovery reference implementation + +The `dummy-discovery` tool is a command line program that interacts via stdio. It accepts commands as plain ASCII strings terminated with LF `\n` and sends response as JSON. +The "communication ports" returned by the tool are fake, this discovery is intenteded only for educational and testing purposes. + +## How to build + +Install a recent go enviroment and run `go build`. The executable `dummy-discovery` will be produced in your working directory. + +## Usage + +After startup, the tool waits for commands. The available commands are: `HELLO`, `START`, `STOP`, `QUIT`, `LIST` and `START_SYNC`. + +#### HELLO command + +The `HELLO` command is used to establish the pluggable discovery protocol between client and discovery. +The format of the command is: + +`HELLO ""` + +for example: + +`HELLO 1 "Arduino IDE"` + +or: + +`HELLO 1 "arduino-cli"` + +in this case the protocol version requested by the client is `1` (at the moment of writing there were no other revisions of the protocol). +The response to the command is: + +```json +{ + "eventType": "hello", + "protocolVersion": 1, + "message": "OK" +} +``` + +`protocolVersion` is the protocol version that the discovery is going to use in the remainder of the communication. + +#### START command + +The `START` starts the internal subroutines of the discovery that looks for ports. This command must be called before `LIST` or `START_SYNC`. The response to the start command is: + +```json +{ + "eventType": "start", + "message": "OK" +} +``` + +#### STOP command + +The `STOP` command stops the discovery internal subroutines and free some resources. This command should be called if the client wants to pause the discovery for a while. The response to the stop command is: + +```json +{ + "eventType": "stop", + "message": "OK" +} +``` + +#### QUIT command + +The `QUIT` command terminates the discovery. The response to quit is: + +```json +{ + "eventType": "quit", + "message": "OK" +} +``` + +after this output the tool quits. + +#### LIST command + +The `LIST` command returns a list of the currently available serial ports. The format of the response is the following: + +```json +{ + "eventType": "list", + "ports": [ + { + "address": "1", + "label": "Dummy upload port", + "protocol": "dummy", + "protocolLabel": "Dummy protocol", + "properties": { + "mac": "73622384782", + "pid": "0x0041", + "vid": "0x2341" + } + } + ] +} +``` + +#### START_SYNC command + +The `START_SYNC` command puts the tool in "events" mode: the discovery will send `add` and `remove` events each time a new port is detected or removed respectively. +The immediate response to the command is: + +```json +{ + "eventType": "start_sync", + "message": "OK" +} +``` + +after that the discovery enters in "events" mode. + +The `add` events looks like the following: + +```json + + "eventType": "add", + "port": { + "address": "4", + "label": "Dummy upload port", + "protocol": "dummy", + "protocolLabel": "Dummy protocol", + "properties": { + "mac": "294489539128", + "pid": "0x0041", + "vid": "0x2341" + } + } +} +``` + +it basically gather the same information as the `list` event but for a single port. After calling `START_SYNC` a bunch of `add` events may be generated in sequence to report all the ports available at the moment of the start. + +The `remove` event looks like this: + +```json +{ + "eventType": "remove", + "port": { + "address": "4", + "protocol": "dummy" + } +} +``` + +in this case only the `address` and `protocol` fields are reported. + +### Example of usage + +A possible transcript of the discovery usage: + +``` +HELLO 1 "arduino-cli" +{ + "eventType": "hello", + "message": "OK", + "protocolVersion": 1 +} +START +{ + "eventType": "start", + "message": "OK" +} +LIST +{ + "eventType": "list", + "ports": [ + { + "address": "1", + "label": "Dummy upload port", + "protocol": "dummy", + "protocolLabel": "Dummy protocol", + "properties": { + "mac": "73622384782", + "pid": "0x0041", + "vid": "0x2341" + } + } + ] +} +STOP +{ + "eventType": "stop", + "message": "OK" +} +START_SYNC +{ + "eventType": "start_sync", + "message": "OK" +} +{ + "eventType": "add", + "port": { + "address": "2", + "label": "Dummy upload port", + "protocol": "dummy", + "protocolLabel": "Dummy protocol", + "properties": { + "mac": "147244769564", + "pid": "0x0041", + "vid": "0x2341" + } + } +} +{ + "eventType": "add", + "port": { + "address": "3", + "label": "Dummy upload port", + "protocol": "dummy", + "protocolLabel": "Dummy protocol", + "properties": { + "mac": "220867154346", + "pid": "0x0041", + "vid": "0x2341" + } + } +} +{ + "eventType": "add", + "port": { + "address": "4", + "label": "Dummy upload port", + "protocol": "dummy", + "protocolLabel": "Dummy protocol", + "properties": { + "mac": "294489539128", + "pid": "0x0041", + "vid": "0x2341" + } + } +} +{ + "eventType": "remove", + "port": { + "address": "4", + "protocol": "dummy" + } +} +QUIT +{ + "eventType": "quit", + "message": "OK" +} +$ +``` + +## Security + +If you think you found a vulnerability or other security-related bug in this project, please read our +[security policy](https://github.com/arduino/serial-discovery/security/policy) and report the bug to our Security Team 🛡️ +Thank you! + +e-mail contact: security@arduino.cc + +## License + +Copyright (c) 2021 ARDUINO SA (www.arduino.cc) + +The software is released under the GNU General Public License, which covers the main body +of the serial-discovery code. The terms of this license can be found at: +https://www.gnu.org/licenses/gpl-3.0.en.html + +See [LICENSE.txt](https://github.com/arduino/serial-discovery/blob/master/LICENSE.txt) for details. From 0e4f7517e818efae598e659917001de1e64e5df8 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Mon, 19 Jul 2021 12:05:23 +0200 Subject: [PATCH 12/20] Draft main README --- README.md | 245 +----------------------------------------------------- 1 file changed, 1 insertion(+), 244 deletions(-) diff --git a/README.md b/README.md index 255f043..6915653 100644 --- a/README.md +++ b/README.md @@ -1,250 +1,7 @@ -# Arduino pluggabe discovery reference implementation - -The `dummy-discovery` tool is a command line program that interacts via stdio. It accepts commands as plain ASCII strings terminated with LF `\n` and sends response as JSON. +# A golang library to handle the Arduino pluggable-discovery communication protocol. ## How to build -Install a recent go enviroment and run `go build`. The executable `dummy-discovery` will be produced in your working directory. - -## Usage - -After startup, the tool waits for commands. The available commands are: `HELLO`, `START`, `STOP`, `QUIT`, `LIST` and `START_SYNC`. - -#### HELLO command - -The `HELLO` command is used to establish the pluggable discovery protocol between client and discovery. -The format of the command is: - -`HELLO ""` - -for example: - -`HELLO 1 "Arduino IDE"` - -or: - -`HELLO 1 "arduino-cli"` - -in this case the protocol version requested by the client is `1` (at the moment of writing there were no other revisions of the protocol). -The response to the command is: - -```json -{ - "eventType": "hello", - "protocolVersion": 1, - "message": "OK" -} -``` - -`protocolVersion` is the protocol version that the discovery is going to use in the remainder of the communication. - -#### START command - -The `START` starts the internal subroutines of the discovery that looks for ports. This command must be called before `LIST` or `START_SYNC`. The response to the start command is: - -```json -{ - "eventType": "start", - "message": "OK" -} -``` - -#### STOP command - -The `STOP` command stops the discovery internal subroutines and free some resources. This command should be called if the client wants to pause the discovery for a while. The response to the stop command is: - -```json -{ - "eventType": "stop", - "message": "OK" -} -``` - -#### QUIT command - -The `QUIT` command terminates the discovery. The response to quit is: - -```json -{ - "eventType": "quit", - "message": "OK" -} -``` - -after this output the tool quits. - -#### LIST command - -The `LIST` command returns a list of the currently available serial ports. The format of the response is the following: - -```json -{ - "eventType": "list", - "ports": [ - { - "address": "1", - "label": "Dummy upload port", - "protocol": "dummy", - "protocolLabel": "Dummy protocol", - "properties": { - "mac": "73622384782", - "pid": "0x0041", - "vid": "0x2341" - } - } - ] -} -``` - -#### START_SYNC command - -The `START_SYNC` command puts the tool in "events" mode: the discovery will send `add` and `remove` events each time a new port is detected or removed respectively. -The immediate response to the command is: - -```json -{ - "eventType": "start_sync", - "message": "OK" -} -``` - -after that the discovery enters in "events" mode. - -The `add` events looks like the following: - -```json - - "eventType": "add", - "port": { - "address": "4", - "label": "Dummy upload port", - "protocol": "dummy", - "protocolLabel": "Dummy protocol", - "properties": { - "mac": "294489539128", - "pid": "0x0041", - "vid": "0x2341" - } - } -} -``` - -it basically gather the same information as the `list` event but for a single port. After calling `START_SYNC` a bunch of `add` events may be generated in sequence to report all the ports available at the moment of the start. - -The `remove` event looks like this: - -```json -{ - "eventType": "remove", - "port": { - "address": "4", - "protocol": "dummy" - } -} -``` - -in this case only the `address` and `protocol` fields are reported. - -### Example of usage - -A possible transcript of the discovery usage: - -``` -HELLO 1 "arduino-cli" -{ - "eventType": "hello", - "message": "OK", - "protocolVersion": 1 -} -START -{ - "eventType": "start", - "message": "OK" -} -LIST -{ - "eventType": "list", - "ports": [ - { - "address": "1", - "label": "Dummy upload port", - "protocol": "dummy", - "protocolLabel": "Dummy protocol", - "properties": { - "mac": "73622384782", - "pid": "0x0041", - "vid": "0x2341" - } - } - ] -} -STOP -{ - "eventType": "stop", - "message": "OK" -} -START_SYNC -{ - "eventType": "start_sync", - "message": "OK" -} -{ - "eventType": "add", - "port": { - "address": "2", - "label": "Dummy upload port", - "protocol": "dummy", - "protocolLabel": "Dummy protocol", - "properties": { - "mac": "147244769564", - "pid": "0x0041", - "vid": "0x2341" - } - } -} -{ - "eventType": "add", - "port": { - "address": "3", - "label": "Dummy upload port", - "protocol": "dummy", - "protocolLabel": "Dummy protocol", - "properties": { - "mac": "220867154346", - "pid": "0x0041", - "vid": "0x2341" - } - } -} -{ - "eventType": "add", - "port": { - "address": "4", - "label": "Dummy upload port", - "protocol": "dummy", - "protocolLabel": "Dummy protocol", - "properties": { - "mac": "294489539128", - "pid": "0x0041", - "vid": "0x2341" - } - } -} -{ - "eventType": "remove", - "port": { - "address": "4", - "protocol": "dummy" - } -} -QUIT -{ - "eventType": "quit", - "message": "OK" -} -$ -``` - ## Security If you think you found a vulnerability or other security-related bug in this project, please read our From ac9e703a1362fae396e0137ec129d5617a727e3d Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Mon, 19 Jul 2021 12:13:49 +0200 Subject: [PATCH 13/20] Renamed repo --- Taskfile.yml | 4 ++-- discovery_server.go | 2 +- dummy-discovery/args/args.go | 2 +- dummy-discovery/main.go | 4 ++-- go.mod | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Taskfile.yml b/Taskfile.yml index 50aaca8..44dff96 100755 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -14,5 +14,5 @@ vars: TIMESTAMP: sh: echo "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" LDFLAGS: > - -X github.com/arduino/dummy-discovery/dummy-discovery/args.Tag={{.VERSION}} - -X github.com/arduino/dummy-discovery/dummy-discovery/args.Timestamp={{.TIMESTAMP}} + -X github.com/arduino/pluggable-discovery-protocol-handler/dummy-discovery/args.Tag={{.VERSION}} + -X github.com/arduino/pluggable-discovery-protocol-handler/dummy-discovery/args.Timestamp={{.TIMESTAMP}} diff --git a/discovery_server.go b/discovery_server.go index 2efe6a4..a6fe7fe 100644 --- a/discovery_server.go +++ b/discovery_server.go @@ -1,5 +1,5 @@ // -// This file is part of dummy-discovery. +// This file is part of pluggable-discovery-protocol-handler. // // Copyright 2021 ARDUINO SA (http://www.arduino.cc/) // diff --git a/dummy-discovery/args/args.go b/dummy-discovery/args/args.go index 7480264..c547fa3 100644 --- a/dummy-discovery/args/args.go +++ b/dummy-discovery/args/args.go @@ -34,7 +34,7 @@ func ParseArgs() { continue } if arg == "-v" || arg == "--version" { - fmt.Printf("serial-discovery %s (build timestamp: %s)\n", Tag, Timestamp) + fmt.Printf("dummy-discovery %s (build timestamp: %s)\n", Tag, Timestamp) os.Exit(0) } fmt.Fprintf(os.Stderr, "invalid argument: %s\n", arg) diff --git a/dummy-discovery/main.go b/dummy-discovery/main.go index 81e684c..f2c791a 100644 --- a/dummy-discovery/main.go +++ b/dummy-discovery/main.go @@ -23,9 +23,9 @@ import ( "os" "time" - discovery "github.com/arduino/dummy-discovery" - "github.com/arduino/dummy-discovery/dummy-discovery/args" "github.com/arduino/go-properties-orderedmap" + discovery "github.com/arduino/pluggable-discovery-protocol-handler" + "github.com/arduino/pluggable-discovery-protocol-handler/dummy-discovery/args" ) type DummyDiscovery struct { diff --git a/go.mod b/go.mod index 222bd4f..0fb3acd 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/arduino/dummy-discovery +module github.com/arduino/pluggable-discovery-protocol-handler go 1.16 From 4569ac0ccc6817806901c38ab4c58d67942a26ed Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Mon, 19 Jul 2021 13:09:57 +0200 Subject: [PATCH 14/20] In sync mode output events after command acknowledge --- discovery_server.go | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/discovery_server.go b/discovery_server.go index a6fe7fe..d65296d 100644 --- a/discovery_server.go +++ b/discovery_server.go @@ -94,6 +94,7 @@ type DiscoveryServer struct { initialized bool started bool syncStarted bool + syncChannel chan interface{} } // NewDiscoveryServer creates a new discovery server backed by the @@ -228,12 +229,22 @@ func (d *DiscoveryServer) startSync() { d.outputError("start_sync", "Discovery already STARTed, cannot START_SYNC") return } + d.syncChannel = make(chan interface{}, 10) // buffer up to 10 events if err := d.impl.StartSync(d.syncEvent); err != nil { d.outputError("start_sync", "Cannot START_SYNC: "+err.Error()) + close(d.syncChannel) // do not leak channel... + d.syncChannel = nil return } d.syncStarted = true d.outputOk("start_sync") + go d.consumeEvents(d.syncChannel) +} + +func (d *DiscoveryServer) consumeEvents(c <-chan interface{}) { + for e := range c { + d.output(e) + } } func (d *DiscoveryServer) stop() { @@ -246,7 +257,11 @@ func (d *DiscoveryServer) stop() { return } d.started = false - d.syncStarted = false + if d.syncStarted { + close(d.syncChannel) + d.syncChannel = nil + d.syncStarted = false + } d.outputOk("stop") } @@ -255,10 +270,10 @@ func (d *DiscoveryServer) syncEvent(event string, port *Port) { EventType string `json:"eventType"` Port *Port `json:"port"` } - d.output(&syncOutputJSON{ + d.syncChannel <- &syncOutputJSON{ EventType: event, Port: port, - }) + } } type genericMessageJSON struct { From 5d1857242ba4f3d7def41225258c79ac5e0f79ab Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Mon, 19 Jul 2021 15:48:23 +0200 Subject: [PATCH 15/20] Trim newlines from command parsing --- discovery_server.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/discovery_server.go b/discovery_server.go index d65296d..84b8b0f 100644 --- a/discovery_server.go +++ b/discovery_server.go @@ -120,8 +120,9 @@ func (d *DiscoveryServer) Run(in io.Reader, out io.Writer) error { d.outputError("command_error", err.Error()) return err } + fullCmd = strings.TrimSpace(fullCmd) split := strings.Split(fullCmd, " ") - cmd := strings.ToUpper(strings.TrimSpace(split[0])) + cmd := strings.ToUpper(split[0]) if !d.initialized && cmd != "HELLO" { d.outputError("command_error", fmt.Sprintf("First command must be HELLO, but got '%s'", cmd)) From ecb723ce0717e37a7547267b86c694e253729808 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Mon, 19 Jul 2021 15:48:39 +0200 Subject: [PATCH 16/20] Use end-line-marker in regexp to match full lines --- discovery_server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discovery_server.go b/discovery_server.go index 84b8b0f..67a8b11 100644 --- a/discovery_server.go +++ b/discovery_server.go @@ -155,7 +155,7 @@ func (d *DiscoveryServer) hello(cmd string) { d.outputError("hello", "HELLO already called") return } - re := regexp.MustCompile(`(\d+) "([^"]+)"`) + re := regexp.MustCompile(`(\d+) "([^"]+)"$`) matches := re.FindStringSubmatch(cmd) if len(matches) != 3 { d.outputError("hello", "Invalid HELLO command") From 40c9cf74b25f77a2ea32fab40e7e8ef41d4fb2d0 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Mon, 19 Jul 2021 15:55:34 +0200 Subject: [PATCH 17/20] style: remove if/else nesting --- discovery_server.go | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/discovery_server.go b/discovery_server.go index 67a8b11..5d9639f 100644 --- a/discovery_server.go +++ b/discovery_server.go @@ -206,19 +206,20 @@ func (d *DiscoveryServer) list() { d.outputError("list", "discovery already START_SYNCed, LIST not allowed") return } - if ports, err := d.impl.List(); err != nil { + ports, err := d.impl.List() + if err != nil { d.outputError("list", "LIST error: "+err.Error()) return - } else { - type listOutputJSON struct { - EventType string `json:"eventType"` - Ports []*Port `json:"ports"` - } - d.output(&listOutputJSON{ - EventType: "list", - Ports: ports, - }) } + + type listOutputJSON struct { + EventType string `json:"eventType"` + Ports []*Port `json:"ports"` + } + d.output(&listOutputJSON{ + EventType: "list", + Ports: ports, + }) } func (d *DiscoveryServer) startSync() { From c650572afa148208cf5229621f2c740f1bf669bb Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Mon, 19 Jul 2021 15:58:24 +0200 Subject: [PATCH 18/20] Use anynomous function for channel consumption --- discovery_server.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/discovery_server.go b/discovery_server.go index 5d9639f..23968e2 100644 --- a/discovery_server.go +++ b/discovery_server.go @@ -231,7 +231,8 @@ func (d *DiscoveryServer) startSync() { d.outputError("start_sync", "Discovery already STARTed, cannot START_SYNC") return } - d.syncChannel = make(chan interface{}, 10) // buffer up to 10 events + c := make(chan interface{}, 10) // buffer up to 10 events + d.syncChannel = c if err := d.impl.StartSync(d.syncEvent); err != nil { d.outputError("start_sync", "Cannot START_SYNC: "+err.Error()) close(d.syncChannel) // do not leak channel... @@ -240,13 +241,12 @@ func (d *DiscoveryServer) startSync() { } d.syncStarted = true d.outputOk("start_sync") - go d.consumeEvents(d.syncChannel) -} -func (d *DiscoveryServer) consumeEvents(c <-chan interface{}) { - for e := range c { - d.output(e) - } + go func() { + for e := range c { + d.output(e) + } + }() } func (d *DiscoveryServer) stop() { From 7ca913aced5b275de458b06418a3c1ffca1daf65 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Mon, 19 Jul 2021 16:00:53 +0200 Subject: [PATCH 19/20] The first command can be also QUIT --- discovery_server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discovery_server.go b/discovery_server.go index 23968e2..6cca0ba 100644 --- a/discovery_server.go +++ b/discovery_server.go @@ -124,7 +124,7 @@ func (d *DiscoveryServer) Run(in io.Reader, out io.Writer) error { split := strings.Split(fullCmd, " ") cmd := strings.ToUpper(split[0]) - if !d.initialized && cmd != "HELLO" { + if !d.initialized && cmd != "HELLO" && cmd != "QUIT" { d.outputError("command_error", fmt.Sprintf("First command must be HELLO, but got '%s'", cmd)) continue } From fc56748fe30c50d822e8fa68798ceab903cebe1e Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Mon, 19 Jul 2021 16:05:18 +0200 Subject: [PATCH 20/20] Use start-line-marker in regexp to match full lines (for real now) --- discovery_server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discovery_server.go b/discovery_server.go index 6cca0ba..0382b07 100644 --- a/discovery_server.go +++ b/discovery_server.go @@ -155,7 +155,7 @@ func (d *DiscoveryServer) hello(cmd string) { d.outputError("hello", "HELLO already called") return } - re := regexp.MustCompile(`(\d+) "([^"]+)"$`) + re := regexp.MustCompile(`^(\d+) "([^"]+)"$`) matches := re.FindStringSubmatch(cmd) if len(matches) != 3 { d.outputError("hello", "Invalid HELLO command")