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/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 diff --git a/Taskfile.yml b/Taskfile.yml index 2acbb6e..44dff96 100755 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -1,10 +1,10 @@ version: "3" tasks: - build: - desc: Build the project + build-dummy-discovery: + desc: Build the dummy-discovery client example cmds: - - go build -v -ldflags '{{.LDFLAGS}}' + - go build -o dist/dummy-discovery -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/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 new file mode 100644 index 0000000..0382b07 --- /dev/null +++ b/discovery_server.go @@ -0,0 +1,316 @@ +// +// This file is part of pluggable-discovery-protocol-handler. +// +// 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. +// + +// 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 ( + "bufio" + "encoding/json" + "fmt" + "io" + "regexp" + "strconv" + "strings" + "sync" + + "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"` + Protocol string `json:"protocol,omitempty"` + ProtocolLabel string `json:"protocolLabel,omitempty"` + Properties *properties.Map `json:"properties,omitempty"` +} + +// 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 + + // 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) 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 + + // 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 +// 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 + outMutex sync.Mutex + userAgent string + reqProtocolVersion int + initialized bool + started bool + syncStarted bool + syncChannel chan interface{} +} + +// 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) + for { + fullCmd, err := reader.ReadString('\n') + if err != nil { + d.outputError("command_error", err.Error()) + return err + } + fullCmd = strings.TrimSpace(fullCmd) + split := strings.Split(fullCmd, " ") + cmd := strings.ToUpper(split[0]) + + if !d.initialized && cmd != "HELLO" && cmd != "QUIT" { + d.outputError("command_error", fmt.Sprintf("First command must be HELLO, but got '%s'", cmd)) + continue + } + + switch cmd { + case "HELLO": + d.hello(fullCmd[6:]) + case "START": + d.start() + case "LIST": + d.list() + case "START_SYNC": + d.startSync() + case "STOP": + d.stop() + case "QUIT": + d.impl.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 + } + ports, err := d.impl.List() + if err != nil { + d.outputError("list", "LIST error: "+err.Error()) + return + } + + 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 + } + 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... + d.syncChannel = nil + return + } + d.syncStarted = true + d.outputOk("start_sync") + + go func() { + for e := range c { + d.output(e) + } + }() +} + +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 + } + d.started = false + if d.syncStarted { + close(d.syncChannel) + d.syncChannel = nil + d.syncStarted = false + } + d.outputOk("stop") +} + +func (d *DiscoveryServer) syncEvent(event string, port *Port) { + type syncOutputJSON struct { + EventType string `json:"eventType"` + Port *Port `json:"port"` + } + d.syncChannel <- &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) 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 { + d.output(&genericMessageJSON{ + EventType: "command_error", + Error: true, + Message: err.Error(), + }) + } else { + d.outMutex.Lock() + fmt.Fprintln(d.out, string(data)) + d.outMutex.Unlock() + } +} 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. 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..c547fa3 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("dummy-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..f2c791a --- /dev/null +++ b/dummy-discovery/main.go @@ -0,0 +1,133 @@ +// +// 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" + + "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 { + startSyncCount int + listCount int + closeChan chan<- bool +} + +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) Quit() {} + +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 { + if d.closeChan != nil { + d.closeChan <- true + d.closeChan = nil + } + return nil +} + +func (d *DummyDiscovery) StartSync(eventCB discovery.EventCallback) error { + d.startSyncCount++ + if d.startSyncCount%5 == 0 { + return errors.New("could not start_sync every 5 times") + } + + c := make(chan bool) + d.closeChan = c + + // 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 { + 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 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/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 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"