Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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

‎commands/board/list.go‎

Lines changed: 64 additions & 50 deletions
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

‎commands/board/list_test.go‎

Lines changed: 3 additions & 4 deletions
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

‎inventory/inventory.go‎

Lines changed: 6 additions & 0 deletions
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)

AltStyle によって変換されたページ (->オリジナル) /