Skip to content

Clean up plugglable discovery and refactor serial-discovery usage #327

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 8, 2019
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 0 additions & 171 deletions arduino/discovery/discovery.go

This file was deleted.

3 changes: 1 addition & 2 deletions cli/board/list.go
Original file line number Diff line number Diff line change
@@ -18,7 +18,6 @@
package board

import (
"context"
"fmt"
"os"
"sort"
@@ -61,7 +60,7 @@ func runListCommand(cmd *cobra.Command, args []string) {
time.Sleep(timeout)
}

resp, err := board.List(context.Background(), &rpc.BoardListReq{Instance: instance.CreateInstance()})
resp, err := board.List(instance.CreateInstance().GetId())
if err != nil {
formatter.PrintError(err, "Error detecting boards")
os.Exit(errorcodes.ErrNetwork)
2 changes: 1 addition & 1 deletion commands/board/attach.go
Original file line number Diff line number Diff line change
@@ -37,7 +37,7 @@ import (
// Attach FIXMEDOC
func Attach(ctx context.Context, req *rpc.BoardAttachReq, taskCB commands.TaskProgressCB) (*rpc.BoardAttachResp, error) {

pm := commands.GetPackageManager(req)
pm := commands.GetPackageManager(req.GetInstance().GetId())
if pm == nil {
return nil, errors.New("invalid instance")
}
2 changes: 1 addition & 1 deletion commands/board/details.go
Original file line number Diff line number Diff line change
@@ -29,7 +29,7 @@ import (

// Details FIXMEDOC
func Details(ctx context.Context, req *rpc.BoardDetailsReq) (*rpc.BoardDetailsResp, error) {
pm := commands.GetPackageManager(req)
pm := commands.GetPackageManager(req.GetInstance().GetId())
if pm == nil {
return nil, errors.New("invalid instance")
}
58 changes: 32 additions & 26 deletions commands/board/list.go
Original file line number Diff line number Diff line change
@@ -18,44 +18,50 @@
package board

import (
"context"
"errors"
"fmt"

"github.com/arduino/arduino-cli/commands"
rpc "github.com/arduino/arduino-cli/rpc/commands"
"github.com/pkg/errors"
)

// List FIXMEDOC
func List(ctx context.Context, req *rpc.BoardListReq) (*rpc.BoardListResp, error) {
pm := commands.GetPackageManager(req)
func List(instanceID int32) (*rpc.BoardListResp, error) {
pm := commands.GetPackageManager(instanceID)
if pm == nil {
return nil, errors.New("invalid instance")
}

serialDiscovery, err := commands.NewBuiltinSerialDiscovery(pm)
if err != nil {
return nil, errors.Wrap(err, "unable to instance serial-discovery")
}

if err := serialDiscovery.Start(); err != nil {
return nil, errors.Wrap(err, "unable to start serial-discovery")
}
defer serialDiscovery.Close()

resp := &rpc.BoardListResp{Ports: []*rpc.DetectedPort{}}
for _, disc := range commands.GetDiscoveries(req) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of going through a dynamic loading mechanism that will always return just the serial discovery, we use it directly

ports, err := disc.List()
if err != nil {
fmt.Printf("Error getting port list from discovery %s: %s\n", disc.ID, err)
continue

ports, err := serialDiscovery.List()
if err != nil {
return nil, errors.Wrap(err, "error getting port list from serial-discovery")
}

for _, port := range ports {
b := []*rpc.BoardListItem{}
for _, board := range pm.IdentifyBoard(port.IdentificationPrefs) {
b = append(b, &rpc.BoardListItem{
Name: board.Name(),
FQBN: board.FQBN(),
})
}
for _, port := range ports {
b := []*rpc.BoardListItem{}
for _, board := range pm.IdentifyBoard(port.IdentificationPrefs) {
b = append(b, &rpc.BoardListItem{
Name: board.Name(),
FQBN: board.FQBN(),
})
}
p := &rpc.DetectedPort{
Address: port.Address,
Protocol: port.Protocol,
ProtocolLabel: port.ProtocolLabel,
Boards: b,
}
resp.Ports = append(resp.Ports, p)
p := &rpc.DetectedPort{
Address: port.Address,
Protocol: port.Protocol,
ProtocolLabel: port.ProtocolLabel,
Boards: b,
}
resp.Ports = append(resp.Ports, p)
}

return resp, nil
2 changes: 1 addition & 1 deletion commands/board/listall.go
Original file line number Diff line number Diff line change
@@ -28,7 +28,7 @@ import (

// ListAll FIXMEDOC
func ListAll(ctx context.Context, req *rpc.BoardListAllReq) (*rpc.BoardListAllResp, error) {
pm := commands.GetPackageManager(req)
pm := commands.GetPackageManager(req.GetInstance().GetId())
if pm == nil {
return nil, errors.New("invalid instance")
}
167 changes: 142 additions & 25 deletions commands/bundled_tools_serial_discovery.go
Original file line number Diff line number Diff line change
@@ -18,27 +18,31 @@
package commands

import (
"encoding/json"
"fmt"
"io"
"os/exec"
"strings"
"sync"
"time"

"github.com/arduino/arduino-cli/arduino/cores"
"github.com/arduino/arduino-cli/arduino/cores/packagemanager"
"github.com/arduino/arduino-cli/arduino/discovery"
"github.com/arduino/arduino-cli/arduino/resources"
"github.com/arduino/arduino-cli/executils"
"github.com/arduino/go-properties-orderedmap"
"github.com/pkg/errors"
semver "go.bug.st/relaxed-semver"
)

var serialDiscoveryVersion = semver.ParseRelaxed("0.5.0")

func loadBuiltinSerialDiscoveryMetadata(pm *packagemanager.PackageManager) {
builtinPackage := pm.Packages.GetOrCreatePackage("builtin")
ctagsTool := builtinPackage.GetOrCreateTool("serial-discovery")
ctagsRel := ctagsTool.GetOrCreateRelease(serialDiscoveryVersion)
ctagsRel.Flavors = []*cores.Flavor{
var (
sdVersion = semver.ParseRelaxed("1.0.0")
flavors = []*cores.Flavor{
{
OS: "i686-pc-linux-gnu",
Resource: &resources.DownloadResource{
ArchiveFileName: "serial-discovery-linux32-v1.0.0.tar.bz2",
URL: "https://downloads.arduino.cc/tools/serial-discovery-linux32-v1.0.0.tar.bz2",
ArchiveFileName: fmt.Sprintf("serial-discovery-linux32-v%s.tar.bz2", sdVersion),
URL: fmt.Sprintf("https://downloads.arduino.cc/tools/serial-discovery-linux32-v%s.tar.bz2", sdVersion),
Size: 1469113,
Checksum: "SHA-256:35d96977844ad8d5ca9363e1ae5794450e5f7cf3d29ce7fdfe656b59e7fff725",
CachePath: "tools",
@@ -47,8 +51,8 @@ func loadBuiltinSerialDiscoveryMetadata(pm *packagemanager.PackageManager) {
{
OS: "x86_64-pc-linux-gnu",
Resource: &resources.DownloadResource{
ArchiveFileName: "serial-discovery-linux64-v1.0.0.tar.bz2",
URL: "https://downloads.arduino.cc/tools/serial-discovery-linux64-v1.0.0.tar.bz2",
ArchiveFileName: fmt.Sprintf("serial-discovery-linux64-v%s.tar.bz2", sdVersion),
URL: fmt.Sprintf("https://downloads.arduino.cc/tools/serial-discovery-linux64-v%s.tar.bz2", sdVersion),
Size: 1503971,
Checksum: "SHA-256:1a870d4d823ea6ebec403f63b10a1dbc9c623a6efea5cfa9141fa20045b731e2",
CachePath: "tools",
@@ -57,8 +61,8 @@ func loadBuiltinSerialDiscoveryMetadata(pm *packagemanager.PackageManager) {
{
OS: "i686-mingw32",
Resource: &resources.DownloadResource{
ArchiveFileName: "serial-discovery-windows-v1.0.0.zip",
URL: "https://downloads.arduino.cc/tools/serial-discovery-windows-v1.0.0.zip",
ArchiveFileName: fmt.Sprintf("serial-discovery-windows-v%s.zip", sdVersion),
URL: fmt.Sprintf("https://downloads.arduino.cc/tools/serial-discovery-windows-v%s.zip", sdVersion),
Size: 1512379,
Checksum: "SHA-256:b956128ab27a3a883c938d17cad640ba396876472f2ed25d8e661f12f5d0f584",
CachePath: "tools",
@@ -67,8 +71,8 @@ func loadBuiltinSerialDiscoveryMetadata(pm *packagemanager.PackageManager) {
{
OS: "x86_64-apple-darwin",
Resource: &resources.DownloadResource{
ArchiveFileName: "serial-discovery-macosx-v1.0.0.tar.bz2",
URL: "https://downloads.arduino.cc/tools/serial-discovery-macosx-v1.0.0.tar.bz2",
ArchiveFileName: fmt.Sprintf("serial-discovery-macosx-v%s.tar.bz2", sdVersion),
URL: fmt.Sprintf("https://downloads.arduino.cc/tools/serial-discovery-macosx-v%s.tar.bz2", sdVersion),
Size: 746132,
Checksum: "SHA-256:fcff1b972b70a73cd738facc6d99174d8323293b60c12149c8f6f3084fb2170e",
CachePath: "tools",
@@ -77,8 +81,8 @@ func loadBuiltinSerialDiscoveryMetadata(pm *packagemanager.PackageManager) {
{
OS: "arm-linux-gnueabihf",
Resource: &resources.DownloadResource{
ArchiveFileName: "serial-discovery-linuxarm-v1.0.0.tar.bz2",
URL: "https://downloads.arduino.cc/tools/serial-discovery-linuxarm-v1.0.0.tar.bz2",
ArchiveFileName: fmt.Sprintf("serial-discovery-linuxarm-v%s.tar.bz2", sdVersion),
URL: fmt.Sprintf("https://downloads.arduino.cc/tools/serial-discovery-linuxarm-v%s.tar.bz2", sdVersion),
Size: 1395174,
Checksum: "SHA-256:f196765caa62d38475208c27b3b516e61427d5d3a8ddc6e863acb4e4a3984701",
CachePath: "tools",
@@ -87,28 +91,141 @@ func loadBuiltinSerialDiscoveryMetadata(pm *packagemanager.PackageManager) {
{
OS: "arm64-linux-gnueabihf",
Resource: &resources.DownloadResource{
ArchiveFileName: "serial-discovery-linuxarm64-v1.0.0.tar.bz2",
URL: "https://downloads.arduino.cc/tools/serial-discovery-linuxarm64-v1.0.0.tar.bz2",
ArchiveFileName: fmt.Sprintf("serial-discovery-linuxarm64-v%s.tar.bz2", sdVersion),
URL: fmt.Sprintf("https://downloads.arduino.cc/tools/serial-discovery-linuxarm64-v%s.tar.bz2", sdVersion),
Size: 1402706,
Checksum: "SHA-256:c87010ed670254c06ac7abbc4daf7446e4e17f1945a75fc2602dd5930835dd25",
CachePath: "tools",
},
},
}
)

// SerialDiscovery is an instance of a discovery tool
type SerialDiscovery struct {
sync.Mutex
ID string
in io.WriteCloser
out io.ReadCloser
outJSON *json.Decoder
cmd *exec.Cmd
}

func getBuiltinSerialDiscoveryTool(pm *packagemanager.PackageManager) (*cores.ToolRelease, error) {
loadBuiltinSerialDiscoveryMetadata(pm)
return pm.Package("builtin").Tool("serial-discovery").Release(serialDiscoveryVersion).Get()
// BoardPort is a generic port descriptor
type BoardPort struct {
Address string `json:"address"`
Label string `json:"label"`
Prefs *properties.Map `json:"prefs"`
IdentificationPrefs *properties.Map `json:"identificationPrefs"`
Protocol string `json:"protocol"`
ProtocolLabel string `json:"protocolLabel"`
}

type eventJSON struct {
EventType string `json:"eventType,required"`
Ports []*BoardPort `json:"ports"`
}

func newBuiltinSerialDiscovery(pm *packagemanager.PackageManager) (*discovery.Discovery, error) {
// NewBuiltinSerialDiscovery returns a wrapper to control the serial-discovery program
func NewBuiltinSerialDiscovery(pm *packagemanager.PackageManager) (*SerialDiscovery, error) {
t, err := getBuiltinSerialDiscoveryTool(pm)
if err != nil {
return nil, err
}

if !t.IsInstalled() {
return nil, fmt.Errorf("missing serial-discovery tool")
}
return discovery.NewFromCommandLine(t.InstallDir.Join("serial-discovery").String())

cmdArgs := []string{
t.InstallDir.Join("serial-discovery").String(),
}

cmd, err := executils.Command(cmdArgs)
if err != nil {
return nil, errors.Wrap(err, "creating discovery process")
}

return &SerialDiscovery{
ID: strings.Join(cmdArgs, " "),
cmd: cmd,
}, nil
}

// Start starts the specified discovery
func (d *SerialDiscovery) Start() error {
if in, err := d.cmd.StdinPipe(); err == nil {
d.in = in
} else {
return fmt.Errorf("creating stdin pipe for discovery: %s", err)
}

if out, err := d.cmd.StdoutPipe(); err == nil {
d.out = out
d.outJSON = json.NewDecoder(d.out)
} else {
return fmt.Errorf("creating stdout pipe for discovery: %s", err)
}

if err := d.cmd.Start(); err != nil {
return fmt.Errorf("starting discovery process: %s", err)
}

return nil
}

// List retrieve the port list from this discovery
func (d *SerialDiscovery) List() ([]*BoardPort, error) {
// ensure the connection to the discoverer is unique to avoid messing up
// the messages exchanged
d.Lock()
defer d.Unlock()

if d.cmd.Process == nil {
return nil, fmt.Errorf("discovery hasn't started")
}

if _, err := d.in.Write([]byte("LIST\n")); err != nil {
return nil, fmt.Errorf("sending LIST command to discovery: %s", err)
}
var event eventJSON
done := make(chan bool)
timeout := false
go func() {
select {
case <-done:
case <-time.After(2000 * time.Millisecond):
timeout = true
d.Close()
}
}()
if err := d.outJSON.Decode(&event); err != nil {
if timeout {
return nil, fmt.Errorf("decoding LIST command: timeout")
}
return nil, fmt.Errorf("decoding LIST command: %s", err)
}
done <- true
return event.Ports, nil
}

// Close stops the Discovery and free the resources
func (d *SerialDiscovery) Close() error {
_, _ = d.in.Write([]byte("QUIT\n"))
_ = d.in.Close()
_ = d.out.Close()
timer := time.AfterFunc(time.Second, func() {
_ = d.cmd.Process.Kill()
})
err := d.cmd.Wait()
_ = timer.Stop()
return err
}

func getBuiltinSerialDiscoveryTool(pm *packagemanager.PackageManager) (*cores.ToolRelease, error) {
builtinPackage := pm.Packages.GetOrCreatePackage("builtin")
ctagsTool := builtinPackage.GetOrCreateTool("serial-discovery")
ctagsRel := ctagsTool.GetOrCreateRelease(sdVersion)
ctagsRel.Flavors = flavors
return pm.Package("builtin").Tool("serial-discovery").Release(sdVersion).Get()
}
2 changes: 1 addition & 1 deletion commands/compile/compile.go
Original file line number Diff line number Diff line change
@@ -42,7 +42,7 @@ import (

// Compile FIXMEDOC
func Compile(ctx context.Context, req *rpc.CompileReq, outStream, errStream io.Writer, config *configs.Configuration, debug bool) (*rpc.CompileResp, error) {
pm := commands.GetPackageManager(req)
pm := commands.GetPackageManager(req.GetInstance().GetId())
if pm == nil {
return nil, errors.New("invalid instance")
}
2 changes: 1 addition & 1 deletion commands/core/download.go
Original file line number Diff line number Diff line change
@@ -32,7 +32,7 @@ import (
// PlatformDownload FIXMEDOC
func PlatformDownload(ctx context.Context, req *rpc.PlatformDownloadReq, downloadCB commands.DownloadProgressCB,
downloaderHeaders http.Header) (*rpc.PlatformDownloadResp, error) {
pm := commands.GetPackageManager(req)
pm := commands.GetPackageManager(req.GetInstance().GetId())
if pm == nil {
return nil, errors.New("invalid instance")
}
2 changes: 1 addition & 1 deletion commands/core/install.go
Original file line number Diff line number Diff line change
@@ -33,7 +33,7 @@ import (
func PlatformInstall(ctx context.Context, req *rpc.PlatformInstallReq,
downloadCB commands.DownloadProgressCB, taskCB commands.TaskProgressCB, downloaderHeaders http.Header) (*rpc.PlatformInstallResp, error) {

pm := commands.GetPackageManager(req)
pm := commands.GetPackageManager(req.GetInstance().GetId())
if pm == nil {
return nil, errors.New("invalid instance")
}
2 changes: 1 addition & 1 deletion commands/core/search.go
Original file line number Diff line number Diff line change
@@ -30,7 +30,7 @@ import (

// PlatformSearch FIXMEDOC
func PlatformSearch(ctx context.Context, req *rpc.PlatformSearchReq) (*rpc.PlatformSearchResp, error) {
pm := commands.GetPackageManager(req)
pm := commands.GetPackageManager(req.GetInstance().GetId())
if pm == nil {
return nil, errors.New("invalid instance")
}
2 changes: 1 addition & 1 deletion commands/core/uninstall.go
Original file line number Diff line number Diff line change
@@ -30,7 +30,7 @@ import (

// PlatformUninstall FIXMEDOC
func PlatformUninstall(ctx context.Context, req *rpc.PlatformUninstallReq, taskCB commands.TaskProgressCB) (*rpc.PlatformUninstallResp, error) {
pm := commands.GetPackageManager(req)
pm := commands.GetPackageManager(req.GetInstance().GetId())
if pm == nil {
return nil, errors.New("invalid instance")
}
2 changes: 1 addition & 1 deletion commands/core/upgrade.go
Original file line number Diff line number Diff line change
@@ -38,7 +38,7 @@ var (
func PlatformUpgrade(ctx context.Context, req *rpc.PlatformUpgradeReq,
downloadCB commands.DownloadProgressCB, taskCB commands.TaskProgressCB, downloaderHeaders http.Header) (*rpc.PlatformUpgradeResp, error) {

pm := commands.GetPackageManager(req)
pm := commands.GetPackageManager(req.GetInstance().GetId())
if pm == nil {
return nil, errors.New("invalid instance")
}
2 changes: 1 addition & 1 deletion commands/daemon/daemon.go
Original file line number Diff line number Diff line change
@@ -49,7 +49,7 @@ func (s *ArduinoCoreServerImpl) BoardDetails(ctx context.Context, req *rpc.Board

// BoardList FIXMEDOC
func (s *ArduinoCoreServerImpl) BoardList(ctx context.Context, req *rpc.BoardListReq) (*rpc.BoardListResp, error) {
return board.List(ctx, req)
return board.List(req.GetInstance().GetId())
}

// BoardListAll FIXMEDOC
70 changes: 0 additions & 70 deletions commands/discoveries.go

This file was deleted.

55 changes: 4 additions & 51 deletions commands/instances.go
Original file line number Diff line number Diff line change
@@ -29,7 +29,6 @@ import (
"github.com/arduino/arduino-cli/arduino/cores"
"github.com/arduino/arduino-cli/arduino/cores/packageindex"
"github.com/arduino/arduino-cli/arduino/cores/packagemanager"
"github.com/arduino/arduino-cli/arduino/discovery"
"github.com/arduino/arduino-cli/arduino/libraries"
"github.com/arduino/arduino-cli/arduino/libraries/librariesmanager"
"github.com/arduino/arduino-cli/configs"
@@ -52,7 +51,6 @@ type CoreInstance struct {
PackageManager *packagemanager.PackageManager
lm *librariesmanager.LibrariesManager
getLibOnly bool
discoveries []*discovery.Discovery
}

// InstanceContainer FIXMEDOC
@@ -66,9 +64,10 @@ func GetInstance(id int32) *CoreInstance {
return instances[id]
}

// GetPackageManager FIXMEDOC
func GetPackageManager(req InstanceContainer) *packagemanager.PackageManager {
i, ok := instances[req.GetInstance().GetId()]
// GetPackageManager returns a PackageManager for the given ID, or nil if
// ID doesn't exist
func GetPackageManager(id int32) *packagemanager.PackageManager {
i, ok := instances[id]
if !ok {
return nil
}
@@ -84,15 +83,6 @@ func GetLibraryManager(instanceID int32) *librariesmanager.LibrariesManager {
return i.lm
}

// GetDiscoveries FIXMEDOC
func GetDiscoveries(req InstanceContainer) []*discovery.Discovery {
i, ok := instances[req.GetInstance().GetId()]
if !ok {
return nil
}
return i.discoveries
}

func (instance *CoreInstance) installToolIfMissing(tool *cores.ToolRelease, downloadCB DownloadProgressCB,
taskCB TaskProgressCB, downloaderHeaders http.Header) (bool, error) {
if tool.IsInstalled() {
@@ -133,32 +123,6 @@ func (instance *CoreInstance) checkForBuiltinTools(downloadCB DownloadProgressCB
return nil
}

func (instance *CoreInstance) startDiscoveries() error {
serialDiscovery, err := newBuiltinSerialDiscovery(instance.PackageManager)
if err != nil {
return fmt.Errorf("starting serial discovery: %s", err)
}

discoveriesToStop := instance.discoveries
discoveriesToStart := append(
discovery.ExtractDiscoveriesFromPlatforms(instance.PackageManager),
serialDiscovery,
)

instance.discoveries = []*discovery.Discovery{}
for _, disc := range discoveriesToStart {
sharedDisc, err := StartSharedDiscovery(disc)
if err != nil {
return fmt.Errorf("starting discovery: %s", err)
}
instance.discoveries = append(instance.discoveries, sharedDisc)
}
for _, disc := range discoveriesToStop {
StopSharedDiscovery(disc)
}
return nil
}

// Init FIXMEDOC
func Init(ctx context.Context, req *rpc.InitReq, downloadCB DownloadProgressCB, taskCB TaskProgressCB, downloaderHeaders http.Header) (*rpc.InitResp, error) {
inConfig := req.GetConfiguration()
@@ -201,11 +165,6 @@ func Init(ctx context.Context, req *rpc.InitReq, downloadCB DownloadProgressCB,
return nil, err
}

if err := instance.startDiscoveries(); err != nil {
// TODO: handle discovery errors
fmt.Println(err)
}

return &rpc.InitResp{
Instance: &rpc.Instance{Id: handle},
PlatformsIndexErrors: reqPltIndex,
@@ -220,10 +179,6 @@ func Destroy(ctx context.Context, req *rpc.DestroyReq) (*rpc.DestroyResp, error)
return nil, fmt.Errorf("invalid handle")
}

for _, disc := range GetDiscoveries(req) {
StopSharedDiscovery(disc)
}

delete(instances, id)
return &rpc.DestroyResp{}, nil
}
@@ -313,8 +268,6 @@ func Rescan(instanceID int32) (*rpc.RescanResp, error) {
coreInstance.PackageManager = pm
coreInstance.lm = lm

coreInstance.startDiscoveries()

return &rpc.RescanResp{
PlatformsIndexErrors: reqPltIndex,
LibrariesIndexError: reqLibIndex,
2 changes: 1 addition & 1 deletion commands/upload/upload.go
Original file line number Diff line number Diff line change
@@ -71,7 +71,7 @@ func Upload(ctx context.Context, req *rpc.UploadReq, outStream io.Writer, errStr
return nil, fmt.Errorf("incorrect FQBN: %s", err)
}

pm := commands.GetPackageManager(req)
pm := commands.GetPackageManager(req.GetInstance().GetId())

// Find target board and board properties
_, _, board, boardProperties, _, err := pm.ResolveFQBN(fqbn)