From 173d76493b2f85ae09c2fb629c43152ea102ca06 Mon Sep 17 00:00:00 2001
From: Massimiliano Pippi <mpippi@gmail.com>
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 <mpippi@gmail.com>
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 <mpippi@gmail.com>
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 <mpippi@gmail.com>
Date: Fri, 9 Aug 2019 15:22:56 +0200
Subject: [PATCH 4/4] Update commands/board/list.go

Co-Authored-By: Maurizio Branca <m.branca@arduino.cc>
---
 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