Unverified Commit c8ff0425 authored by Cristian Maglie's avatar Cristian Maglie Committed by GitHub

[breaking] Refactor `DownloadProgress` gRPC message: more clear field meaning. (#1875)

* Refactored DownloadProgress protocol

* Updated integration tests

* Do not create overly verbose errors

* Updated docs

* Update rpc/cc/arduino/cli/commands/v1/commands.proto
Co-authored-by: default avatarper1234 <accounts@perglass.com>
Co-authored-by: default avatarper1234 <accounts@perglass.com>
parent bcb69d99
...@@ -127,12 +127,7 @@ func (pme *Explorer) DownloadToolRelease(tool *cores.ToolRelease, config *downlo ...@@ -127,12 +127,7 @@ func (pme *Explorer) DownloadToolRelease(tool *cores.ToolRelease, config *downlo
Message: tr("Error downloading tool %s", tool), Message: tr("Error downloading tool %s", tool),
Cause: errors.New(tr("no versions available for the current OS"))} Cause: errors.New(tr("no versions available for the current OS"))}
} }
if err := resource.Download(pme.DownloadDir, config, tool.String(), progressCB); err != nil { return resource.Download(pme.DownloadDir, config, tool.String(), progressCB)
return &arduino.FailedDownloadError{
Message: tr("Error downloading tool %s", tool),
Cause: err}
}
return nil
} }
// DownloadPlatformRelease downloads a PlatformRelease. If the platform is already downloaded a // DownloadPlatformRelease downloads a PlatformRelease. If the platform is already downloaded a
......
...@@ -32,7 +32,16 @@ var tr = i18n.Tr ...@@ -32,7 +32,16 @@ var tr = i18n.Tr
// DownloadFile downloads a file from a URL into the specified path. An optional config and options may be passed (or nil to use the defaults). // DownloadFile downloads a file from a URL into the specified path. An optional config and options may be passed (or nil to use the defaults).
// A DownloadProgressCB callback function must be passed to monitor download progress. // A DownloadProgressCB callback function must be passed to monitor download progress.
func DownloadFile(path *paths.Path, URL string, label string, downloadCB rpc.DownloadProgressCB, config *downloader.Config, options ...downloader.DownloadOptions) error { func DownloadFile(path *paths.Path, URL string, label string, downloadCB rpc.DownloadProgressCB, config *downloader.Config, options ...downloader.DownloadOptions) (returnedError error) {
downloadCB.Start(URL, label)
defer func() {
if returnedError == nil {
downloadCB.End(true, "")
} else {
downloadCB.End(false, returnedError.Error())
}
}()
if config == nil { if config == nil {
c, err := GetDownloaderConfig() c, err := GetDownloaderConfig()
if err != nil { if err != nil {
...@@ -45,15 +54,9 @@ func DownloadFile(path *paths.Path, URL string, label string, downloadCB rpc.Dow ...@@ -45,15 +54,9 @@ func DownloadFile(path *paths.Path, URL string, label string, downloadCB rpc.Dow
if err != nil { if err != nil {
return err return err
} }
downloadCB(&rpc.DownloadProgress{
File: label,
Url: d.URL,
TotalSize: d.Size(),
})
defer downloadCB(&rpc.DownloadProgress{Completed: true})
err = d.RunAndPoll(func(downloaded int64) { err = d.RunAndPoll(func(downloaded int64) {
downloadCB(&rpc.DownloadProgress{Downloaded: downloaded}) downloadCB.Update(downloaded, d.Size())
}, 250*time.Millisecond) }, 250*time.Millisecond)
if err != nil { if err != nil {
return err return err
...@@ -61,7 +64,8 @@ func DownloadFile(path *paths.Path, URL string, label string, downloadCB rpc.Dow ...@@ -61,7 +64,8 @@ func DownloadFile(path *paths.Path, URL string, label string, downloadCB rpc.Dow
// The URL is not reachable for some reason // The URL is not reachable for some reason
if d.Resp.StatusCode >= 400 && d.Resp.StatusCode <= 599 { if d.Resp.StatusCode >= 400 && d.Resp.StatusCode <= 599 {
return &arduino.FailedDownloadError{Message: tr("Server responded with: %s", d.Resp.Status)} msg := tr("Server responded with: %s", d.Resp.Status)
return &arduino.FailedDownloadError{Message: msg}
} }
return nil return nil
......
...@@ -44,12 +44,8 @@ func (r *DownloadResource) Download(downloadDir *paths.Path, config *downloader. ...@@ -44,12 +44,8 @@ func (r *DownloadResource) Download(downloadDir *paths.Path, config *downloader.
} }
} else { } else {
// File is cached, nothing to do here // File is cached, nothing to do here
downloadCB.Start(r.URL, label)
// This signal means that the file is already downloaded downloadCB.End(true, tr("%s already downloaded", label))
downloadCB(&rpc.DownloadProgress{
File: label,
Completed: true,
})
return nil return nil
} }
} else { } else {
......
...@@ -67,9 +67,7 @@ func runSearchCommand(cmd *cobra.Command, args []string) { ...@@ -67,9 +67,7 @@ func runSearchCommand(cmd *cobra.Command, args []string) {
} }
if indexesNeedUpdating(indexUpdateInterval) { if indexesNeedUpdating(indexUpdateInterval) {
err := commands.UpdateIndex(context.Background(), &rpc.UpdateIndexRequest{Instance: inst}, err := commands.UpdateIndex(context.Background(), &rpc.UpdateIndexRequest{Instance: inst}, output.ProgressBar())
output.ProgressBar(),
output.PrintErrorFromDownloadResult(tr("Error updating index")))
if err != nil { if err != nil {
os.Exit(errorcodes.ErrGeneric) os.Exit(errorcodes.ErrGeneric)
} }
......
...@@ -20,6 +20,7 @@ import ( ...@@ -20,6 +20,7 @@ import (
"os" "os"
"github.com/arduino/arduino-cli/cli/errorcodes" "github.com/arduino/arduino-cli/cli/errorcodes"
"github.com/arduino/arduino-cli/cli/feedback"
"github.com/arduino/arduino-cli/cli/instance" "github.com/arduino/arduino-cli/cli/instance"
"github.com/arduino/arduino-cli/cli/output" "github.com/arduino/arduino-cli/cli/output"
"github.com/arduino/arduino-cli/commands" "github.com/arduino/arduino-cli/commands"
...@@ -44,10 +45,9 @@ func runUpdateIndexCommand(cmd *cobra.Command, args []string) { ...@@ -44,10 +45,9 @@ func runUpdateIndexCommand(cmd *cobra.Command, args []string) {
inst := instance.CreateInstanceAndRunFirstUpdate() inst := instance.CreateInstanceAndRunFirstUpdate()
logrus.Info("Executing `arduino-cli core update-index`") logrus.Info("Executing `arduino-cli core update-index`")
err := commands.UpdateIndex(context.Background(), &rpc.UpdateIndexRequest{Instance: inst}, err := commands.UpdateIndex(context.Background(), &rpc.UpdateIndexRequest{Instance: inst}, output.ProgressBar())
output.ProgressBar(),
output.PrintErrorFromDownloadResult(tr("Error updating index")))
if err != nil { if err != nil {
feedback.Error(err)
os.Exit(errorcodes.ErrGeneric) os.Exit(errorcodes.ErrGeneric)
} }
} }
...@@ -157,8 +157,7 @@ func FirstUpdate(instance *rpc.Instance) error { ...@@ -157,8 +157,7 @@ func FirstUpdate(instance *rpc.Instance) error {
Instance: instance, Instance: instance,
IgnoreCustomPackageIndexes: true, IgnoreCustomPackageIndexes: true,
}, },
output.ProgressBar(), output.ProgressBar())
output.PrintErrorFromDownloadResult(tr("Error updating index")))
if err != nil { if err != nil {
return err return err
} }
......
...@@ -17,6 +17,7 @@ package output ...@@ -17,6 +17,7 @@ package output
import ( import (
"fmt" "fmt"
"sync"
"github.com/arduino/arduino-cli/cli/feedback" "github.com/arduino/arduino-cli/cli/feedback"
"github.com/arduino/arduino-cli/i18n" "github.com/arduino/arduino-cli/i18n"
...@@ -41,21 +42,6 @@ func ProgressBar() rpc.DownloadProgressCB { ...@@ -41,21 +42,6 @@ func ProgressBar() rpc.DownloadProgressCB {
} }
} }
// PrintErrorFromDownloadResult returns a DownloadResultCB that only prints
// the errors with the give message prefixed.
func PrintErrorFromDownloadResult(msg string) rpc.DownloadResultCB {
if OutputFormat != "json" {
return func(res *rpc.DownloadResult) {
if !res.GetSuccessful() {
feedback.Errorf("%s: %s", msg, res.GetError())
}
}
}
return func(res *rpc.DownloadResult) {
// XXX: Output result in JSON?
}
}
// TaskProgress returns a TaskProgressCB that prints the task progress. // TaskProgress returns a TaskProgressCB that prints the task progress.
// If JSON output format has been selected, the callback outputs nothing. // If JSON output format has been selected, the callback outputs nothing.
func TaskProgress() rpc.TaskProgressCB { func TaskProgress() rpc.TaskProgressCB {
...@@ -70,25 +56,39 @@ func TaskProgress() rpc.TaskProgressCB { ...@@ -70,25 +56,39 @@ func TaskProgress() rpc.TaskProgressCB {
// NewDownloadProgressBarCB creates a progress bar callback that outputs a progress // NewDownloadProgressBarCB creates a progress bar callback that outputs a progress
// bar on the terminal // bar on the terminal
func NewDownloadProgressBarCB() func(*rpc.DownloadProgress) { func NewDownloadProgressBarCB() func(*rpc.DownloadProgress) {
var mux sync.Mutex
var bar *pb.ProgressBar var bar *pb.ProgressBar
var prefix string var label string
started := false
return func(curr *rpc.DownloadProgress) { return func(curr *rpc.DownloadProgress) {
mux.Lock()
defer mux.Unlock()
// fmt.Printf(">>> %v\n", curr) // fmt.Printf(">>> %v\n", curr)
if filename := curr.GetFile(); filename != "" { if start := curr.GetStart(); start != nil {
if curr.GetCompleted() { label = start.GetLabel()
fmt.Println(tr("%s already downloaded", filename)) bar = pb.New(0)
return bar.Prefix(label)
}
prefix = filename
bar = pb.StartNew(int(curr.GetTotalSize()))
bar.Prefix(prefix)
bar.SetUnits(pb.U_BYTES) bar.SetUnits(pb.U_BYTES)
} }
if curr.GetDownloaded() != 0 { if update := curr.GetUpdate(); update != nil {
bar.Set(int(curr.GetDownloaded())) bar.SetTotal64(update.GetTotalSize())
if !started {
bar.Start()
started = true
}
bar.Set64(update.GetDownloaded())
} }
if curr.GetCompleted() { if end := curr.GetEnd(); end != nil {
bar.FinishPrintOver(tr("%s downloaded", prefix)) msg := end.GetMessage()
if end.GetSuccess() && msg == "" {
msg = tr("downloaded")
}
if started {
bar.FinishPrintOver(label + " " + msg)
} else {
feedback.Print(label + " " + msg)
}
} }
} }
} }
......
...@@ -56,9 +56,9 @@ func runUpdateCommand(cmd *cobra.Command, args []string) { ...@@ -56,9 +56,9 @@ func runUpdateCommand(cmd *cobra.Command, args []string) {
inst := instance.CreateInstanceAndRunFirstUpdate() inst := instance.CreateInstanceAndRunFirstUpdate()
logrus.Info("Executing `arduino-cli update`") logrus.Info("Executing `arduino-cli update`")
err := commands.UpdateCoreLibrariesIndex(context.Background(), &rpc.UpdateCoreLibrariesIndexRequest{Instance: inst}, err := commands.UpdateCoreLibrariesIndex(context.Background(),
output.ProgressBar(), &rpc.UpdateCoreLibrariesIndexRequest{Instance: inst},
output.PrintErrorFromDownloadResult(tr("Error updating index"))) output.ProgressBar())
if err != nil { if err != nil {
feedback.Errorf(tr("Error updating core and libraries index: %v"), err) feedback.Errorf(tr("Error updating core and libraries index: %v"), err)
os.Exit(errorcodes.ErrGeneric) os.Exit(errorcodes.ErrGeneric)
......
...@@ -162,12 +162,7 @@ func (s *ArduinoCoreServerImpl) Destroy(ctx context.Context, req *rpc.DestroyReq ...@@ -162,12 +162,7 @@ func (s *ArduinoCoreServerImpl) Destroy(ctx context.Context, req *rpc.DestroyReq
// UpdateIndex FIXMEDOC // UpdateIndex FIXMEDOC
func (s *ArduinoCoreServerImpl) UpdateIndex(req *rpc.UpdateIndexRequest, stream rpc.ArduinoCoreService_UpdateIndexServer) error { func (s *ArduinoCoreServerImpl) UpdateIndex(req *rpc.UpdateIndexRequest, stream rpc.ArduinoCoreService_UpdateIndexServer) error {
err := commands.UpdateIndex(stream.Context(), req, err := commands.UpdateIndex(stream.Context(), req,
func(p *rpc.DownloadProgress) { func(p *rpc.DownloadProgress) { stream.Send(&rpc.UpdateIndexResponse{DownloadProgress: p}) },
stream.Send(&rpc.UpdateIndexResponse{Message: &rpc.UpdateIndexResponse_DownloadProgress{DownloadProgress: p}})
},
func(r *rpc.DownloadResult) {
stream.Send(&rpc.UpdateIndexResponse{Message: &rpc.UpdateIndexResponse_DownloadResult{DownloadResult: r}})
},
) )
return convertErrorToRPCStatus(err) return convertErrorToRPCStatus(err)
} }
...@@ -187,7 +182,6 @@ func (s *ArduinoCoreServerImpl) UpdateLibrariesIndex(req *rpc.UpdateLibrariesInd ...@@ -187,7 +182,6 @@ func (s *ArduinoCoreServerImpl) UpdateLibrariesIndex(req *rpc.UpdateLibrariesInd
func (s *ArduinoCoreServerImpl) UpdateCoreLibrariesIndex(req *rpc.UpdateCoreLibrariesIndexRequest, stream rpc.ArduinoCoreService_UpdateCoreLibrariesIndexServer) error { func (s *ArduinoCoreServerImpl) UpdateCoreLibrariesIndex(req *rpc.UpdateCoreLibrariesIndexRequest, stream rpc.ArduinoCoreService_UpdateCoreLibrariesIndexServer) error {
err := commands.UpdateCoreLibrariesIndex(stream.Context(), req, err := commands.UpdateCoreLibrariesIndex(stream.Context(), req,
func(p *rpc.DownloadProgress) { stream.Send(&rpc.UpdateCoreLibrariesIndexResponse{DownloadProgress: p}) }, func(p *rpc.DownloadProgress) { stream.Send(&rpc.UpdateCoreLibrariesIndexResponse{DownloadProgress: p}) },
func(res *rpc.DownloadResult) { /* ignore */ },
) )
if err != nil { if err != nil {
return convertErrorToRPCStatus(err) return convertErrorToRPCStatus(err)
......
...@@ -19,7 +19,7 @@ import ( ...@@ -19,7 +19,7 @@ import (
"context" "context"
"fmt" "fmt"
"net/url" "net/url"
"os" "path/filepath"
"strings" "strings"
"sync" "sync"
...@@ -490,7 +490,7 @@ func UpdateLibrariesIndex(ctx context.Context, req *rpc.UpdateLibrariesIndexRequ ...@@ -490,7 +490,7 @@ func UpdateLibrariesIndex(ctx context.Context, req *rpc.UpdateLibrariesIndexRequ
} }
// UpdateIndex FIXMEDOC // UpdateIndex FIXMEDOC
func UpdateIndex(ctx context.Context, req *rpc.UpdateIndexRequest, downloadCB rpc.DownloadProgressCB, downloadResultCB rpc.DownloadResultCB) error { func UpdateIndex(ctx context.Context, req *rpc.UpdateIndexRequest, downloadCB rpc.DownloadProgressCB) error {
if instances.GetInstance(req.GetInstance().GetId()) == nil { if instances.GetInstance(req.GetInstance().GetId()) == nil {
return &arduino.InvalidInstanceError{} return &arduino.InvalidInstanceError{}
} }
...@@ -504,14 +504,12 @@ func UpdateIndex(ctx context.Context, req *rpc.UpdateIndexRequest, downloadCB rp ...@@ -504,14 +504,12 @@ func UpdateIndex(ctx context.Context, req *rpc.UpdateIndexRequest, downloadCB rp
failed := false failed := false
for _, u := range urls { for _, u := range urls {
logrus.Info("URL: ", u)
URL, err := utils.URLParse(u) URL, err := utils.URLParse(u)
if err != nil { if err != nil {
logrus.Warnf("unable to parse additional URL: %s", u) logrus.Warnf("unable to parse additional URL: %s", u)
downloadResultCB(&rpc.DownloadResult{ msg := fmt.Sprintf("%s: %v", tr("Unable to parse URL"), err)
Url: u, downloadCB.Start(u, tr("Downloading index: %s", u))
Error: fmt.Sprintf("%s: %v", tr("Unable to parse URL"), err), downloadCB.End(false, msg)
})
failed = true failed = true
continue continue
} }
...@@ -519,49 +517,26 @@ func UpdateIndex(ctx context.Context, req *rpc.UpdateIndexRequest, downloadCB rp ...@@ -519,49 +517,26 @@ func UpdateIndex(ctx context.Context, req *rpc.UpdateIndexRequest, downloadCB rp
logrus.WithField("url", URL).Print("Updating index") logrus.WithField("url", URL).Print("Updating index")
if URL.Scheme == "file" { if URL.Scheme == "file" {
downloadCB.Start(u, tr("Downloading index: %s", filepath.Base(URL.Path)))
path := paths.New(URL.Path) path := paths.New(URL.Path)
if _, err := packageindex.LoadIndexNoSign(path); err != nil { if _, err := packageindex.LoadIndexNoSign(path); err != nil {
downloadResultCB(&rpc.DownloadResult{ msg := fmt.Sprintf("%s: %v", tr("Invalid package index in %s", path), err)
Url: u, downloadCB.End(false, msg)
Error: fmt.Sprintf("%s: %v", tr("Invalid package index in %s", path), err),
})
failed = true failed = true
continue } else {
downloadCB.End(true, "")
} }
fi, _ := os.Stat(path.String())
downloadCB(&rpc.DownloadProgress{
File: tr("Downloading index: %s", path.Base()),
TotalSize: fi.Size(),
})
downloadCB(&rpc.DownloadProgress{Completed: true})
downloadResultCB(&rpc.DownloadResult{
Url: u,
Successful: true,
})
continue continue
} }
indexResource := resources.IndexResource{ indexResource := resources.IndexResource{URL: URL}
URL: URL,
}
if strings.HasSuffix(URL.Host, "arduino.cc") && strings.HasSuffix(URL.Path, ".json") { if strings.HasSuffix(URL.Host, "arduino.cc") && strings.HasSuffix(URL.Path, ".json") {
indexResource.SignatureURL, _ = url.Parse(u) // should not fail because we already parsed it indexResource.SignatureURL, _ = url.Parse(u) // should not fail because we already parsed it
indexResource.SignatureURL.Path += ".sig" indexResource.SignatureURL.Path += ".sig"
} }
if err := indexResource.Download(indexpath, downloadCB); err != nil { if err := indexResource.Download(indexpath, downloadCB); err != nil {
downloadResultCB(&rpc.DownloadResult{
Url: u,
Error: err.Error(),
})
failed = true failed = true
continue
} }
downloadResultCB(&rpc.DownloadResult{
Url: u,
Successful: true,
})
} }
if failed { if failed {
...@@ -571,17 +546,13 @@ func UpdateIndex(ctx context.Context, req *rpc.UpdateIndexRequest, downloadCB rp ...@@ -571,17 +546,13 @@ func UpdateIndex(ctx context.Context, req *rpc.UpdateIndexRequest, downloadCB rp
} }
// UpdateCoreLibrariesIndex updates both Cores and Libraries indexes // UpdateCoreLibrariesIndex updates both Cores and Libraries indexes
func UpdateCoreLibrariesIndex(ctx context.Context, req *rpc.UpdateCoreLibrariesIndexRequest, downloadCB rpc.DownloadProgressCB, downloadResultCB rpc.DownloadResultCB) error { func UpdateCoreLibrariesIndex(ctx context.Context, req *rpc.UpdateCoreLibrariesIndexRequest, downloadCB rpc.DownloadProgressCB) error {
err := UpdateIndex(ctx, &rpc.UpdateIndexRequest{ err := UpdateIndex(ctx, &rpc.UpdateIndexRequest{Instance: req.Instance}, downloadCB)
Instance: req.Instance,
}, downloadCB, downloadResultCB)
if err != nil { if err != nil {
return err return err
} }
err = UpdateLibrariesIndex(ctx, &rpc.UpdateLibrariesIndexRequest{ err = UpdateLibrariesIndex(ctx, &rpc.UpdateLibrariesIndexRequest{Instance: req.Instance}, downloadCB)
Instance: req.Instance,
}, downloadCB)
if err != nil { if err != nil {
return err return err
} }
......
...@@ -76,55 +76,96 @@ replacement. ...@@ -76,55 +76,96 @@ replacement.
### Breaking changes in UpdateIndex API (both gRPC and go-lang) ### Breaking changes in UpdateIndex API (both gRPC and go-lang)
The gRPC message `cc.arduino.cli.commands.v1.UpdateIndexResponse` has been changed from: The gRPC message `cc.arduino.cli.commands.v1.DownloadProgress` has been changed from:
``` ```
message UpdateIndexResponse { message DownloadProgress {
// Progress of the platforms index download. // URL of the download.
DownloadProgress download_progress = 1; string url = 1;
// The file being downloaded.
string file = 2;
// Total size of the file being downloaded.
int64 total_size = 3;
// Size of the downloaded portion of the file.
int64 downloaded = 4;
// Whether the download is complete.
bool completed = 5;
} }
``` ```
to to
``` ```
message UpdateIndexResponse { message DownloadProgress {
oneof message { oneof message {
// Progress of the platforms index download. DownloadProgressStart start = 1;
DownloadProgress download_progress = 1; DownloadProgressUpdate update = 2;
// Report of the index update downloads. DownloadProgressEnd end = 3;
DownloadResult download_result = 2;
} }
} }
message DownloadResult { message DownloadProgressStart {
// Index URL. // URL of the download.
string url = 1; string url = 1;
// Download result: true if successful, false if an error occurred. // The label to display on the progress bar.
bool successful = 2; string label = 2;
// Download error details.
string error = 3;
} }
message DownloadProgressUpdate {
// Size of the downloaded portion of the file.
int64 downloaded = 1;
// Total size of the file being downloaded.
int64 total_size = 2;
}
message DownloadProgressEnd {
// True if the download is successful
bool success = 1;
// Info or error message, depending on the value of 'success'. Some examples:
// "File xxx already downloaded" or "Connection timeout"
string message = 2;
}
```
The new message format allows a better handling of the progress update reports on downloads. Every download now will
report a sequence of message as follows:
```
DownloadProgressStart{url="https://...", label="Downloading package index..."}
DownloadProgressUpdate{downloaded=0, total_size=103928}
DownloadProgressUpdate{downloaded=29380, total_size=103928}
DownloadProgressUpdate{downloaded=69540, total_size=103928}
DownloadProgressEnd{success=true, message=""}
```
or if an error occurs:
```
DownloadProgressStart{url="https://...", label="Downloading package index..."}
DownloadProgressUpdate{downloaded=0, total_size=103928}
DownloadProgressEnd{success=false, message="Server closed connection"}
```
or if the file is already cached:
```
DownloadProgressStart{url="https://...", label="Downloading package index..."}
DownloadProgressEnd{success=true, message="Index already downloaded"}
``` ```
even if not strictly a breaking change it's worth noting that the detailed error message is now streamed in the About the go-lang API the following functions in `github.com/arduino/arduino-cli/commands`:
response. About the go-lang API the following functions in `github.com/arduino/arduino-cli/commands`:
```go ```go
func UpdateIndex(ctx context.Context, req *rpc.UpdateIndexRequest, downloadCB rpc.DownloadProgressCB) (*rpc.UpdateIndexResponse, error) { ... } func UpdateIndex(ctx context.Context, req *rpc.UpdateIndexRequest, downloadCB rpc.DownloadProgressCB) (*rpc.UpdateIndexResponse, error) { ... }
func UpdateCoreLibrariesIndex(ctx context.Context, req *rpc.UpdateCoreLibrariesIndexRequest, downloadCB rpc.DownloadProgressCB) error { ... }
``` ```
have changed their signature to: have changed their signature to:
```go ```go
func UpdateIndex(ctx context.Context, req *rpc.UpdateIndexRequest, downloadCB rpc.DownloadProgressCB, downloadResultCB rpc.DownloadResultCB) error { ... } func UpdateIndex(ctx context.Context, req *rpc.UpdateIndexRequest, downloadCB rpc.DownloadProgressCB, downloadResultCB rpc.DownloadResultCB) error { ... }
func UpdateCoreLibrariesIndex(ctx context.Context, req *rpc.UpdateCoreLibrariesIndexRequest, downloadCB rpc.DownloadProgressCB, downloadResultCB rpc.DownloadResultCB) error { ... }
``` ```
`UpdateIndex` do not return anymore the latest `UpdateIndexResponse` (it was always empty). Both `UpdateIndex` and `UpdateIndex` do not return anymore the latest `UpdateIndexResponse` (beacuse it was always empty).
`UpdateCoreLibrariesIndex` now accepts an `rpc.DownloadResultCB` to get download results, you can pass an empty callback
if you're not interested in the error details.
## 0.27.0 ## 0.27.0
......
...@@ -41,59 +41,38 @@ func TestDaemonCoreUpdateIndex(t *testing.T) { ...@@ -41,59 +41,38 @@ func TestDaemonCoreUpdateIndex(t *testing.T) {
` "http://downloads.arduino.cc/package_inexistent_index.json"]`) ` "http://downloads.arduino.cc/package_inexistent_index.json"]`)
require.NoError(t, err) require.NoError(t, err)
analyzeUpdateIndexClient := func(cl commands.ArduinoCoreService_UpdateIndexClient) (error, map[string]*commands.DownloadProgressEnd) {
analyzer := NewDownloadProgressAnalyzer(t)
for {
msg, err := cl.Recv()
// fmt.Println("DOWNLOAD>", msg)
if err == io.EOF {
return nil, analyzer.Results
}
if err != nil {
return err, analyzer.Results
}
require.NoError(t, err)
analyzer.Process(msg.GetDownloadProgress())
}
}
{ {
cl, err := grpcInst.UpdateIndex(context.Background(), true) cl, err := grpcInst.UpdateIndex(context.Background(), true)
require.NoError(t, err) require.NoError(t, err)
res, err := analyzeUpdateIndexStream(t, cl) err, res := analyzeUpdateIndexClient(cl)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, res, 1) require.Len(t, res, 1)
require.True(t, res["https://downloads.arduino.cc/packages/package_index.tar.bz2"].Successful) require.True(t, res["https://downloads.arduino.cc/packages/package_index.tar.bz2"].Success)
} }
{ {
cl, err := grpcInst.UpdateIndex(context.Background(), false) cl, err := grpcInst.UpdateIndex(context.Background(), false)
require.NoError(t, err) require.NoError(t, err)
res, err := analyzeUpdateIndexStream(t, cl) err, res := analyzeUpdateIndexClient(cl)
require.Error(t, err) require.Error(t, err)
require.Len(t, res, 3) require.Len(t, res, 3)
require.True(t, res["https://downloads.arduino.cc/packages/package_index.tar.bz2"].Successful) require.True(t, res["https://downloads.arduino.cc/packages/package_index.tar.bz2"].Success)
require.True(t, res["http://arduino.esp8266.com/stable/package_esp8266com_index.json"].Successful) require.True(t, res["http://arduino.esp8266.com/stable/package_esp8266com_index.json"].Success)
require.False(t, res["http://downloads.arduino.cc/package_inexistent_index.json"].Successful) require.False(t, res["http://downloads.arduino.cc/package_inexistent_index.json"].Success)
}
}
// analyzeUpdateIndexStream runs an update index checking if the sequence of DownloadProgress and
// DownloadResult messages is correct. It returns a map reporting all the DownloadResults messages
// received (it maps urls to DownloadResults).
func analyzeUpdateIndexStream(t *testing.T, cl commands.ArduinoCoreService_UpdateIndexClient) (map[string]*commands.DownloadResult, error) {
ongoingDownload := ""
results := map[string]*commands.DownloadResult{}
for {
msg, err := cl.Recv()
if err == io.EOF {
return results, nil
}
if err != nil {
return results, err
}
require.NoError(t, err)
fmt.Printf("UPDATE> %+v\n", msg)
if progress := msg.GetDownloadProgress(); progress != nil {
if progress.Url != "" && progress.Url != ongoingDownload {
require.Empty(t, ongoingDownload, "DownloadProgress: started a download without 'completing' the previous one")
ongoingDownload = progress.Url
}
if progress.Completed {
require.NotEmpty(t, ongoingDownload, "DownloadProgress: received a 'completed' notification but never initiated a download")
ongoingDownload = ""
}
if progress.Downloaded > 0 {
require.NotEmpty(t, ongoingDownload, "DownloadProgress: received a download update but never initiated a download")
}
} else if result := msg.GetDownloadResult(); result != nil {
require.Empty(t, ongoingDownload, "DownloadResult: got a download result without completing the current download first")
results[result.Url] = result
} else {
require.FailNow(t, "DownloadProgress: received an empty message (without a Progress or a Result)")
}
} }
} }
// This file is part of arduino-cli.
//
// Copyright 2022 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 daemon_test
import (
"testing"
"github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
"github.com/stretchr/testify/require"
)
// DownloadProgressAnalyzer analyzes DownloadProgress messages for consistency
type DownloadProgressAnalyzer struct {
t *testing.T
ongoingDownload string
Results map[string]*commands.DownloadProgressEnd
}
// NewDownloadProgressAnalyzer creates a new DownloadProgressAnalyzer
func NewDownloadProgressAnalyzer(t *testing.T) *DownloadProgressAnalyzer {
return &DownloadProgressAnalyzer{
t: t,
Results: map[string]*commands.DownloadProgressEnd{},
}
}
// Process the given DownloadProgress message. If inconsistencies are detected the
// test will fail.
func (a *DownloadProgressAnalyzer) Process(progress *commands.DownloadProgress) {
if progress == nil {
return
}
if start := progress.GetStart(); start != nil {
require.Empty(a.t, a.ongoingDownload, "DownloadProgressStart: started a download without 'completing' the previous one")
a.ongoingDownload = start.Url
} else if update := progress.GetUpdate(); update != nil {
require.NotEmpty(a.t, a.ongoingDownload, "DownloadProgressUpdate: received update, but the download is not yet started...")
} else if end := progress.GetEnd(); end != nil {
require.NotEmpty(a.t, a.ongoingDownload, "DownloadProgress: received a 'completed' notification but never initiated a download")
a.Results[a.ongoingDownload] = end
a.ongoingDownload = ""
} else {
require.FailNow(a.t, "DownloadProgress: received an empty DownloadProgress (without Start, Update or End)")
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -238,21 +238,8 @@ message UpdateIndexRequest { ...@@ -238,21 +238,8 @@ message UpdateIndexRequest {
} }
message UpdateIndexResponse { message UpdateIndexResponse {
oneof message { // Progress of the package index download.
// Progress of the platforms index download. DownloadProgress download_progress = 1;
DownloadProgress download_progress = 1;
// Report of the index update downloads.
DownloadResult download_result = 2;
}
}
message DownloadResult {
// Index URL.
string url = 1;
// Download result: true if successful, false if an error occurred.
bool successful = 2;
// Download error details.
string error = 3;
} }
message UpdateLibrariesIndexRequest { message UpdateLibrariesIndexRequest {
......
...@@ -18,8 +18,41 @@ package commands ...@@ -18,8 +18,41 @@ package commands
// DownloadProgressCB is a callback to get updates on download progress // DownloadProgressCB is a callback to get updates on download progress
type DownloadProgressCB func(curr *DownloadProgress) type DownloadProgressCB func(curr *DownloadProgress)
// DownloadResultCB is a callback to get the result of a download // Start sends a "start" DownloadProgress message to the callback function
type DownloadResultCB func(res *DownloadResult) func (d DownloadProgressCB) Start(url, label string) {
d(&DownloadProgress{
Message: &DownloadProgress_Start{
Start: &DownloadProgressStart{
Url: url,
Label: label,
},
},
})
}
// Update sends an "update" DownloadProgress message to the callback function
func (d DownloadProgressCB) Update(downloaded int64, totalSize int64) {
d(&DownloadProgress{
Message: &DownloadProgress_Update{
Update: &DownloadProgressUpdate{
Downloaded: downloaded,
TotalSize: totalSize,
},
},
})
}
// End sends an "end" DownloadProgress message to the callback function
func (d DownloadProgressCB) End(success bool, message string) {
d(&DownloadProgress{
Message: &DownloadProgress_End{
End: &DownloadProgressEnd{
Success: success,
Message: message,
},
},
})
}
// TaskProgressCB is a callback to receive progress messages // TaskProgressCB is a callback to receive progress messages
type TaskProgressCB func(msg *TaskProgress) type TaskProgressCB func(msg *TaskProgress)
......
This diff is collapsed.
...@@ -25,16 +25,33 @@ message Instance { ...@@ -25,16 +25,33 @@ message Instance {
} }
message DownloadProgress { message DownloadProgress {
oneof message {
DownloadProgressStart start = 1;
DownloadProgressUpdate update = 2;
DownloadProgressEnd end = 3;
}
}
message DownloadProgressStart {
// URL of the download. // URL of the download.
string url = 1; string url = 1;
// The file being downloaded. // The label to display on the progress bar.
string file = 2; string label = 2;
// Total size of the file being downloaded. }
int64 total_size = 3;
message DownloadProgressUpdate {
// Size of the downloaded portion of the file. // Size of the downloaded portion of the file.
int64 downloaded = 4; int64 downloaded = 1;
// Whether the download is complete. // Total size of the file being downloaded.
bool completed = 5; int64 total_size = 2;
}
message DownloadProgressEnd {
// True if the download is successful
bool success = 1;
// Info or error message, depending on the value of 'success'. Some examples:
// "File xxx already downloaded" or "Connection timeout"
string message = 2;
} }
message TaskProgress { message TaskProgress {
......
...@@ -94,8 +94,10 @@ def test_core_updateindex_url_not_found(run_command, httpserver): ...@@ -94,8 +94,10 @@ def test_core_updateindex_url_not_found(run_command, httpserver):
result = run_command(["core", "update-index", f"--additional-urls={url}"]) result = run_command(["core", "update-index", f"--additional-urls={url}"])
assert result.failed assert result.failed
lines = [l.strip() for l in result.stderr.splitlines()] linesout = [l.strip() for l in result.stdout.splitlines()]
assert f"Error updating index: Error downloading index '{url}': Server responded with: 404 NOT FOUND" in lines assert "Downloading index: test_index.json Server responded with: 404 NOT FOUND" in linesout
lineserr = [l.strip() for l in result.stderr.splitlines()]
assert "Some indexes could not be updated." in lineserr
def test_core_updateindex_internal_server_error(run_command, httpserver): def test_core_updateindex_internal_server_error(run_command, httpserver):
...@@ -107,11 +109,8 @@ def test_core_updateindex_internal_server_error(run_command, httpserver): ...@@ -107,11 +109,8 @@ def test_core_updateindex_internal_server_error(run_command, httpserver):
result = run_command(["core", "update-index", f"--additional-urls={url}"]) result = run_command(["core", "update-index", f"--additional-urls={url}"])
assert result.failed assert result.failed
lines = [l.strip() for l in result.stderr.splitlines()] lines = [l.strip() for l in result.stdout.splitlines()]
assert ( assert "Downloading index: test_index.json Server responded with: 500 INTERNAL SERVER ERROR" in lines
f"Error updating index: Error downloading index '{url}': Server responded with: 500 INTERNAL SERVER ERROR"
in lines
)
def test_core_install_without_updateindex(run_command): def test_core_install_without_updateindex(run_command):
......
...@@ -60,8 +60,8 @@ def test_update_with_url_not_found(run_command, httpserver): ...@@ -60,8 +60,8 @@ def test_update_with_url_not_found(run_command, httpserver):
res = run_command(["update", f"--additional-urls={url}"]) res = run_command(["update", f"--additional-urls={url}"])
assert res.failed assert res.failed
lines = [l.strip() for l in res.stderr.splitlines()] lines = [l.strip() for l in res.stdout.splitlines()]
assert f"Error updating index: Error downloading index '{url}': Server responded with: 404 NOT FOUND" in lines assert "Downloading index: test_index.json Server responded with: 404 NOT FOUND" in lines
def test_update_with_url_internal_server_error(run_command, httpserver): def test_update_with_url_internal_server_error(run_command, httpserver):
...@@ -73,11 +73,8 @@ def test_update_with_url_internal_server_error(run_command, httpserver): ...@@ -73,11 +73,8 @@ def test_update_with_url_internal_server_error(run_command, httpserver):
res = run_command(["update", f"--additional-urls={url}"]) res = run_command(["update", f"--additional-urls={url}"])
assert res.failed assert res.failed
lines = [l.strip() for l in res.stderr.splitlines()] lines = [l.strip() for l in res.stdout.splitlines()]
assert ( assert "Downloading index: test_index.json Server responded with: 500 INTERNAL SERVER ERROR" in lines
f"Error updating index: Error downloading index '{url}': Server responded with: 500 INTERNAL SERVER ERROR"
in lines
)
def test_update_showing_outdated_using_library_with_invalid_version(run_command, data_dir): def test_update_showing_outdated_using_library_with_invalid_version(run_command, data_dir):
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment