From 173d76493b2f85ae09c2fb629c43152ea102ca06 Mon Sep 17 00:00:00 2001 From: Massimiliano Pippi Date: Thu, 8 Aug 2019 14:44:19 +0200 Subject: [PATCH 1/4] query the backend to get the fqbn --- arduino/cores/packagemanager/identify.go | 2 +- cli/board/list.go | 11 +++---- commands/board/list.go | 41 +++++++++++++++++++++--- commands/daemon/daemon.go | 9 +++++- 4 files changed, 50 insertions(+), 13 deletions(-) diff --git a/arduino/cores/packagemanager/identify.go b/arduino/cores/packagemanager/identify.go index 834cd95d00e..926fa4dd1bf 100644 --- a/arduino/cores/packagemanager/identify.go +++ b/arduino/cores/packagemanager/identify.go @@ -24,7 +24,7 @@ import ( properties "github.com/arduino/go-properties-orderedmap" ) -// IdentifyBoard returns a list of baords matching the provided identification properties. +// IdentifyBoard returns a list of boards matching the provided identification properties. func (pm *PackageManager) IdentifyBoard(idProps *properties.Map) []*cores.Board { if idProps.Size() == 0 { return []*cores.Board{} diff --git a/cli/board/list.go b/cli/board/list.go index fdaf4f3fdb0..63018402a28 100644 --- a/cli/board/list.go +++ b/cli/board/list.go @@ -60,19 +60,18 @@ func runListCommand(cmd *cobra.Command, args []string) { time.Sleep(timeout) } - resp, err := board.List(instance.CreateInstance().GetId()) + ports, err := board.List(instance.CreateInstance().GetId()) if err != nil { formatter.PrintError(err, "Error detecting boards") os.Exit(errorcodes.ErrNetwork) } - if output.JSONOrElse(resp) { - outputListResp(resp) + if output.JSONOrElse(ports) { + outputListResp(ports) } } -func outputListResp(resp *rpc.BoardListResp) { - ports := resp.GetPorts() +func outputListResp(ports []*rpc.DetectedPort) { if len(ports) == 0 { formatter.Print("No boards found.") return @@ -84,7 +83,7 @@ func outputListResp(resp *rpc.BoardListResp) { }) table := output.NewTable() table.SetHeader("Port", "Type", "Board Name", "FQBN") - for _, port := range resp.GetPorts() { + for _, port := range ports { address := port.GetProtocol() + "://" + port.GetAddress() if port.GetProtocol() == "serial" { address = port.GetAddress() diff --git a/commands/board/list.go b/commands/board/list.go index 0f58a6e3852..2f5a154c2ca 100644 --- a/commands/board/list.go +++ b/commands/board/list.go @@ -18,13 +18,18 @@ package board import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "github.com/arduino/arduino-cli/commands" rpc "github.com/arduino/arduino-cli/rpc/commands" "github.com/pkg/errors" ) // List FIXMEDOC -func List(instanceID int32) (*rpc.BoardListResp, error) { +func List(instanceID int32) ([]*rpc.DetectedPort, error) { pm := commands.GetPackageManager(instanceID) if pm == nil { return nil, errors.New("invalid instance") @@ -40,29 +45,55 @@ func List(instanceID int32) (*rpc.BoardListResp, error) { } defer serialDiscovery.Close() - resp := &rpc.BoardListResp{Ports: []*rpc.DetectedPort{}} - ports, err := serialDiscovery.List() if err != nil { return nil, errors.Wrap(err, "error getting port list from serial-discovery") } + retVal := []*rpc.DetectedPort{} for _, port := range ports { b := []*rpc.BoardListItem{} + + // first query installed cores through the Package Manager for _, board := range pm.IdentifyBoard(port.IdentificationPrefs) { b = append(b, &rpc.BoardListItem{ Name: board.Name(), FQBN: board.FQBN(), }) } + + // if installed cores didn't recognize the board, try querying + // the builder API + if len(b) == 0 { + url := fmt.Sprintf("https://builder.arduino.cc/v3/boards/byVidPid/%s/%s", + port.IdentificationPrefs.Get("vid"), + port.IdentificationPrefs.Get("pid")) + req, _ := http.NewRequest("GET", url, nil) + if res, err := http.DefaultClient.Do(req); err == nil { + body, _ := ioutil.ReadAll(res.Body) + res.Body.Close() + + var dat map[string]interface{} + + if err := json.Unmarshal(body, &dat); err == nil { + b = append(b, &rpc.BoardListItem{ + Name: dat["name"].(string), + FQBN: dat["fqbn"].(string), + }) + } + } + } + + // boards slice can be empty at this point if neither the cores nor the + // API managed to recognize the connected board p := &rpc.DetectedPort{ Address: port.Address, Protocol: port.Protocol, ProtocolLabel: port.ProtocolLabel, Boards: b, } - resp.Ports = append(resp.Ports, p) + retVal = append(retVal, p) } - return resp, nil + return retVal, nil } diff --git a/commands/daemon/daemon.go b/commands/daemon/daemon.go index aa5feafc5f0..b77b8f1d5ff 100644 --- a/commands/daemon/daemon.go +++ b/commands/daemon/daemon.go @@ -49,7 +49,14 @@ 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(req.GetInstance().GetId()) + ports, err := board.List(req.GetInstance().GetId()) + if err != nil { + return nil, err + } + + return &rpc.BoardListResp{ + Ports: ports, + }, nil } // BoardListAll FIXMEDOC From fa4d9d4fd5541c5e98fe128bc21b65ae0aa61383 Mon Sep 17 00:00:00 2001 From: Massimiliano Pippi Date: Fri, 9 Aug 2019 12:27:51 +0200 Subject: [PATCH 2/4] add tests --- commands/board/list.go | 49 ++++++++++++++++++++-------- commands/board/list_test.go | 64 +++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 13 deletions(-) create mode 100644 commands/board/list_test.go diff --git a/commands/board/list.go b/commands/board/list.go index 2f5a154c2ca..0a59247b765 100644 --- a/commands/board/list.go +++ b/commands/board/list.go @@ -28,6 +28,37 @@ import ( "github.com/pkg/errors" ) +func apiByVidPid(url string) ([]*rpc.BoardListItem, error) { + retVal := []*rpc.BoardListItem{} + req, _ := http.NewRequest("GET", url, nil) + if res, err := http.DefaultClient.Do(req); err == nil { + body, _ := ioutil.ReadAll(res.Body) + res.Body.Close() + + var dat map[string]interface{} + err = json.Unmarshal(body, &dat) + if err != nil { + return nil, errors.Wrap(err, "error processing response from server") + } + + name, nameFound := dat["name"].(string) + fqbn, fbqnFound := dat["fqbn"].(string) + + if !nameFound || !fbqnFound { + return nil, errors.New("wrong format in server response") + } + + retVal = append(retVal, &rpc.BoardListItem{ + Name: name, + FQBN: fqbn, + }) + } else { + return nil, errors.Wrap(err, "error querying Arduino Cloud Api") + } + + return retVal, nil +} + // List FIXMEDOC func List(instanceID int32) ([]*rpc.DetectedPort, error) { pm := commands.GetPackageManager(instanceID) @@ -68,20 +99,12 @@ func List(instanceID int32) ([]*rpc.DetectedPort, error) { url := fmt.Sprintf("https://builder.arduino.cc/v3/boards/byVidPid/%s/%s", port.IdentificationPrefs.Get("vid"), port.IdentificationPrefs.Get("pid")) - req, _ := http.NewRequest("GET", url, nil) - if res, err := http.DefaultClient.Do(req); err == nil { - body, _ := ioutil.ReadAll(res.Body) - res.Body.Close() - - var dat map[string]interface{} - - if err := json.Unmarshal(body, &dat); err == nil { - b = append(b, &rpc.BoardListItem{ - Name: dat["name"].(string), - FQBN: dat["fqbn"].(string), - }) - } + items, err := apiByVidPid(url) + if err != nil { + return nil, errors.Wrap(err, "error getting bard info from Arduino Cloud") } + + b = items } // boards slice can be empty at this point if neither the cores nor the diff --git a/commands/board/list_test.go b/commands/board/list_test.go new file mode 100644 index 00000000000..5f0e8c41903 --- /dev/null +++ b/commands/board/list_test.go @@ -0,0 +1,64 @@ +// This file is part of arduino-cli. +// +// Copyright 2019 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 board + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGetByVidPid(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, ` +{ + "architecture": "samd", + "fqbn": "arduino:samd:mkr1000", + "href": "/v3/boards/arduino:samd:mkr1000", + "id": "mkr1000", + "name": "Arduino/Genuino MKR1000", + "package": "arduino", + "plan": "create-free" +} + `) + })) + defer ts.Close() + + res, err := apiByVidPid(ts.URL) + require.Nil(t, err) + require.Len(t, res, 1) + require.Equal(t, "Arduino/Genuino MKR1000", res[0].Name) + require.Equal(t, "arduino:samd:mkr1000", res[0].FQBN) + + // wrong url + res, err = apiByVidPid("http://0.0.0.0") + require.NotNil(t, err) +} + +func TestGetByVidPidMalformedResponse(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, "{}") + })) + defer ts.Close() + + res, err := apiByVidPid(ts.URL) + require.NotNil(t, err) + require.Equal(t, "wrong format in server response", err.Error()) + require.Len(t, res, 0) +} From 73232e7e066876628b4a7f23acf433b74c856ca5 Mon Sep 17 00:00:00 2001 From: Massimiliano Pippi Date: Fri, 9 Aug 2019 13:52:52 +0200 Subject: [PATCH 3/4] send headers, betteer error handling --- cli/output/table.go | 4 ++-- cli/output/text.go | 3 +-- commands/board/list.go | 22 +++++++++++++++++++++- commands/board/list_test.go | 25 +++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 5 deletions(-) diff --git a/cli/output/table.go b/cli/output/table.go index 5273576126d..efc9aca92cd 100644 --- a/cli/output/table.go +++ b/cli/output/table.go @@ -71,9 +71,9 @@ func (t *Table) makeTableRow(columns ...interface{}) *TableRow { case TextBox: cells[i] = text case string: - cells[i] = Sprintf("%s", text) + cells[i] = sprintf("%s", text) case fmt.Stringer: - cells[i] = Sprintf("%s", text.String()) + cells[i] = sprintf("%s", text.String()) default: panic(fmt.Sprintf("invalid column argument type: %t", col)) } diff --git a/cli/output/text.go b/cli/output/text.go index e8151dfd701..f22d65fa3b6 100644 --- a/cli/output/text.go +++ b/cli/output/text.go @@ -121,8 +121,7 @@ func spaces(n int) string { return res } -// Sprintf FIXMEDOC -func Sprintf(format string, args ...interface{}) TextBox { +func sprintf(format string, args ...interface{}) TextBox { cleanArgs := make([]interface{}, len(args)) for i, arg := range args { if text, ok := arg.(*Text); ok { diff --git a/commands/board/list.go b/commands/board/list.go index 0a59247b765..f366f303493 100644 --- a/commands/board/list.go +++ b/commands/board/list.go @@ -23,15 +23,31 @@ import ( "io/ioutil" "net/http" + "github.com/arduino/arduino-cli/cli/globals" "github.com/arduino/arduino-cli/commands" rpc "github.com/arduino/arduino-cli/rpc/commands" "github.com/pkg/errors" ) +var ( + // ErrNotFound is returned when the API returns 404 + ErrNotFound = errors.New("board not found") +) + func apiByVidPid(url string) ([]*rpc.BoardListItem, error) { retVal := []*rpc.BoardListItem{} req, _ := http.NewRequest("GET", url, nil) + req.Header = globals.HTTPClientHeader + req.Header.Set("Content-Type", "application/json") + if res, err := http.DefaultClient.Do(req); err == nil { + if res.StatusCode >= 400 { + if res.StatusCode == 404 { + return nil, ErrNotFound + } + return nil, errors.Errorf("the server responded with status %s", res.Status) + } + body, _ := ioutil.ReadAll(res.Body) res.Body.Close() @@ -100,7 +116,11 @@ func List(instanceID int32) ([]*rpc.DetectedPort, error) { port.IdentificationPrefs.Get("vid"), port.IdentificationPrefs.Get("pid")) items, err := apiByVidPid(url) - if err != nil { + if err == ErrNotFound { + // the board couldn't be detected, keep going with the next port + continue + } else if err != nil { + // this is bad, bail out return nil, errors.Wrap(err, "error getting bard info from Arduino Cloud") } diff --git a/commands/board/list_test.go b/commands/board/list_test.go index 5f0e8c41903..65fcaaa8cfb 100644 --- a/commands/board/list_test.go +++ b/commands/board/list_test.go @@ -51,6 +51,31 @@ func TestGetByVidPid(t *testing.T) { require.NotNil(t, err) } +func TestGetByVidPidNotFound(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer ts.Close() + + res, err := apiByVidPid(ts.URL) + require.NotNil(t, err) + require.Equal(t, "board not found", err.Error()) + require.Len(t, res, 0) +} + +func TestGetByVidPid5xx(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("500 - Ooooops!")) + })) + defer ts.Close() + + res, err := apiByVidPid(ts.URL) + require.NotNil(t, err) + require.Equal(t, "the server responded with status 500 Internal Server Error", err.Error()) + require.Len(t, res, 0) +} + func TestGetByVidPidMalformedResponse(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "{}") From e9362d5a4d5ab72dfdf9bdfec6e750406c7b9831 Mon Sep 17 00:00:00 2001 From: Massimiliano Pippi Date: Fri, 9 Aug 2019 15:22:56 +0200 Subject: [PATCH 4/4] Update commands/board/list.go Co-Authored-By: Maurizio Branca --- commands/board/list.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/board/list.go b/commands/board/list.go index f366f303493..11e4c3cc3d8 100644 --- a/commands/board/list.go +++ b/commands/board/list.go @@ -121,7 +121,7 @@ func List(instanceID int32) ([]*rpc.DetectedPort, error) { continue } else if err != nil { // this is bad, bail out - return nil, errors.Wrap(err, "error getting bard info from Arduino Cloud") + return nil, errors.Wrap(err, "error getting board info from Arduino Cloud") } b = items