Skip to content

Commit 2931737

Browse files
authored
[skip-changelog] Added discovery.Discovery object to handle communication with pluggable discoveries (#1029)
* Pluggable Discovery handler: first implementation * Added 'discovery_client' (for debugging discoveries) * Added 'event' mode in PluggableDiscovery * discovery_client now supports multiple discoveries * Added ID to PluggableDiscovery * Added PluggableDiscovery.String() implementation * Fixed TestDiscoveryStdioHandling * Fixed discovery test run on Windows It really takes that long for messages to go back and forth. I don't know if there is a simpler way to reduce stdio buffering time.
1 parent 18c4c40 commit 2931737

File tree

6 files changed

+805
-0
lines changed

6 files changed

+805
-0
lines changed

Diff for: arduino/discovery/discovery.go

+306
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
// This file is part of arduino-cli.
2+
//
3+
// Copyright 2020 ARDUINO SA (http://www.arduino.cc/)
4+
//
5+
// This software is released under the GNU General Public License version 3,
6+
// which covers the main part of arduino-cli.
7+
// The terms of this license can be found at:
8+
// https://www.gnu.org/licenses/gpl-3.0.en.html
9+
//
10+
// You can be released from the requirements of the above licenses by purchasing
11+
// a commercial license. Buying such a license is mandatory if you want to
12+
// modify or otherwise use the software for commercial activities involving the
13+
// Arduino software without disclosing the source code of your own applications.
14+
// To purchase a commercial license, send an email to [email protected].
15+
16+
package discovery
17+
18+
import (
19+
"encoding/json"
20+
"io"
21+
"sync"
22+
"time"
23+
24+
"github.com/arduino/arduino-cli/executils"
25+
"github.com/arduino/go-properties-orderedmap"
26+
"github.com/pkg/errors"
27+
)
28+
29+
// PluggableDiscovery is a tool that detects communication ports to interact
30+
// with the boards.
31+
type PluggableDiscovery struct {
32+
id string
33+
args []string
34+
process *executils.Process
35+
outgoingCommandsPipe io.Writer
36+
incomingMessagesChan <-chan *discoveryMessage
37+
38+
// All the following fields are guarded by statusMutex
39+
statusMutex sync.Mutex
40+
incomingMessagesError error
41+
alive bool
42+
eventsMode bool
43+
eventChan chan<- *Event
44+
cachedPorts map[string]*Port
45+
}
46+
47+
type discoveryMessage struct {
48+
EventType string `json:"eventType"`
49+
Message string `json:"message"`
50+
Ports []*Port `json:"ports"`
51+
Port *Port `json:"port"`
52+
}
53+
54+
// Port containts metadata about a port to connect to a board.
55+
type Port struct {
56+
Address string `json:"address"`
57+
AddressLabel string `json:"label"`
58+
Protocol string `json:"protocol"`
59+
ProtocolLabel string `json:"protocolLabel"`
60+
Properties *properties.Map `json:"prefs"`
61+
IdentificationProperties *properties.Map `json:"identificationPrefs"`
62+
}
63+
64+
func (p *Port) String() string {
65+
if p == nil {
66+
return "none"
67+
}
68+
return p.Address
69+
}
70+
71+
// Event is a pluggable discovery event
72+
type Event struct {
73+
Type string
74+
Port *Port
75+
}
76+
77+
// New create and connect to the given pluggable discovery
78+
func New(id string, args ...string) (*PluggableDiscovery, error) {
79+
proc, err := executils.NewProcess(args...)
80+
if err != nil {
81+
return nil, err
82+
}
83+
stdout, err := proc.StdoutPipe()
84+
if err != nil {
85+
return nil, err
86+
}
87+
stdin, err := proc.StdinPipe()
88+
if err != nil {
89+
return nil, err
90+
}
91+
if err := proc.Start(); err != nil {
92+
return nil, err
93+
}
94+
messageChan := make(chan *discoveryMessage)
95+
disc := &PluggableDiscovery{
96+
id: id,
97+
process: proc,
98+
incomingMessagesChan: messageChan,
99+
outgoingCommandsPipe: stdin,
100+
alive: true,
101+
}
102+
go disc.jsonDecodeLoop(stdout, messageChan)
103+
return disc, nil
104+
}
105+
106+
// GetID returns the identifier for this discovery
107+
func (disc *PluggableDiscovery) GetID() string {
108+
return disc.id
109+
}
110+
111+
func (disc *PluggableDiscovery) String() string {
112+
return disc.id
113+
}
114+
115+
func (disc *PluggableDiscovery) jsonDecodeLoop(in io.Reader, outChan chan<- *discoveryMessage) {
116+
decoder := json.NewDecoder(in)
117+
closeAndReportError := func(err error) {
118+
disc.statusMutex.Lock()
119+
disc.alive = false
120+
disc.incomingMessagesError = err
121+
disc.statusMutex.Unlock()
122+
close(outChan)
123+
}
124+
for {
125+
var msg discoveryMessage
126+
if err := decoder.Decode(&msg); err != nil {
127+
closeAndReportError(err)
128+
return
129+
}
130+
131+
if msg.EventType == "add" {
132+
if msg.Port == nil {
133+
closeAndReportError(errors.New("invalid 'add' message: missing port"))
134+
return
135+
}
136+
disc.statusMutex.Lock()
137+
disc.cachedPorts[msg.Port.Address] = msg.Port
138+
if disc.eventChan != nil {
139+
disc.eventChan <- &Event{"add", msg.Port}
140+
}
141+
disc.statusMutex.Unlock()
142+
} else if msg.EventType == "remove" {
143+
if msg.Port == nil {
144+
closeAndReportError(errors.New("invalid 'remove' message: missing port"))
145+
return
146+
}
147+
disc.statusMutex.Lock()
148+
delete(disc.cachedPorts, msg.Port.Address)
149+
if disc.eventChan != nil {
150+
disc.eventChan <- &Event{"remove", msg.Port}
151+
}
152+
disc.statusMutex.Unlock()
153+
} else {
154+
outChan <- &msg
155+
}
156+
}
157+
}
158+
159+
// IsAlive return true if the discovery process is running and so is able to receive commands
160+
// and produce events.
161+
func (disc *PluggableDiscovery) IsAlive() bool {
162+
disc.statusMutex.Lock()
163+
defer disc.statusMutex.Unlock()
164+
return disc.alive
165+
}
166+
167+
// IsEventMode return true if the discovery is in "events" mode
168+
func (disc *PluggableDiscovery) IsEventMode() bool {
169+
disc.statusMutex.Lock()
170+
defer disc.statusMutex.Unlock()
171+
return disc.eventsMode
172+
}
173+
174+
func (disc *PluggableDiscovery) waitMessage(timeout time.Duration) (*discoveryMessage, error) {
175+
select {
176+
case msg := <-disc.incomingMessagesChan:
177+
if msg == nil {
178+
// channel has been closed
179+
disc.statusMutex.Lock()
180+
defer disc.statusMutex.Unlock()
181+
return nil, disc.incomingMessagesError
182+
}
183+
return msg, nil
184+
case <-time.After(timeout):
185+
return nil, errors.New("timeout")
186+
}
187+
}
188+
189+
func (disc *PluggableDiscovery) sendCommand(command string) error {
190+
if n, err := disc.outgoingCommandsPipe.Write([]byte(command)); err != nil {
191+
return err
192+
} else if n < len(command) {
193+
return disc.sendCommand(command[n:])
194+
} else {
195+
return nil
196+
}
197+
}
198+
199+
// Start initializes and start the discovery internal subroutines. This command must be
200+
// called before List or StartSync.
201+
func (disc *PluggableDiscovery) Start() error {
202+
if err := disc.sendCommand("START\n"); err != nil {
203+
return err
204+
}
205+
if msg, err := disc.waitMessage(time.Second * 10); err != nil {
206+
return err
207+
} else if msg.EventType != "start" {
208+
return errors.Errorf("communication out of sync, expected 'start', received '%s'", msg.EventType)
209+
} else if msg.Message != "OK" {
210+
return errors.Errorf("command failed: %s", msg.Message)
211+
}
212+
return nil
213+
}
214+
215+
// Stop stops the discovery internal subroutines and possibly free the internally
216+
// used resources. This command should be called if the client wants to pause the
217+
// discovery for a while.
218+
func (disc *PluggableDiscovery) Stop() error {
219+
if err := disc.sendCommand("STOP\n"); err != nil {
220+
return err
221+
}
222+
if msg, err := disc.waitMessage(time.Second * 10); err != nil {
223+
return err
224+
} else if msg.EventType != "stop" {
225+
return errors.Errorf("communication out of sync, expected 'stop', received '%s'", msg.EventType)
226+
} else if msg.Message != "OK" {
227+
return errors.Errorf("command failed: %s", msg.Message)
228+
}
229+
return nil
230+
}
231+
232+
// Quit terminates the discovery. No more commands can be accepted by the discovery.
233+
func (disc *PluggableDiscovery) Quit() error {
234+
if err := disc.sendCommand("QUIT\n"); err != nil {
235+
return err
236+
}
237+
if msg, err := disc.waitMessage(time.Second * 10); err != nil {
238+
return err
239+
} else if msg.EventType != "quit" {
240+
return errors.Errorf("communication out of sync, expected 'quit', received '%s'", msg.EventType)
241+
} else if msg.Message != "OK" {
242+
return errors.Errorf("command failed: %s", msg.Message)
243+
}
244+
return nil
245+
}
246+
247+
// List executes an enumeration of the ports and returns a list of the available
248+
// ports at the moment of the call.
249+
func (disc *PluggableDiscovery) List() ([]*Port, error) {
250+
if err := disc.sendCommand("LIST\n"); err != nil {
251+
return nil, err
252+
}
253+
if msg, err := disc.waitMessage(time.Second * 10); err != nil {
254+
return nil, err
255+
} else if msg.EventType != "list" {
256+
return nil, errors.Errorf("communication out of sync, expected 'list', received '%s'", msg.EventType)
257+
} else {
258+
return msg.Ports, nil
259+
}
260+
}
261+
262+
// EventChannel creates a channel used to receive events from the pluggable discovery.
263+
// The event channel must be consumed as quickly as possible since it may block the
264+
// discovery if it becomes full. The channel size is configurable.
265+
func (disc *PluggableDiscovery) EventChannel(size int) <-chan *Event {
266+
c := make(chan *Event, size)
267+
disc.statusMutex.Lock()
268+
defer disc.statusMutex.Unlock()
269+
disc.eventChan = c
270+
return c
271+
}
272+
273+
// StartSync puts the discovery in "events" mode: the discovery will send "add"
274+
// and "remove" events each time a new port is detected or removed respectively.
275+
// After calling StartSync an initial burst of "add" events may be generated to
276+
// report all the ports available at the moment of the start.
277+
func (disc *PluggableDiscovery) StartSync() error {
278+
disc.statusMutex.Lock()
279+
defer disc.statusMutex.Unlock()
280+
281+
if disc.eventsMode {
282+
return errors.New("already in events mode")
283+
}
284+
if err := disc.sendCommand("START_SYNC\n"); err != nil {
285+
return err
286+
}
287+
288+
// START_SYNC does not give any response
289+
290+
disc.eventsMode = true
291+
disc.cachedPorts = map[string]*Port{}
292+
return nil
293+
}
294+
295+
// ListSync returns a list of the available ports. The list is a cache of all the
296+
// add/remove events happened from the StartSync call and it will not consume any
297+
// resource from the underliying discovery.
298+
func (disc *PluggableDiscovery) ListSync() []*Port {
299+
disc.statusMutex.Lock()
300+
defer disc.statusMutex.Unlock()
301+
res := []*Port{}
302+
for _, port := range disc.cachedPorts {
303+
res = append(res, port)
304+
}
305+
return res
306+
}

Diff for: arduino/discovery/discovery_client/go.mod

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module github.com/arduino/arduino-cli/arduino/discovery/discovery_client
2+
3+
go 1.14
4+
5+
replace github.com/arduino/arduino-cli => ../../..
6+
7+
require (
8+
github.com/arduino/arduino-cli v0.0.0-00010101000000-000000000000
9+
github.com/gizak/termui/v3 v3.1.0
10+
)

0 commit comments

Comments
 (0)