Unverified Commit 9c334ed8 authored by Cristian Maglie's avatar Cristian Maglie Committed by GitHub

Fix gRPC `BoardList*` methods concurrency issues (#1804)

* Improved streaming of pluggable-discoveries events (WIP)

Now the DiscoveryManager is able to start the discoveries and add/remove
them in a thread-safe way. Also the watchers may connect and disconnect
seamlessly at any time, the incoming events from the discovery are
broadcasted correctly to each active watcher.

This refactoring dramatically simplifies the DiscoveryManager design.

* Added discovery id in discovery.Event struct

* Cache active ports and transmit them when a new watcher connects

* Correctly handle discovery cleanup

* Fixed wrong test

* Correctly handle discovery cleanup and re-add

* Added some doc comments in the source code

* Move Unlock under defer

* Factored subrotuine into a function

it will be useful in the next commits.

* Do not cache ports in the DiscoveryClient

there is already a cache in the DiscoveryManager there is no need to
duplicate it.

* Discovery: eventChan must be protected by mutex when doing START_SYNC

otherwise the discovery may send some events before the eventChan is
setup (and those events will be lost)

* Increased error level for logging watchers that lags

* Updated discvoery_client to the latest API

* Report discovery start errors

* Update arduino/discovery/discovery_client/main.go
Co-authored-by: default avatarUmberto Baldi <34278123+umbynos@users.noreply.github.com>
Co-authored-by: default avatarUmberto Baldi <34278123+umbynos@users.noreply.github.com>
parent 312cfdb9
...@@ -329,16 +329,14 @@ func TestPackageManagerClear(t *testing.T) { ...@@ -329,16 +329,14 @@ func TestPackageManagerClear(t *testing.T) {
packageManager := packagemanager.NewPackageManager(customHardware, customHardware, customHardware, customHardware, "test") packageManager := packagemanager.NewPackageManager(customHardware, customHardware, customHardware, customHardware, "test")
packageManager.LoadHardwareFromDirectory(customHardware) packageManager.LoadHardwareFromDirectory(customHardware)
// Creates another PackageManager but don't load the hardware // Check that the hardware is loaded
emptyPackageManager := packagemanager.NewPackageManager(customHardware, customHardware, customHardware, customHardware, "test") require.NotEmpty(t, packageManager.Packages)
// Verifies they're not equal // Clear the package manager
require.NotEqual(t, packageManager, emptyPackageManager)
// Clear the first PackageManager that contains loaded hardware
packageManager.Clear() packageManager.Clear()
// Verifies both PackageManagers are now equal
require.Equal(t, packageManager, emptyPackageManager) // Check that the hardware is cleared
require.Empty(t, packageManager.Packages)
} }
func TestFindToolsRequiredFromPlatformRelease(t *testing.T) { func TestFindToolsRequiredFromPlatformRelease(t *testing.T) {
......
...@@ -57,7 +57,6 @@ type PluggableDiscovery struct { ...@@ -57,7 +57,6 @@ type PluggableDiscovery struct {
incomingMessagesError error incomingMessagesError error
state int state int
eventChan chan<- *Event eventChan chan<- *Event
cachedPorts map[string]*Port
} }
type discoveryMessage struct { type discoveryMessage struct {
...@@ -121,8 +120,9 @@ func (p *Port) String() string { ...@@ -121,8 +120,9 @@ func (p *Port) String() string {
// Event is a pluggable discovery event // Event is a pluggable discovery event
type Event struct { type Event struct {
Type string Type string
Port *Port Port *Port
DiscoveryID string
} }
// New create and connect to the given pluggable discovery // New create and connect to the given pluggable discovery
...@@ -131,7 +131,6 @@ func New(id string, args ...string) *PluggableDiscovery { ...@@ -131,7 +131,6 @@ func New(id string, args ...string) *PluggableDiscovery {
id: id, id: id,
processArgs: args, processArgs: args,
state: Dead, state: Dead,
cachedPorts: map[string]*Port{},
} }
} }
...@@ -176,9 +175,8 @@ func (disc *PluggableDiscovery) jsonDecodeLoop(in io.Reader, outChan chan<- *dis ...@@ -176,9 +175,8 @@ func (disc *PluggableDiscovery) jsonDecodeLoop(in io.Reader, outChan chan<- *dis
return return
} }
disc.statusMutex.Lock() disc.statusMutex.Lock()
disc.cachedPorts[msg.Port.Address+"|"+msg.Port.Protocol] = msg.Port
if disc.eventChan != nil { if disc.eventChan != nil {
disc.eventChan <- &Event{"add", msg.Port} disc.eventChan <- &Event{"add", msg.Port, disc.GetID()}
} }
disc.statusMutex.Unlock() disc.statusMutex.Unlock()
} else if msg.EventType == "remove" { } else if msg.EventType == "remove" {
...@@ -187,9 +185,8 @@ func (disc *PluggableDiscovery) jsonDecodeLoop(in io.Reader, outChan chan<- *dis ...@@ -187,9 +185,8 @@ func (disc *PluggableDiscovery) jsonDecodeLoop(in io.Reader, outChan chan<- *dis
return return
} }
disc.statusMutex.Lock() disc.statusMutex.Lock()
delete(disc.cachedPorts, msg.Port.Address+"|"+msg.Port.Protocol)
if disc.eventChan != nil { if disc.eventChan != nil {
disc.eventChan <- &Event{"remove", msg.Port} disc.eventChan <- &Event{"remove", msg.Port, disc.GetID()}
} }
disc.statusMutex.Unlock() disc.statusMutex.Unlock()
} else { } else {
...@@ -276,10 +273,7 @@ func (disc *PluggableDiscovery) killProcess() error { ...@@ -276,10 +273,7 @@ func (disc *PluggableDiscovery) killProcess() error {
} }
disc.statusMutex.Lock() disc.statusMutex.Lock()
defer disc.statusMutex.Unlock() defer disc.statusMutex.Unlock()
if disc.eventChan != nil { disc.stopSync()
close(disc.eventChan)
disc.eventChan = nil
}
disc.state = Dead disc.state = Dead
logrus.Infof("killed discovery %s process", disc.id) logrus.Infof("killed discovery %s process", disc.id)
return nil return nil
...@@ -366,13 +360,17 @@ func (disc *PluggableDiscovery) Stop() error { ...@@ -366,13 +360,17 @@ func (disc *PluggableDiscovery) Stop() error {
} }
disc.statusMutex.Lock() disc.statusMutex.Lock()
defer disc.statusMutex.Unlock() defer disc.statusMutex.Unlock()
disc.cachedPorts = map[string]*Port{} disc.stopSync()
disc.state = Idling
return nil
}
func (disc *PluggableDiscovery) stopSync() {
if disc.eventChan != nil { if disc.eventChan != nil {
disc.eventChan <- &Event{"stop", nil, disc.GetID()}
close(disc.eventChan) close(disc.eventChan)
disc.eventChan = nil disc.eventChan = nil
} }
disc.state = Idling
return nil
} }
// Quit terminates the discovery. No more commands can be accepted by the discovery. // Quit terminates the discovery. No more commands can be accepted by the discovery.
...@@ -409,6 +407,9 @@ func (disc *PluggableDiscovery) List() ([]*Port, error) { ...@@ -409,6 +407,9 @@ func (disc *PluggableDiscovery) List() ([]*Port, error) {
// The event channel must be consumed as quickly as possible since it may block the // The event channel must be consumed as quickly as possible since it may block the
// discovery if it becomes full. The channel size is configurable. // discovery if it becomes full. The channel size is configurable.
func (disc *PluggableDiscovery) StartSync(size int) (<-chan *Event, error) { func (disc *PluggableDiscovery) StartSync(size int) (<-chan *Event, error) {
disc.statusMutex.Lock()
defer disc.statusMutex.Unlock()
if err := disc.sendCommand("START_SYNC\n"); err != nil { if err := disc.sendCommand("START_SYNC\n"); err != nil {
return nil, err return nil, err
} }
...@@ -423,29 +424,10 @@ func (disc *PluggableDiscovery) StartSync(size int) (<-chan *Event, error) { ...@@ -423,29 +424,10 @@ func (disc *PluggableDiscovery) StartSync(size int) (<-chan *Event, error) {
return nil, errors.Errorf(tr("communication out of sync, expected '%[1]s', received '%[2]s'"), "OK", msg.Message) return nil, errors.Errorf(tr("communication out of sync, expected '%[1]s', received '%[2]s'"), "OK", msg.Message)
} }
disc.statusMutex.Lock()
defer disc.statusMutex.Unlock()
disc.state = Syncing disc.state = Syncing
disc.cachedPorts = map[string]*Port{} // In case there is already an existing event channel in use we close it before creating a new one.
if disc.eventChan != nil { disc.stopSync()
// In case there is already an existing event channel in use we close it
// before creating a new one.
close(disc.eventChan)
}
c := make(chan *Event, size) c := make(chan *Event, size)
disc.eventChan = c disc.eventChan = c
return c, nil return c, nil
} }
// ListCachedPorts returns a list of the available ports. The list is a cache of all the
// add/remove events happened from the StartSync call and it will not consume any
// resource from the underliying discovery.
func (disc *PluggableDiscovery) ListCachedPorts() []*Port {
disc.statusMutex.Lock()
defer disc.statusMutex.Unlock()
res := []*Port{}
for _, port := range disc.cachedPorts {
res = append(res, port)
}
return res
}
...@@ -7,6 +7,7 @@ replace github.com/arduino/arduino-cli => ../../.. ...@@ -7,6 +7,7 @@ replace github.com/arduino/arduino-cli => ../../..
require ( require (
github.com/arduino/arduino-cli v0.0.0-00010101000000-000000000000 github.com/arduino/arduino-cli v0.0.0-00010101000000-000000000000
github.com/gizak/termui/v3 v3.1.0 github.com/gizak/termui/v3 v3.1.0
github.com/sirupsen/logrus v1.4.2
) )
require ( require (
...@@ -20,7 +21,6 @@ require ( ...@@ -20,7 +21,6 @@ require (
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d // indirect github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect
github.com/sirupsen/logrus v1.4.2 // indirect
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125 // indirect golang.org/x/net v0.0.0-20210505024714-0287a6fb4125 // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
golang.org/x/text v0.3.6 // indirect golang.org/x/text v0.3.6 // indirect
......
...@@ -21,36 +21,28 @@ import ( ...@@ -21,36 +21,28 @@ import (
"log" "log"
"os" "os"
"sort" "sort"
"time"
"github.com/arduino/arduino-cli/arduino/discovery" "github.com/arduino/arduino-cli/arduino/discovery"
"github.com/arduino/arduino-cli/arduino/discovery/discoverymanager"
ui "github.com/gizak/termui/v3" ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets" "github.com/gizak/termui/v3/widgets"
"github.com/sirupsen/logrus"
) )
func main() { func main() {
discoveries := []*discovery.PluggableDiscovery{} logrus.SetLevel(logrus.ErrorLevel)
discEvent := make(chan *discovery.Event) dm := discoverymanager.New()
for _, discCmd := range os.Args[1:] { for _, discCmd := range os.Args[1:] {
disc := discovery.New("", discCmd) disc := discovery.New(discCmd, discCmd)
if err := disc.Run(); err != nil { dm.Add(disc)
log.Fatal("Error starting discovery:", err)
}
if err := disc.Start(); err != nil {
log.Fatal("Error starting discovery:", err)
}
eventChan, err := disc.StartSync(10)
if err != nil {
log.Fatal("Error starting discovery:", err)
}
go func() {
for msg := range eventChan {
discEvent <- msg
}
}()
discoveries = append(discoveries, disc)
} }
dm.Start()
activePorts := map[string]*discovery.Port{}
watcher, err := dm.Watch()
if err != nil {
log.Fatalf("failed to start discoveries: %v", err)
}
if err := ui.Init(); err != nil { if err := ui.Init(); err != nil {
log.Fatalf("failed to initialize termui: %v", err) log.Fatalf("failed to initialize termui: %v", err)
} }
...@@ -66,15 +58,20 @@ func main() { ...@@ -66,15 +58,20 @@ func main() {
updateList := func() { updateList := func() {
rows := []string{} rows := []string{}
rows = append(rows, "Available ports list:") rows = append(rows, "Available ports list:")
for _, disc := range discoveries {
for i, port := range disc.ListCachedPorts() { ids := sort.StringSlice{}
rows = append(rows, fmt.Sprintf(" [%04d] Address: %s", i, port.AddressLabel)) for id := range activePorts {
rows = append(rows, fmt.Sprintf(" Protocol: %s", port.ProtocolLabel)) ids = append(ids, id)
keys := port.Properties.Keys() }
sort.Strings(keys) ids.Sort()
for _, k := range keys { for _, id := range ids {
rows = append(rows, fmt.Sprintf(" %s=%s", k, port.Properties.Get(k))) port := activePorts[id]
} rows = append(rows, fmt.Sprintf("> Address: %s", port.AddressLabel))
rows = append(rows, fmt.Sprintf(" Protocol: %s", port.ProtocolLabel))
keys := port.Properties.Keys()
sort.Strings(keys)
for _, k := range keys {
rows = append(rows, fmt.Sprintf(" %s=%s", k, port.Properties.Get(k)))
} }
} }
l.Rows = rows l.Rows = rows
...@@ -123,20 +120,16 @@ out: ...@@ -123,20 +120,16 @@ out:
previousKey = e.ID previousKey = e.ID
} }
case <-discEvent: case ev := <-watcher.Feed():
if ev.Type == "add" {
activePorts[ev.Port.Address+"|"+ev.Port.Protocol] = ev.Port
}
if ev.Type == "remove" {
delete(activePorts, ev.Port.Address+"|"+ev.Port.Protocol)
}
updateList() updateList()
} }
ui.Render(l) ui.Render(l)
} }
for _, disc := range discoveries {
disc.Quit()
fmt.Println("Discovery QUITed")
for disc.State() == discovery.Alive {
time.Sleep(time.Millisecond)
}
fmt.Println("Discovery correctly terminated")
}
} }
...@@ -18,6 +18,7 @@ package discoverymanager ...@@ -18,6 +18,7 @@ package discoverymanager
import ( import (
"fmt" "fmt"
"sync" "sync"
"time"
"github.com/arduino/arduino-cli/arduino/discovery" "github.com/arduino/arduino-cli/arduino/discovery"
"github.com/arduino/arduino-cli/i18n" "github.com/arduino/arduino-cli/i18n"
...@@ -25,11 +26,20 @@ import ( ...@@ -25,11 +26,20 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
// DiscoveryManager is required to handle multiple pluggable-discovery that // DiscoveryManager manages the many-to-many communication between all pluggable
// may be shared across platforms // discoveries and all watchers. Each PluggableDiscovery, once started, will
// produce a sequence of "events". These events will be broadcasted to all
// listening Watcher.
// The DiscoveryManager will not start the discoveries until the Start method
// is called.
type DiscoveryManager struct { type DiscoveryManager struct {
discoveriesMutex sync.Mutex discoveriesMutex sync.Mutex
discoveries map[string]*discovery.PluggableDiscovery discoveries map[string]*discovery.PluggableDiscovery // all registered PluggableDiscovery
discoveriesRunning bool // set to true once discoveries are started
feed chan *discovery.Event // all events will pass through this channel
watchersMutex sync.Mutex
watchers map[*PortWatcher]bool // all registered Watcher
watchersCache map[string]map[string]*discovery.Event // this is a cache of all active ports
} }
var tr = i18n.Tr var tr = i18n.Tr
...@@ -37,15 +47,24 @@ var tr = i18n.Tr ...@@ -37,15 +47,24 @@ var tr = i18n.Tr
// New creates a new DiscoveryManager // New creates a new DiscoveryManager
func New() *DiscoveryManager { func New() *DiscoveryManager {
return &DiscoveryManager{ return &DiscoveryManager{
discoveries: map[string]*discovery.PluggableDiscovery{}, discoveries: map[string]*discovery.PluggableDiscovery{},
watchers: map[*PortWatcher]bool{},
feed: make(chan *discovery.Event, 50),
watchersCache: map[string]map[string]*discovery.Event{},
} }
} }
// Clear resets the DiscoveryManager to its initial state // Clear resets the DiscoveryManager to its initial state
func (dm *DiscoveryManager) Clear() { func (dm *DiscoveryManager) Clear() {
dm.QuitAll()
dm.discoveriesMutex.Lock() dm.discoveriesMutex.Lock()
defer dm.discoveriesMutex.Unlock() defer dm.discoveriesMutex.Unlock()
if dm.discoveriesRunning {
for _, d := range dm.discoveries {
d.Quit()
logrus.Infof("Closed and removed discovery %s", d.GetID())
}
}
dm.discoveries = map[string]*discovery.PluggableDiscovery{} dm.discoveries = map[string]*discovery.PluggableDiscovery{}
} }
...@@ -60,234 +79,200 @@ func (dm *DiscoveryManager) IDs() []string { ...@@ -60,234 +79,200 @@ func (dm *DiscoveryManager) IDs() []string {
return ids return ids
} }
// Add adds a discovery to the list of managed discoveries // Start starts all the discoveries in this DiscoveryManager.
func (dm *DiscoveryManager) Add(disc *discovery.PluggableDiscovery) error { // If the discoveries are already running, this function does nothing.
id := disc.GetID() func (dm *DiscoveryManager) Start() []error {
dm.discoveriesMutex.Lock() dm.discoveriesMutex.Lock()
defer dm.discoveriesMutex.Unlock() defer dm.discoveriesMutex.Unlock()
if _, has := dm.discoveries[id]; has { if dm.discoveriesRunning {
return errors.Errorf(tr("pluggable discovery already added: %s"), id) return nil
} }
dm.discoveries[id] = disc
return nil
}
// remove quits and deletes the discovery with specified id go func() {
// from the discoveries managed by this DiscoveryManager // Send all events coming from the feed channel to all active watchers
func (dm *DiscoveryManager) remove(id string) { for ev := range dm.feed {
dm.discoveriesMutex.Lock() dm.feedEvent(ev)
d := dm.discoveries[id] }
delete(dm.discoveries, id) }()
dm.discoveriesMutex.Unlock()
d.Quit() errs := []error{}
logrus.Infof("Closed and removed discovery %s", id) var errsLock sync.Mutex
}
// parallelize runs function f concurrently for each discovery.
// Returns a list of errors returned by each call of f.
func (dm *DiscoveryManager) parallelize(f func(d *discovery.PluggableDiscovery) error) []error {
var wg sync.WaitGroup var wg sync.WaitGroup
errChan := make(chan error)
dm.discoveriesMutex.Lock()
discoveries := []*discovery.PluggableDiscovery{}
for _, d := range dm.discoveries { for _, d := range dm.discoveries {
discoveries = append(discoveries, d)
}
dm.discoveriesMutex.Unlock()
for _, d := range discoveries {
wg.Add(1) wg.Add(1)
go func(d *discovery.PluggableDiscovery) { go func(d *discovery.PluggableDiscovery) {
defer wg.Done() if err := dm.startDiscovery(d); err != nil {
if err := f(d); err != nil { errsLock.Lock()
errChan <- err errs = append(errs, err)
errsLock.Unlock()
} }
wg.Done()
}(d) }(d)
} }
wg.Wait()
dm.discoveriesRunning = true
// Wait in a goroutine to collect eventual errors running a discovery.
// When all goroutines that are calling discoveries are done close the errors chan.
go func() {
wg.Wait()
close(errChan)
}()
errs := []error{}
for err := range errChan {
errs = append(errs, err)
}
return errs return errs
} }
// RunAll the discoveries for this DiscoveryManager, // Add adds a discovery to the list of managed discoveries
// returns an error for each discovery failing to run func (dm *DiscoveryManager) Add(d *discovery.PluggableDiscovery) error {
func (dm *DiscoveryManager) RunAll() []error { dm.discoveriesMutex.Lock()
return dm.parallelize(func(d *discovery.PluggableDiscovery) error { defer dm.discoveriesMutex.Unlock()
if d.State() != discovery.Dead {
// This discovery is already alive, nothing to do
return nil
}
if err := d.Run(); err != nil { id := d.GetID()
dm.remove(d.GetID()) if _, has := dm.discoveries[id]; has {
return fmt.Errorf(tr("discovery %[1]s process not started: %[2]w"), d.GetID(), err) return errors.Errorf(tr("pluggable discovery already added: %s"), id)
} }
return nil dm.discoveries[id] = d
})
if dm.discoveriesRunning {
dm.startDiscovery(d)
}
return nil
} }
// StartAll the discoveries for this DiscoveryManager, // PortWatcher is a watcher for all discovery events (port connection/disconnection)
// returns an error for each discovery failing to start type PortWatcher struct {
func (dm *DiscoveryManager) StartAll() []error { closeCB func()
return dm.parallelize(func(d *discovery.PluggableDiscovery) error { feed chan *discovery.Event
state := d.State()
if state != discovery.Idling {
// Already started
return nil
}
if err := d.Start(); err != nil {
dm.remove(d.GetID())
return fmt.Errorf(tr("starting discovery %[1]s: %[2]w"), d.GetID(), err)
}
return nil
})
} }
// StartSyncAll the discoveries for this DiscoveryManager, // Feed returns the feed of events coming from the discoveries
// returns an error for each discovery failing to start syncing func (pw *PortWatcher) Feed() <-chan *discovery.Event {
func (dm *DiscoveryManager) StartSyncAll() (<-chan *discovery.Event, []error) { return pw.feed
eventSink := make(chan *discovery.Event, 5) }
var wg sync.WaitGroup
errs := dm.parallelize(func(d *discovery.PluggableDiscovery) error {
state := d.State()
if state != discovery.Idling || state == discovery.Syncing {
// Already syncing
return nil
}
eventCh, err := d.StartSync(5) // Close closes the PortWatcher
if err != nil { func (pw *PortWatcher) Close() {
dm.remove(d.GetID()) pw.closeCB()
return fmt.Errorf(tr("start syncing discovery %[1]s: %[2]w"), d.GetID(), err) }
}
wg.Add(1) // Watch starts a watcher for all discovery events (port connection/disconnection).
go func() { // The watcher must be closed when it is no longer needed with the Close method.
for ev := range eventCh { func (dm *DiscoveryManager) Watch() (*PortWatcher, error) {
eventSink <- ev dm.Start()
}
wg.Done() watcher := &PortWatcher{
}() feed: make(chan *discovery.Event, 10),
return nil }
}) watcher.closeCB = func() {
dm.watchersMutex.Lock()
defer dm.watchersMutex.Unlock()
delete(dm.watchers, watcher)
close(watcher.feed)
}
go func() { go func() {
wg.Wait() dm.watchersMutex.Lock()
eventSink <- &discovery.Event{Type: "quit"} // When a watcher is started, send all the current active ports first...
close(eventSink) for _, cache := range dm.watchersCache {
for _, ev := range cache {
watcher.feed <- ev
}
}
// ...and after that add the watcher to the list of watchers receiving events
dm.watchers[watcher] = true
dm.watchersMutex.Unlock()
}() }()
return eventSink, errs return watcher, nil
} }
// StopAll the discoveries for this DiscoveryManager, func (dm *DiscoveryManager) startDiscovery(d *discovery.PluggableDiscovery) (discErr error) {
// returns an error for each discovery failing to stop defer func() {
func (dm *DiscoveryManager) StopAll() []error { // If this function returns an error log it
return dm.parallelize(func(d *discovery.PluggableDiscovery) error { if discErr != nil {
state := d.State() logrus.Errorf("Discovery %s failed to run: %s", d.GetID(), discErr)
if state != discovery.Syncing && state != discovery.Running {
// Not running nor syncing, nothing to stop
return nil
} }
}()
if err := d.Stop(); err != nil { if err := d.Run(); err != nil {
dm.remove(d.GetID()) return fmt.Errorf(tr("discovery %[1]s process not started: %[2]w"), d.GetID(), err)
return fmt.Errorf(tr("stopping discovery %[1]s: %[2]w"), d.GetID(), err) }
} eventCh, err := d.StartSync(5)
return nil if err != nil {
}) return fmt.Errorf("%s: %s", tr("starting discovery %s", d.GetID()), err)
} }
// QuitAll quits all the discoveries managed by this DiscoveryManager. go func() {
// Returns an error for each discovery that fails quitting // Transfer all incoming events from this discovery to the feed channel
func (dm *DiscoveryManager) QuitAll() []error { for ev := range eventCh {
errs := dm.parallelize(func(d *discovery.PluggableDiscovery) error { dm.feed <- ev
if d.State() == discovery.Dead {
// Stop! Stop! It's already dead!
return nil
} }
}()
d.Quit() return nil
return nil
})
return errs
} }
// List returns a list of available ports detected from all discoveries func (dm *DiscoveryManager) feedEvent(ev *discovery.Event) {
// and a list of errors for those discoveries that returned one. dm.watchersMutex.Lock()
func (dm *DiscoveryManager) List() ([]*discovery.Port, []error) { defer dm.watchersMutex.Unlock()
var wg sync.WaitGroup
// Use this struct to avoid the need of two separate sendToAllWatchers := func(ev *discovery.Event) {
// channels for ports and errors. // Send the event to all watchers
type listMsg struct { for watcher := range dm.watchers {
Err error select {
Port *discovery.Port case watcher.feed <- ev:
} // OK
msgChan := make(chan listMsg) case <-time.After(time.Millisecond * 500):
dm.discoveriesMutex.Lock() // If the watcher is not able to process event fast enough
discoveries := []*discovery.PluggableDiscovery{} // remove the watcher from the list of watchers
for _, d := range dm.discoveries { logrus.Error("Watcher is not able to process events fast enough, removing it from the list of watchers")
discoveries = append(discoveries, d) delete(dm.watchers, watcher)
}
dm.discoveriesMutex.Unlock()
for _, d := range discoveries {
wg.Add(1)
go func(d *discovery.PluggableDiscovery) {
defer wg.Done()
if d.State() != discovery.Running {
// Discovery is not running, it won't return anything
return
}
ports, err := d.List()
if err != nil {
msgChan <- listMsg{Err: fmt.Errorf(tr("listing ports from discovery %[1]s: %[2]w"), d.GetID(), err)}
} }
for _, p := range ports { }
msgChan <- listMsg{Port: p} }
if ev.Type == "stop" {
// Send remove events for all the cached ports of the terminating discovery
cache := dm.watchersCache[ev.DiscoveryID]
for _, addEv := range cache {
removeEv := &discovery.Event{
Type: "remove",
Port: &discovery.Port{
Address: addEv.Port.Address,
AddressLabel: addEv.Port.AddressLabel,
Protocol: addEv.Port.Protocol,
ProtocolLabel: addEv.Port.ProtocolLabel},
DiscoveryID: addEv.DiscoveryID,
} }
}(d) sendToAllWatchers(removeEv)
}
// Remove the cache for the terminating discovery
delete(dm.watchersCache, ev.DiscoveryID)
return
} }
go func() { sendToAllWatchers(ev)
// Close the channel only after all goroutines are done
wg.Wait()
close(msgChan)
}()
ports := []*discovery.Port{} // Cache the event for the discovery
errs := []error{} cache := dm.watchersCache[ev.DiscoveryID]
for msg := range msgChan { if cache == nil {
if msg.Err != nil { cache = map[string]*discovery.Event{}
errs = append(errs, msg.Err) dm.watchersCache[ev.DiscoveryID] = cache
} else { }
ports = append(ports, msg.Port) eventID := ev.Port.Address + "|" + ev.Port.Protocol
} switch ev.Type {
case "add":
cache[eventID] = ev
case "remove":
delete(cache, eventID)
default:
logrus.Errorf("Unhandled event from discovery: %s", ev.Type)
} }
return ports, errs
} }
// ListCachedPorts return the current list of ports detected from all discoveries // List return the current list of ports detected from all discoveries
func (dm *DiscoveryManager) ListCachedPorts() []*discovery.Port { func (dm *DiscoveryManager) List() []*discovery.Port {
dm.Start()
res := []*discovery.Port{} res := []*discovery.Port{}
dm.discoveriesMutex.Lock() dm.watchersMutex.Lock()
discoveries := []*discovery.PluggableDiscovery{} defer dm.watchersMutex.Unlock()
for _, d := range dm.discoveries { for _, cache := range dm.watchersCache {
discoveries = append(discoveries, d) for _, ev := range cache {
} res = append(res, ev.Port)
dm.discoveriesMutex.Unlock()
for _, d := range discoveries {
if d.State() != discovery.Syncing {
// Discovery is not syncing
continue
} }
res = append(res, d.ListCachedPorts()...)
} }
return res return res
} }
...@@ -178,7 +178,7 @@ func GetInstallableLibs() []string { ...@@ -178,7 +178,7 @@ func GetInstallableLibs() []string {
func GetConnectedBoards() []string { func GetConnectedBoards() []string {
inst := instance.CreateAndInit() inst := instance.CreateAndInit()
list, _ := board.List(&rpc.BoardListRequest{ list, _, _ := board.List(&rpc.BoardListRequest{
Instance: inst, Instance: inst,
}) })
var res []string var res []string
......
...@@ -106,31 +106,16 @@ func (p *Port) GetPort(instance *rpc.Instance, sk *sketch.Sketch) (*discovery.Po ...@@ -106,31 +106,16 @@ func (p *Port) GetPort(instance *rpc.Instance, sk *sketch.Sketch) (*discovery.Po
return nil, errors.New("invalid instance") return nil, errors.New("invalid instance")
} }
dm := pm.DiscoveryManager() dm := pm.DiscoveryManager()
if errs := dm.RunAll(); len(errs) == len(dm.IDs()) { watcher, err := dm.Watch()
// All discoveries failed to run, we can't do anything if err != nil {
return nil, fmt.Errorf("%v", errs) return nil, err
} else if len(errs) > 0 {
// If only some discoveries failed to run just tell the user and go on
for _, err := range errs {
feedback.Error(err)
}
}
eventChan, errs := dm.StartSyncAll()
if len(errs) > 0 {
return nil, fmt.Errorf("%v", errs)
} }
defer watcher.Close()
defer func() {
// Quit all discoveries at the end.
if errs := dm.QuitAll(); len(errs) > 0 {
logrus.Errorf("quitting discoveries when getting port metadata: %v", errs)
}
}()
deadline := time.After(p.timeout.Get()) deadline := time.After(p.timeout.Get())
for { for {
select { select {
case portEvent := <-eventChan: case portEvent := <-watcher.Feed():
if portEvent.Type != "add" { if portEvent.Type != "add" {
continue continue
} }
...@@ -161,7 +146,7 @@ func (p *Port) GetSearchTimeout() time.Duration { ...@@ -161,7 +146,7 @@ func (p *Port) GetSearchTimeout() time.Duration {
// discovered Port object together with the FQBN. If the port does not match // discovered Port object together with the FQBN. If the port does not match
// exactly 1 board, // exactly 1 board,
func (p *Port) DetectFQBN(inst *rpc.Instance) (string, *rpc.Port) { func (p *Port) DetectFQBN(inst *rpc.Instance) (string, *rpc.Port) {
detectedPorts, err := board.List(&rpc.BoardListRequest{ detectedPorts, _, err := board.List(&rpc.BoardListRequest{
Instance: inst, Instance: inst,
Timeout: p.timeout.Get().Milliseconds(), Timeout: p.timeout.Get().Milliseconds(),
}) })
......
...@@ -64,22 +64,26 @@ func runListCommand(cmd *cobra.Command, args []string) { ...@@ -64,22 +64,26 @@ func runListCommand(cmd *cobra.Command, args []string) {
os.Exit(0) os.Exit(0)
} }
ports, err := board.List(&rpc.BoardListRequest{ ports, discvoeryErrors, err := board.List(&rpc.BoardListRequest{
Instance: inst, Instance: inst,
Timeout: timeoutArg.Get().Milliseconds(), Timeout: timeoutArg.Get().Milliseconds(),
}) })
if err != nil { if err != nil {
feedback.Errorf(tr("Error detecting boards: %v"), err) feedback.Errorf(tr("Error detecting boards: %v"), err)
} }
for _, err := range discvoeryErrors {
feedback.Errorf(tr("Error starting discovery: %v"), err)
}
feedback.PrintResult(result{ports}) feedback.PrintResult(result{ports})
} }
func watchList(cmd *cobra.Command, inst *rpc.Instance) { func watchList(cmd *cobra.Command, inst *rpc.Instance) {
eventsChan, err := board.Watch(inst.Id, nil) eventsChan, closeCB, err := board.Watch(inst.Id)
if err != nil { if err != nil {
feedback.Errorf(tr("Error detecting boards: %v"), err) feedback.Errorf(tr("Error detecting boards: %v"), err)
os.Exit(errorcodes.ErrNetwork) os.Exit(errorcodes.ErrNetwork)
} }
defer closeCB()
// This is done to avoid printing the header each time a new event is received // This is done to avoid printing the header each time a new event is received
if feedback.GetFormat() == feedback.Text { if feedback.GetFormat() == feedback.Text {
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package board package board
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
...@@ -176,32 +177,21 @@ func identify(pm *packagemanager.PackageManager, port *discovery.Port) ([]*rpc.B ...@@ -176,32 +177,21 @@ func identify(pm *packagemanager.PackageManager, port *discovery.Port) ([]*rpc.B
// List returns a list of boards found by the loaded discoveries. // List returns a list of boards found by the loaded discoveries.
// In case of errors partial results from discoveries that didn't fail // In case of errors partial results from discoveries that didn't fail
// are returned. // are returned.
func List(req *rpc.BoardListRequest) (r []*rpc.DetectedPort, e error) { func List(req *rpc.BoardListRequest) (r []*rpc.DetectedPort, discoveryStartErrors []error, e error) {
pm := commands.GetPackageManager(req.GetInstance().Id) pm := commands.GetPackageManager(req.GetInstance().Id)
if pm == nil { if pm == nil {
return nil, &arduino.InvalidInstanceError{} return nil, nil, &arduino.InvalidInstanceError{}
} }
dm := pm.DiscoveryManager() dm := pm.DiscoveryManager()
if errs := dm.RunAll(); len(errs) > 0 { discoveryStartErrors = dm.Start()
return nil, &arduino.UnavailableError{Message: tr("Error starting board discoveries"), Cause: fmt.Errorf("%v", errs)}
}
if errs := dm.StartAll(); len(errs) > 0 {
return nil, &arduino.UnavailableError{Message: tr("Error starting board discoveries"), Cause: fmt.Errorf("%v", errs)}
}
defer func() {
if errs := dm.StopAll(); len(errs) > 0 {
logrus.Error(errs)
}
}()
time.Sleep(time.Duration(req.GetTimeout()) * time.Millisecond) time.Sleep(time.Duration(req.GetTimeout()) * time.Millisecond)
retVal := []*rpc.DetectedPort{} retVal := []*rpc.DetectedPort{}
ports, errs := pm.DiscoveryManager().List() for _, port := range dm.List() {
for _, port := range ports {
boards, err := identify(pm, port) boards, err := identify(pm, port)
if err != nil { if err != nil {
return nil, err return nil, discoveryStartErrors, err
} }
// boards slice can be empty at this point if neither the cores nor the // boards slice can be empty at this point if neither the cores nor the
...@@ -212,92 +202,49 @@ func List(req *rpc.BoardListRequest) (r []*rpc.DetectedPort, e error) { ...@@ -212,92 +202,49 @@ func List(req *rpc.BoardListRequest) (r []*rpc.DetectedPort, e error) {
} }
retVal = append(retVal, b) retVal = append(retVal, b)
} }
if len(errs) > 0 { return retVal, discoveryStartErrors, nil
return retVal, &arduino.UnavailableError{Message: tr("Error getting board list"), Cause: fmt.Errorf("%v", errs)}
}
return retVal, nil
} }
// Watch returns a channel that receives boards connection and disconnection events. // Watch returns a channel that receives boards connection and disconnection events.
// The discovery process can be interrupted by sending a message to the interrupt channel. // It also returns a callback function that must be used to stop and dispose the watch.
func Watch(instanceID int32, interrupt <-chan bool) (<-chan *rpc.BoardListWatchResponse, error) { func Watch(instanceID int32) (<-chan *rpc.BoardListWatchResponse, func(), error) {
pm := commands.GetPackageManager(instanceID) pm := commands.GetPackageManager(instanceID)
dm := pm.DiscoveryManager() dm := pm.DiscoveryManager()
runErrs := dm.RunAll() watcher, err := dm.Watch()
if len(runErrs) == len(dm.IDs()) { if err != nil {
// All discoveries failed to run, we can't do anything return nil, nil, err
return nil, &arduino.UnavailableError{Message: tr("Error starting board discoveries"), Cause: fmt.Errorf("%v", runErrs)}
} }
eventsChan, errs := dm.StartSyncAll() ctx, cancel := context.WithCancel(context.Background())
if len(runErrs) > 0 { go func() {
errs = append(runErrs, errs...) <-ctx.Done()
} watcher.Close()
}()
outChan := make(chan *rpc.BoardListWatchResponse) outChan := make(chan *rpc.BoardListWatchResponse)
go func() { go func() {
defer close(outChan) defer close(outChan)
for _, err := range errs { for event := range watcher.Feed() {
outChan <- &rpc.BoardListWatchResponse{ port := &rpc.DetectedPort{
EventType: "error", Port: event.Port.ToRPC(),
Error: err.Error(),
} }
}
for {
select {
case event := <-eventsChan:
if event.Type == "quit" {
// The discovery manager has closed its event channel because it's
// quitting all the discovery processes that are running, this
// means that the events channel we're listening from won't receive any
// more events.
// Handling this case is necessary when the board watcher is running and
// the instance being used is reinitialized since that quits all the
// discovery processes and reset the discovery manager. That would leave
// this goroutine listening forever on a "dead" channel and might even
// cause panics.
// This message avoid all this issues.
// It will be the client's task restarting the board watcher if necessary,
// this host won't attempt restarting it.
outChan <- &rpc.BoardListWatchResponse{
EventType: event.Type,
}
return
}
port := &rpc.DetectedPort{
Port: event.Port.ToRPC(),
}
boardsError := "" boardsError := ""
if event.Type == "add" { if event.Type == "add" {
boards, err := identify(pm, event.Port) boards, err := identify(pm, event.Port)
if err != nil { if err != nil {
boardsError = err.Error() boardsError = err.Error()
}
port.MatchingBoards = boards
}
outChan <- &rpc.BoardListWatchResponse{
EventType: event.Type,
Port: port,
Error: boardsError,
}
case <-interrupt:
for _, err := range dm.StopAll() {
// Discoveries that return errors have their process
// closed and are removed from the list of discoveries
// in the manager
outChan <- &rpc.BoardListWatchResponse{
EventType: "error",
Error: tr("stopping discoveries: %s", err),
}
} }
return port.MatchingBoards = boards
}
outChan <- &rpc.BoardListWatchResponse{
EventType: event.Type,
Port: port,
Error: boardsError,
} }
} }
}() }()
return outChan, nil return outChan, cancel, nil
} }
...@@ -36,9 +36,7 @@ import ( ...@@ -36,9 +36,7 @@ import (
"github.com/arduino/arduino-cli/i18n" "github.com/arduino/arduino-cli/i18n"
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1" rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata" "google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
) )
// ArduinoCoreServerImpl FIXMEDOC // ArduinoCoreServerImpl FIXMEDOC
...@@ -69,7 +67,7 @@ func (s *ArduinoCoreServerImpl) BoardDetails(ctx context.Context, req *rpc.Board ...@@ -69,7 +67,7 @@ func (s *ArduinoCoreServerImpl) BoardDetails(ctx context.Context, req *rpc.Board
// BoardList FIXMEDOC // BoardList FIXMEDOC
func (s *ArduinoCoreServerImpl) BoardList(ctx context.Context, req *rpc.BoardListRequest) (*rpc.BoardListResponse, error) { func (s *ArduinoCoreServerImpl) BoardList(ctx context.Context, req *rpc.BoardListRequest) (*rpc.BoardListResponse, error) {
ports, err := board.List(req) ports, _, err := board.List(req)
if err != nil { if err != nil {
return nil, convertErrorToRPCStatus(err) return nil, convertErrorToRPCStatus(err)
} }
...@@ -109,42 +107,35 @@ func (s *ArduinoCoreServerImpl) BoardListWatch(stream rpc.ArduinoCoreService_Boa ...@@ -109,42 +107,35 @@ func (s *ArduinoCoreServerImpl) BoardListWatch(stream rpc.ArduinoCoreService_Boa
return err return err
} }
interrupt := make(chan bool, 1) eventsChan, closeWatcher, err := board.Watch(msg.Instance.Id)
if err != nil {
return convertErrorToRPCStatus(err)
}
go func() { go func() {
defer close(interrupt) defer closeWatcher()
for { for {
msg, err := stream.Recv() msg, err := stream.Recv()
// Handle client closing the stream and eventual errors // Handle client closing the stream and eventual errors
if err == io.EOF { if err == io.EOF {
logrus.Info("boards watcher stream closed") logrus.Info("boards watcher stream closed")
interrupt <- true
return
} else if st, ok := status.FromError(err); ok && st.Code() == codes.Canceled {
logrus.Info("boards watcher interrupted by host")
return return
} else if err != nil { }
if err != nil {
logrus.Infof("interrupting boards watcher: %v", err) logrus.Infof("interrupting boards watcher: %v", err)
interrupt <- true
return return
} }
// Message received, does the client want to interrupt? // Message received, does the client want to interrupt?
if msg != nil && msg.Interrupt { if msg != nil && msg.Interrupt {
logrus.Info("boards watcher interrupted by client") logrus.Info("boards watcher interrupted by client")
interrupt <- msg.Interrupt
return return
} }
} }
}() }()
eventsChan, err := board.Watch(msg.Instance.Id, interrupt)
if err != nil {
return convertErrorToRPCStatus(err)
}
for event := range eventsChan { for event := range eventsChan {
err = stream.Send(event) if err := stream.Send(event); err != nil {
if err != nil {
logrus.Infof("sending board watch message: %v", err) logrus.Infof("sending board watch message: %v", err)
} }
} }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment