Skip to content

Commit 6faca0f

Browse files
authored
[breaking] gRPC UpdateIndex and UpdateLibrariesIndex improvements (#2569)
* gRPC UpdateIndex and UpdateLibrariesIndex improvements The two calls now have the update_if_older_than_secs field that allows to avoid updating the index if it has been already updated. Also the response is more explicit with oneof(..) clause and the status of each update (in case of multiple index update) is returned in the response. * Do not make any output in case of skipped/already-up-to-date * Added json output to 'core update index' * Added json output to 'lib update index' * Removed unused function * Workaround for Windows paths in URI
1 parent ad9936f commit 6faca0f

File tree

12 files changed

+1457
-822
lines changed

12 files changed

+1457
-822
lines changed

Diff for: commands/daemon/daemon.go

+22-4
Original file line numberDiff line numberDiff line change
@@ -121,18 +121,36 @@ func (s *ArduinoCoreServerImpl) Destroy(ctx context.Context, req *rpc.DestroyReq
121121
// UpdateIndex FIXMEDOC
122122
func (s *ArduinoCoreServerImpl) UpdateIndex(req *rpc.UpdateIndexRequest, stream rpc.ArduinoCoreService_UpdateIndexServer) error {
123123
syncSend := NewSynchronizedSend(stream.Send)
124-
err := commands.UpdateIndex(stream.Context(), req,
125-
func(p *rpc.DownloadProgress) { syncSend.Send(&rpc.UpdateIndexResponse{DownloadProgress: p}) },
124+
res, err := commands.UpdateIndex(stream.Context(), req,
125+
func(p *rpc.DownloadProgress) {
126+
syncSend.Send(&rpc.UpdateIndexResponse{
127+
Message: &rpc.UpdateIndexResponse_DownloadProgress{DownloadProgress: p},
128+
})
129+
},
126130
)
131+
if res != nil {
132+
syncSend.Send(&rpc.UpdateIndexResponse{
133+
Message: &rpc.UpdateIndexResponse_Result_{Result: res},
134+
})
135+
}
127136
return convertErrorToRPCStatus(err)
128137
}
129138

130139
// UpdateLibrariesIndex FIXMEDOC
131140
func (s *ArduinoCoreServerImpl) UpdateLibrariesIndex(req *rpc.UpdateLibrariesIndexRequest, stream rpc.ArduinoCoreService_UpdateLibrariesIndexServer) error {
132141
syncSend := NewSynchronizedSend(stream.Send)
133-
err := commands.UpdateLibrariesIndex(stream.Context(), req,
134-
func(p *rpc.DownloadProgress) { syncSend.Send(&rpc.UpdateLibrariesIndexResponse{DownloadProgress: p}) },
142+
res, err := commands.UpdateLibrariesIndex(stream.Context(), req,
143+
func(p *rpc.DownloadProgress) {
144+
syncSend.Send(&rpc.UpdateLibrariesIndexResponse{
145+
Message: &rpc.UpdateLibrariesIndexResponse_DownloadProgress{DownloadProgress: p},
146+
})
147+
},
135148
)
149+
if res != nil {
150+
syncSend.Send(&rpc.UpdateLibrariesIndexResponse{
151+
Message: &rpc.UpdateLibrariesIndexResponse_Result_{Result: res},
152+
})
153+
}
136154
return convertErrorToRPCStatus(err)
137155
}
138156

Diff for: commands/instances.go

+74-14
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ import (
2020
"fmt"
2121
"net/url"
2222
"path/filepath"
23+
"runtime"
2324
"strings"
25+
"time"
2426

2527
"github.com/arduino/arduino-cli/commands/cmderrors"
2628
"github.com/arduino/arduino-cli/commands/internal/instances"
@@ -406,30 +408,59 @@ func Destroy(ctx context.Context, req *rpc.DestroyRequest) (*rpc.DestroyResponse
406408
}
407409

408410
// UpdateLibrariesIndex updates the library_index.json
409-
func UpdateLibrariesIndex(ctx context.Context, req *rpc.UpdateLibrariesIndexRequest, downloadCB rpc.DownloadProgressCB) error {
411+
func UpdateLibrariesIndex(ctx context.Context, req *rpc.UpdateLibrariesIndexRequest, downloadCB rpc.DownloadProgressCB) (*rpc.UpdateLibrariesIndexResponse_Result, error) {
410412
logrus.Info("Updating libraries index")
413+
411414
pme, release, err := instances.GetPackageManagerExplorer(req.GetInstance())
412415
if err != nil {
413-
return err
416+
return nil, err
414417
}
415418
indexDir := pme.IndexDir
416419
release()
417420

421+
index := globals.LibrariesIndexResource
422+
result := func(status rpc.IndexUpdateReport_Status) *rpc.UpdateLibrariesIndexResponse_Result {
423+
return &rpc.UpdateLibrariesIndexResponse_Result{
424+
LibrariesIndex: &rpc.IndexUpdateReport{
425+
IndexUrl: globals.LibrariesIndexResource.URL.String(),
426+
Status: status,
427+
},
428+
}
429+
}
430+
431+
// Create the index directory if it doesn't exist
418432
if err := indexDir.MkdirAll(); err != nil {
419-
return &cmderrors.PermissionDeniedError{Message: tr("Could not create index directory"), Cause: err}
433+
return result(rpc.IndexUpdateReport_STATUS_FAILED), &cmderrors.PermissionDeniedError{Message: tr("Could not create index directory"), Cause: err}
434+
}
435+
436+
// Check if the index file is already up-to-date
437+
indexFileName, _ := index.IndexFileName()
438+
if info, err := indexDir.Join(indexFileName).Stat(); err == nil {
439+
ageSecs := int64(time.Since(info.ModTime()).Seconds())
440+
if ageSecs < req.GetUpdateIfOlderThanSecs() {
441+
return result(rpc.IndexUpdateReport_STATUS_ALREADY_UP_TO_DATE), nil
442+
}
420443
}
421444

445+
// Perform index update
422446
if err := globals.LibrariesIndexResource.Download(indexDir, downloadCB); err != nil {
423-
return err
447+
return nil, err
424448
}
425449

426-
return nil
450+
return result(rpc.IndexUpdateReport_STATUS_UPDATED), nil
427451
}
428452

429453
// UpdateIndex FIXMEDOC
430-
func UpdateIndex(ctx context.Context, req *rpc.UpdateIndexRequest, downloadCB rpc.DownloadProgressCB) error {
454+
func UpdateIndex(ctx context.Context, req *rpc.UpdateIndexRequest, downloadCB rpc.DownloadProgressCB) (*rpc.UpdateIndexResponse_Result, error) {
431455
if !instances.IsValid(req.GetInstance()) {
432-
return &cmderrors.InvalidInstanceError{}
456+
return nil, &cmderrors.InvalidInstanceError{}
457+
}
458+
459+
report := func(indexURL *url.URL, status rpc.IndexUpdateReport_Status) *rpc.IndexUpdateReport {
460+
return &rpc.IndexUpdateReport{
461+
IndexUrl: indexURL.String(),
462+
Status: status,
463+
}
433464
}
434465

435466
indexpath := configuration.DataDir(configuration.Settings)
@@ -440,46 +471,75 @@ func UpdateIndex(ctx context.Context, req *rpc.UpdateIndexRequest, downloadCB rp
440471
}
441472

442473
failed := false
474+
result := &rpc.UpdateIndexResponse_Result{}
443475
for _, u := range urls {
444-
URL, err := utils.URLParse(u)
476+
URL, err := url.Parse(u)
445477
if err != nil {
446478
logrus.Warnf("unable to parse additional URL: %s", u)
447479
msg := fmt.Sprintf("%s: %v", tr("Unable to parse URL"), err)
448480
downloadCB.Start(u, tr("Downloading index: %s", u))
449481
downloadCB.End(false, msg)
450482
failed = true
483+
result.UpdatedIndexes = append(result.UpdatedIndexes, report(URL, rpc.IndexUpdateReport_STATUS_FAILED))
451484
continue
452485
}
453486

454487
logrus.WithField("url", URL).Print("Updating index")
455488

456489
if URL.Scheme == "file" {
457-
downloadCB.Start(u, tr("Downloading index: %s", filepath.Base(URL.Path)))
458490
path := paths.New(URL.Path)
491+
if URL.Scheme == "file" && runtime.GOOS == "windows" && len(URL.Path) > 1 {
492+
// https://github.com/golang/go/issues/32456
493+
// Parsed local file URLs on Windows are returned with a leading / so we remove it
494+
path = paths.New(URL.Path[1:])
495+
}
459496
if _, err := packageindex.LoadIndexNoSign(path); err != nil {
460497
msg := fmt.Sprintf("%s: %v", tr("Invalid package index in %s", path), err)
498+
downloadCB.Start(u, tr("Downloading index: %s", filepath.Base(URL.Path)))
461499
downloadCB.End(false, msg)
462500
failed = true
501+
result.UpdatedIndexes = append(result.UpdatedIndexes, report(URL, rpc.IndexUpdateReport_STATUS_FAILED))
463502
} else {
464-
downloadCB.End(true, "")
503+
result.UpdatedIndexes = append(result.UpdatedIndexes, report(URL, rpc.IndexUpdateReport_STATUS_SKIPPED))
465504
}
466505
continue
467506
}
468507

508+
// Check if the index is up-to-date
469509
indexResource := resources.IndexResource{URL: URL}
510+
indexFileName, err := indexResource.IndexFileName()
511+
if err != nil {
512+
downloadCB.Start(u, tr("Downloading index: %s", filepath.Base(URL.Path)))
513+
downloadCB.End(false, tr("Invalid index URL: %s", err))
514+
failed = true
515+
result.UpdatedIndexes = append(result.UpdatedIndexes, report(URL, rpc.IndexUpdateReport_STATUS_FAILED))
516+
continue
517+
}
518+
indexFile := indexpath.Join(indexFileName)
519+
if info, err := indexFile.Stat(); err == nil {
520+
ageSecs := int64(time.Since(info.ModTime()).Seconds())
521+
if ageSecs < req.GetUpdateIfOlderThanSecs() {
522+
result.UpdatedIndexes = append(result.UpdatedIndexes, report(URL, rpc.IndexUpdateReport_STATUS_ALREADY_UP_TO_DATE))
523+
continue
524+
}
525+
}
526+
470527
if strings.HasSuffix(URL.Host, "arduino.cc") && strings.HasSuffix(URL.Path, ".json") {
471528
indexResource.SignatureURL, _ = url.Parse(u) // should not fail because we already parsed it
472529
indexResource.SignatureURL.Path += ".sig"
473530
}
474531
if err := indexResource.Download(indexpath, downloadCB); err != nil {
475532
failed = true
533+
result.UpdatedIndexes = append(result.UpdatedIndexes, report(URL, rpc.IndexUpdateReport_STATUS_FAILED))
534+
} else {
535+
result.UpdatedIndexes = append(result.UpdatedIndexes, report(URL, rpc.IndexUpdateReport_STATUS_UPDATED))
476536
}
477537
}
478538

479539
if failed {
480-
return &cmderrors.FailedDownloadError{Message: tr("Some indexes could not be updated.")}
540+
return result, &cmderrors.FailedDownloadError{Message: tr("Some indexes could not be updated.")}
481541
}
482-
return nil
542+
return result, nil
483543
}
484544

485545
// firstUpdate downloads libraries and packages indexes if they don't exist.
@@ -493,7 +553,7 @@ func firstUpdate(ctx context.Context, instance *rpc.Instance, downloadCb func(ms
493553
// The library_index.json file doesn't exists, that means the CLI is run for the first time
494554
// so we proceed with the first update that downloads the file
495555
req := &rpc.UpdateLibrariesIndexRequest{Instance: instance}
496-
if err := UpdateLibrariesIndex(ctx, req, downloadCb); err != nil {
556+
if _, err := UpdateLibrariesIndex(ctx, req, downloadCb); err != nil {
497557
return err
498558
}
499559
}
@@ -515,7 +575,7 @@ func firstUpdate(ctx context.Context, instance *rpc.Instance, downloadCb func(ms
515575
// library update we download that file and all the other package indexes from
516576
// additional_urls
517577
req := &rpc.UpdateIndexRequest{Instance: instance}
518-
if err := UpdateIndex(ctx, req, downloadCb); err != nil {
578+
if _, err := UpdateIndex(ctx, req, downloadCb); err != nil {
519579
return err
520580
}
521581
break

Diff for: docs/UPGRADING.md

+70
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,76 @@ Here you can find a list of migration guides to handle breaking changes between
44

55
## 0.36.0
66

7+
### The gRPC `cc.arduino.cli.commands.v1.UpdateIndexResponse` and `UpdateLibrariesIndexResponse` have changed.
8+
9+
The responses coming from the update index commands:
10+
11+
```proto
12+
message UpdateIndexResponse {
13+
// Progress of the package index download.
14+
DownloadProgress download_progress = 1;
15+
}
16+
17+
message UpdateLibrariesIndexResponse {
18+
// Progress of the libraries index download.
19+
DownloadProgress download_progress = 1;
20+
}
21+
```
22+
23+
are now more explicit and contains details about the result of the operation:
24+
25+
```proto
26+
message UpdateIndexResponse {
27+
message Result {
28+
// The result of the packages index update.
29+
repeated IndexUpdateReport updated_indexes = 1;
30+
}
31+
oneof message {
32+
// Progress of the package index download.
33+
DownloadProgress download_progress = 1;
34+
// The result of the index update.
35+
Result result = 2;
36+
}
37+
}
38+
39+
message UpdateLibrariesIndexResponse {
40+
message Result {
41+
// The result of the libraries index update.
42+
IndexUpdateReport libraries_index = 1;
43+
}
44+
oneof message {
45+
// Progress of the libraries index download.
46+
DownloadProgress download_progress = 1;
47+
// The result of the index update.
48+
Result result = 2;
49+
}
50+
}
51+
```
52+
53+
The `IndexUpdateReport` message contains details for each index update operation performed:
54+
55+
```proto
56+
message IndexUpdateReport {
57+
enum Status {
58+
// The status of the index update is unspecified.
59+
STATUS_UNSPECIFIED = 0;
60+
// The index has been successfully updated.
61+
STATUS_UPDATED = 1;
62+
// The index was already up to date.
63+
STATUS_ALREADY_UP_TO_DATE = 2;
64+
// The index update failed.
65+
STATUS_FAILED = 3;
66+
// The index update was skipped.
67+
STATUS_SKIPPED = 4;
68+
}
69+
70+
// The URL of the index that was updated.
71+
string index_url = 1;
72+
// The result of the index update.
73+
Status status = 2;
74+
}
75+
```
76+
777
### The gRPC `cc.arduino.cli.commands.v1.Profile` message has been removed in favor of `SketchProfile`
878

979
The message `Profile` has been replaced with `SketchProfile` in the `InitResponse.profile` field:

Diff for: internal/cli/core/search.go

+13-65
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,11 @@ import (
1919
"context"
2020
"fmt"
2121
"os"
22-
"path"
2322
"strings"
2423
"time"
2524

2625
"github.com/arduino/arduino-cli/commands"
2726
"github.com/arduino/arduino-cli/commands/core"
28-
"github.com/arduino/arduino-cli/internal/arduino/globals"
29-
"github.com/arduino/arduino-cli/internal/arduino/utils"
30-
"github.com/arduino/arduino-cli/internal/cli/configuration"
3127
"github.com/arduino/arduino-cli/internal/cli/feedback"
3228
"github.com/arduino/arduino-cli/internal/cli/feedback/result"
3329
"github.com/arduino/arduino-cli/internal/cli/feedback/table"
@@ -55,17 +51,24 @@ func initSearchCommand() *cobra.Command {
5551
}
5652

5753
// indexUpdateInterval specifies the time threshold over which indexes are updated
58-
const indexUpdateInterval = "24h"
54+
const indexUpdateInterval = 24 * time.Hour
5955

6056
func runSearchCommand(cmd *cobra.Command, args []string, allVersions bool) {
6157
inst := instance.CreateAndInit()
6258

63-
if indexesNeedUpdating(indexUpdateInterval) {
64-
err := commands.UpdateIndex(context.Background(), &rpc.UpdateIndexRequest{Instance: inst}, feedback.ProgressBar())
65-
if err != nil {
66-
feedback.FatalError(err, feedback.ErrGeneric)
59+
res, err := commands.UpdateIndex(
60+
context.Background(),
61+
&rpc.UpdateIndexRequest{Instance: inst, UpdateIfOlderThanSecs: int64(indexUpdateInterval.Seconds())},
62+
feedback.ProgressBar())
63+
if err != nil {
64+
feedback.FatalError(err, feedback.ErrGeneric)
65+
}
66+
for _, idxRes := range res.GetUpdatedIndexes() {
67+
if idxRes.GetStatus() == rpc.IndexUpdateReport_STATUS_UPDATED {
68+
// At least one index has been updated, reinitialize the instance
69+
instance.Init(inst)
70+
break
6771
}
68-
instance.Init(inst)
6972
}
7073

7174
arguments := strings.ToLower(strings.Join(args, " "))
@@ -134,58 +137,3 @@ func (sr searchResults) String() string {
134137
}
135138
return t.Render()
136139
}
137-
138-
// indexesNeedUpdating returns whether one or more index files need updating.
139-
// A duration string must be provided to calculate the time threshold
140-
// used to update the indexes, if the duration is not valid a default
141-
// of 24 hours is used.
142-
// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
143-
func indexesNeedUpdating(duration string) bool {
144-
indexpath := configuration.DataDir(configuration.Settings)
145-
146-
now := time.Now()
147-
modTimeThreshold, err := time.ParseDuration(duration)
148-
if err != nil {
149-
feedback.Fatal(tr("Invalid timeout: %s", err), feedback.ErrBadArgument)
150-
}
151-
152-
urls := []string{globals.DefaultIndexURL}
153-
urls = append(urls, configuration.Settings.GetStringSlice("board_manager.additional_urls")...)
154-
for _, u := range urls {
155-
URL, err := utils.URLParse(u)
156-
if err != nil {
157-
continue
158-
}
159-
160-
if URL.Scheme == "file" {
161-
// No need to update local files
162-
continue
163-
}
164-
165-
// should handle:
166-
// - package_index.json
167-
// - package_index.json.sig
168-
// - package_index.json.gz
169-
// - package_index.tar.bz2
170-
indexFileName := path.Base(URL.Path)
171-
indexFileName = strings.TrimSuffix(indexFileName, ".tar.bz2")
172-
indexFileName = strings.TrimSuffix(indexFileName, ".gz")
173-
indexFileName = strings.TrimSuffix(indexFileName, ".sig")
174-
indexFileName = strings.TrimSuffix(indexFileName, ".json")
175-
// and obtain package_index.json as result
176-
coreIndexPath := indexpath.Join(indexFileName + ".json")
177-
if coreIndexPath.NotExist() {
178-
return true
179-
}
180-
181-
info, err := coreIndexPath.Stat()
182-
if err != nil {
183-
return true
184-
}
185-
186-
if now.After(info.ModTime().Add(modTimeThreshold)) {
187-
return true
188-
}
189-
}
190-
return false
191-
}

0 commit comments

Comments
 (0)