Skip to content

Commit 06a3564

Browse files
authored
Improve board list detection via cloud API (cache responses / do not error on network failure) (#1982)
* Slightly refactored apiByVidPid * Cache cloud-api response for 24h to improve responsiveness * Do not fail with errors in case of cloud-api is not available * Fixed linter warning... * Removed useless ErrNotFound from `apiByVidPid` The `apiByVidPid` function now masks the odd behavior of the builder-api returning an HTTP 404 if the request succeed but the result is empty.
1 parent b1150e0 commit 06a3564

File tree

3 files changed

+73
-54
lines changed

3 files changed

+73
-54
lines changed

Diff for: commands/board/list.go

+64-50
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import (
1919
"context"
2020
"encoding/json"
2121
"fmt"
22-
"io/ioutil"
22+
"io"
2323
"net/http"
2424
"regexp"
2525
"sort"
@@ -32,24 +32,43 @@ import (
3232
"github.com/arduino/arduino-cli/arduino/discovery"
3333
"github.com/arduino/arduino-cli/arduino/httpclient"
3434
"github.com/arduino/arduino-cli/commands"
35+
"github.com/arduino/arduino-cli/inventory"
3536
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
3637
"github.com/pkg/errors"
3738
"github.com/sirupsen/logrus"
3839
)
3940

40-
type boardNotFoundError struct{}
41-
42-
func (e *boardNotFoundError) Error() string {
43-
return tr("board not found")
44-
}
45-
4641
var (
47-
// ErrNotFound is returned when the API returns 404
48-
ErrNotFound = &boardNotFoundError{}
4942
vidPidURL = "https://builder.arduino.cc/v3/boards/byVidPid"
5043
validVidPid = regexp.MustCompile(`0[xX][a-fA-F\d]{4}`)
5144
)
5245

46+
func cachedAPIByVidPid(vid, pid string) ([]*rpc.BoardListItem, error) {
47+
var resp []*rpc.BoardListItem
48+
49+
cacheKey := fmt.Sprintf("cache.builder-api.v3/boards/byvid/pid/%s/%s", vid, pid)
50+
if cachedResp := inventory.Store.GetString(cacheKey + ".data"); cachedResp != "" {
51+
ts := inventory.Store.GetTime(cacheKey + ".ts")
52+
if time.Since(ts) < time.Hour*24 {
53+
// Use cached response
54+
if err := json.Unmarshal([]byte(cachedResp), &resp); err == nil {
55+
return resp, nil
56+
}
57+
}
58+
}
59+
60+
resp, err := apiByVidPid(vid, pid) // Perform API requrest
61+
62+
if err == nil {
63+
if cachedResp, err := json.Marshal(resp); err == nil {
64+
inventory.Store.Set(cacheKey+".data", string(cachedResp))
65+
inventory.Store.Set(cacheKey+".ts", time.Now())
66+
inventory.WriteStore()
67+
}
68+
}
69+
return resp, err
70+
}
71+
5372
func apiByVidPid(vid, pid string) ([]*rpc.BoardListItem, error) {
5473
// ensure vid and pid are valid before hitting the API
5574
if !validVidPid.MatchString(vid) {
@@ -60,7 +79,6 @@ func apiByVidPid(vid, pid string) ([]*rpc.BoardListItem, error) {
6079
}
6180

6281
url := fmt.Sprintf("%s/%s/%s", vidPidURL, vid, pid)
63-
retVal := []*rpc.BoardListItem{}
6482
req, _ := http.NewRequest("GET", url, nil)
6583
req.Header.Set("Content-Type", "application/json")
6684

@@ -72,50 +90,53 @@ func apiByVidPid(vid, pid string) ([]*rpc.BoardListItem, error) {
7290
return nil, errors.Wrap(err, tr("failed to initialize http client"))
7391
}
7492

75-
if res, err := httpClient.Do(req); err == nil {
76-
if res.StatusCode >= 400 {
77-
if res.StatusCode == 404 {
78-
return nil, ErrNotFound
79-
}
80-
return nil, errors.Errorf(tr("the server responded with status %s"), res.Status)
81-
}
82-
83-
body, _ := ioutil.ReadAll(res.Body)
84-
res.Body.Close()
85-
86-
var dat map[string]interface{}
87-
err = json.Unmarshal(body, &dat)
88-
if err != nil {
89-
return nil, errors.Wrap(err, tr("error processing response from server"))
90-
}
93+
res, err := httpClient.Do(req)
94+
if err != nil {
95+
return nil, errors.Wrap(err, tr("error querying Arduino Cloud Api"))
96+
}
97+
if res.StatusCode == 404 {
98+
// This is not an error, it just means that the board is not recognized
99+
return nil, nil
100+
}
101+
if res.StatusCode >= 400 {
102+
return nil, errors.Errorf(tr("the server responded with status %s"), res.Status)
103+
}
91104

92-
name, nameFound := dat["name"].(string)
93-
fqbn, fbqnFound := dat["fqbn"].(string)
105+
resp, err := io.ReadAll(res.Body)
106+
if err != nil {
107+
return nil, err
108+
}
109+
if err := res.Body.Close(); err != nil {
110+
return nil, err
111+
}
94112

95-
if !nameFound || !fbqnFound {
96-
return nil, errors.New(tr("wrong format in server response"))
97-
}
113+
var dat map[string]interface{}
114+
if err := json.Unmarshal(resp, &dat); err != nil {
115+
return nil, errors.Wrap(err, tr("error processing response from server"))
116+
}
117+
name, nameFound := dat["name"].(string)
118+
fqbn, fbqnFound := dat["fqbn"].(string)
119+
if !nameFound || !fbqnFound {
120+
return nil, errors.New(tr("wrong format in server response"))
121+
}
98122

99-
retVal = append(retVal, &rpc.BoardListItem{
123+
return []*rpc.BoardListItem{
124+
{
100125
Name: name,
101126
Fqbn: fqbn,
102-
})
103-
} else {
104-
return nil, errors.Wrap(err, tr("error querying Arduino Cloud Api"))
105-
}
106-
107-
return retVal, nil
127+
},
128+
}, nil
108129
}
109130

110131
func identifyViaCloudAPI(port *discovery.Port) ([]*rpc.BoardListItem, error) {
111132
// If the port is not USB do not try identification via cloud
112133
id := port.Properties
113134
if !id.ContainsKey("vid") || !id.ContainsKey("pid") {
114-
return nil, ErrNotFound
135+
return nil, nil
115136
}
116137

117138
logrus.Debug("Querying builder API for board identification...")
118-
return apiByVidPid(id.Get("vid"), id.Get("pid"))
139+
return cachedAPIByVidPid(id.Get("vid"), id.Get("pid"))
119140
}
120141

121142
// identify returns a list of boards checking first the installed platforms or the Cloud API
@@ -146,17 +167,10 @@ func identify(pme *packagemanager.Explorer, port *discovery.Port) ([]*rpc.BoardL
146167
// the builder API if the board is a USB device port
147168
if len(boards) == 0 {
148169
items, err := identifyViaCloudAPI(port)
149-
if errors.Is(err, ErrNotFound) {
150-
// the board couldn't be detected, print a warning
151-
logrus.Debug("Board not recognized")
152-
} else if err != nil {
153-
// this is bad, bail out
154-
return nil, &arduino.UnavailableError{Message: tr("Error getting board info from Arduino Cloud")}
170+
if err != nil {
171+
// this is bad, but keep going
172+
logrus.WithError(err).Debug("Error querying builder API")
155173
}
156-
157-
// add a DetectedPort entry in any case: the `Boards` field will
158-
// be empty but the port will be shown anyways (useful for 3rd party
159-
// boards)
160174
boards = items
161175
}
162176

Diff for: commands/board/list_test.go

+3-4
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,8 @@ func TestGetByVidPidNotFound(t *testing.T) {
7171

7272
vidPidURL = ts.URL
7373
res, err := apiByVidPid("0x0420", "0x0069")
74-
require.NotNil(t, err)
75-
require.Equal(t, "board not found", err.Error())
76-
require.Len(t, res, 0)
74+
require.NoError(t, err)
75+
require.Empty(t, res)
7776
}
7877

7978
func TestGetByVidPid5xx(t *testing.T) {
@@ -108,7 +107,7 @@ func TestBoardDetectionViaAPIWithNonUSBPort(t *testing.T) {
108107
Properties: properties.NewMap(),
109108
}
110109
items, err := identifyViaCloudAPI(port)
111-
require.ErrorIs(t, err, ErrNotFound)
110+
require.NoError(t, err)
112111
require.Empty(t, items)
113112
}
114113

Diff for: inventory/inventory.go

+6
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"fmt"
2020
"os"
2121
"path/filepath"
22+
"sync"
2223

2324
"github.com/arduino/arduino-cli/i18n"
2425
"github.com/gofrs/uuid"
@@ -77,9 +78,14 @@ func generateInstallationData() error {
7778
return nil
7879
}
7980

81+
var writeStoreMux sync.Mutex
82+
8083
// WriteStore writes the current information from Store to configFilePath.
8184
// Returns err if it fails.
8285
func WriteStore() error {
86+
writeStoreMux.Lock()
87+
defer writeStoreMux.Unlock()
88+
8389
configPath := filepath.Dir(configFilePath)
8490

8591
// Create config dir if not present,

0 commit comments

Comments
 (0)