Skip to content

Commit 8d50a1d

Browse files
committed
Externalize the Progress Bar output from the Download Logic
The progress bar output was embedded in the download logic, preventing a full reuse of the download logic itself. It is now outsourced and the download logic just has lifecycle handles to notify an external actor of the progress.
1 parent 703a579 commit 8d50a1d

File tree

7 files changed

+245
-53
lines changed

7 files changed

+245
-53
lines changed

commands/commands.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ package commands
3131

3232
import (
3333
"github.com/sirupsen/logrus"
34+
"github.com/bcmi-labs/arduino-cli/common/releases"
35+
"github.com/bcmi-labs/arduino-cli/common/formatter"
3436
)
3537

3638
// Error codes to be used for os.Exit().
@@ -53,3 +55,11 @@ var GlobalFlags struct {
5355
Debug bool // If true, dump debug output to stderr.
5456
Format string // The Output format (e.g. text, json).
5557
}
58+
59+
// FIXME: Move away? Where should the display logic reside; in the formatter? That causes an import cycle BTW...
60+
func GenerateDownloadProgressFormatter() releases.ParallelDownloadProgressHandler {
61+
if formatter.IsCurrentFormat("text") {
62+
return &ProgressBarFormatter{}
63+
}
64+
return nil
65+
}

commands/core/download.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ func downloadToolsArchives(tools []*cores.ToolRelease, results *output.CoreProce
9696
})
9797
}
9898
logrus.Info("Downloading tools")
99-
releases.ParallelDownload(downloads, false, "Downloaded", &results.Tools, "tool")
99+
releases.ParallelDownload(downloads, false, "Downloaded", &results.Tools, "tool", commands.GenerateDownloadProgressFormatter())
100100
}
101101

102102
func downloadPlatformArchives(platforms []*cores.PlatformRelease, results *output.CoreProcessResults) {
@@ -108,5 +108,5 @@ func downloadPlatformArchives(platforms []*cores.PlatformRelease, results *outpu
108108
})
109109
}
110110
logrus.Info("Downloading cores")
111-
releases.ParallelDownload(downloads, false, "Downloaded", &results.Cores, "core")
111+
releases.ParallelDownload(downloads, false, "Downloaded", &results.Cores, "core", commands.GenerateDownloadProgressFormatter())
112112
}

commands/lib/download.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ func runDownloadCommand(cmd *cobra.Command, args []string) {
7878
libs[i] = releases.DownloadItem(libsToDownload[i])
7979
}
8080
logrus.Info("Downloading")
81-
releases.ParallelDownload(libs, false, "Downloaded", &outputResults.Libraries, "library")
81+
releases.ParallelDownload(libs, false, "Downloaded", &outputResults.Libraries, "library", commands.GenerateDownloadProgressFormatter())
8282
logrus.Info("Download finished")
8383
formatter.Print(outputResults)
8484
logrus.Info("Done")

commands/lib/install.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ func runInstallCommand(cmd *cobra.Command, args []string) {
8080
}
8181

8282
logrus.Info("Downloading")
83-
releases.ParallelDownload(libs, false, "Installed", &outputResults.Libraries, "library")
83+
releases.ParallelDownload(libs, false, "Installed", &outputResults.Libraries, "library", commands.GenerateDownloadProgressFormatter())
8484
logrus.Info("Download finished")
8585

8686
logrus.Info("Installing")

commands/progress_formatter.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package commands
2+
3+
import (
4+
"gopkg.in/cheggaaa/pb.v1"
5+
"fmt"
6+
7+
"github.com/sirupsen/logrus"
8+
"github.com/bcmi-labs/arduino-cli/common/releases"
9+
)
10+
11+
// ProgressBarFormatter implements the visualization of the progress bars
12+
// to display the progress of a ParallelDownload task set (i.e. one bar per file)
13+
// WARNING: The implementation library is experimental and unstable; Do not print to terminal while the bars are active.
14+
type ProgressBarFormatter struct {
15+
progressBars map[string]*pb.ProgressBar
16+
progressBarsPool *pb.Pool
17+
}
18+
19+
// Implement interface releases.ParallelDownloadProgressHandler
20+
21+
func (pbf *ProgressBarFormatter) OnNewDownloadTask(fileName string, fileSize int64) releases.FileDownloadFilter {
22+
// Initialize progress bars and a new one for each the new task
23+
if pbf.progressBars == nil {
24+
pbf.progressBars = map[string]*pb.ProgressBar{}
25+
}
26+
27+
logrus.Debug(fmt.Sprintf("Initializing progress bar for file '%s'", fileName))
28+
29+
// Initialization is in bytes, to display full information about the file (not only the percentage)
30+
progressBar := pb.New64(fileSize).SetUnits(pb.U_BYTES).Prefix(fmt.Sprintf("%-20s", fileName)).Start()
31+
pbf.progressBars[fileName] = progressBar
32+
33+
// TODO: this was the legacy way to run the progress bar update; since the logic has been outsourced
34+
// and the OnProgressChanged callback is now available, it can be safely removed.
35+
/*return func(source io.Reader, initialData *os.File, initialSize int) (io.Reader, error) {
36+
logrus.Info(fmt.Sprintf("Initialized progress bar for file '%s'", fileName))
37+
38+
progressBar.Add(int(initialSize))
39+
return progressBar.NewProxyReader(source), nil
40+
}*/
41+
return nil
42+
}
43+
44+
func (pbf *ProgressBarFormatter) OnProgressChanged(fileName string, fileSize int64, downloadedSoFar int64) {
45+
// Update a specific file's progress bar
46+
progressBar := pbf.progressBars[fileName]
47+
48+
if progressBar != nil {
49+
progressBar.Set(int(downloadedSoFar))
50+
} else {
51+
logrus.Debug(fmt.Sprintf("Progress bar for file '%s' not found", fileName))
52+
}
53+
}
54+
55+
func (pbf *ProgressBarFormatter) OnDownloadStarted() {
56+
// WARNING!!
57+
// (experimental and unstable) Do not print to terminal while pool is active.
58+
// See https://github.com/cheggaaa/pb#multiple-progress-bars-experimental-and-unstable
59+
60+
// Start the progress bar pool
61+
progressBarsAsSlice := []*pb.ProgressBar{}
62+
for _, value := range pbf.progressBars {
63+
progressBarsAsSlice = append(progressBarsAsSlice, value)
64+
}
65+
pbf.progressBarsPool, _ = pb.StartPool(progressBarsAsSlice...)
66+
}
67+
68+
func (pbf *ProgressBarFormatter) OnDownloadFinished() {
69+
// Stop the progress bar pool
70+
if pbf.progressBarsPool != nil {
71+
pbf.progressBarsPool.Stop()
72+
}
73+
}
74+
75+
// END -- Implement interface releases.ParallelDownloadProgressHandler

common/net_functions.go

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,32 @@ func DownloadIndex(indexPath pathutils.Path, URL string) error {
7575
return nil
7676
}
7777

78-
// DownloadPackage downloads a package from arduino repository.
79-
func DownloadPackage(URL string, initialData *os.File, totalSize int64, handleResultFunc func(io.Reader, *os.File, int) error) error {
78+
// HandleResultFunc defines a function able to handle the content of the
79+
// download stream of the package (DownloadPackage), filling the File with the content
80+
// of the Reader, starting from the initial position
81+
type HandleDownloadPackageResultFunc func(io.Reader, *os.File, int) error
82+
83+
// DefaultDownloadHandlerFunc is the default HandleDownloadPackageResultFunc, which
84+
// simply copies the content of the Reader into the File, starting from the initialSize
85+
func DefaultDownloadHandlerFunc(source io.Reader, initialData *os.File, initialSize int) error {
86+
// Copy the file content
87+
_, err := io.Copy(initialData, source)
88+
return err
89+
}
90+
91+
// DownloadPackageProgressChangedHandler defines a function able to handle the update
92+
// of the progress of the current download
93+
type DownloadPackageProgressChangedHandler func(fileSize int64, downloadedSoFar int64)
94+
95+
// DownloadPackage downloads a package from Arduino repository.
96+
// Besides the download information (URL, initialData and totalSize), two external handlers are available for:
97+
// - (handleResultFunc) handling the result of the download (i.e. decide how to copy the download to the file
98+
// or do something weird during the operation)
99+
// - (progressChangedHandler) handling the download progress change (and perhaps display it somehow)
100+
// None of the handlers is mandatory; they won't be used if nil.
101+
func DownloadPackage(URL string, initialData *os.File, totalSize int64, handleResultFunc HandleDownloadPackageResultFunc,
102+
progressChangedHandler DownloadPackageProgressChangedHandler) error {
103+
80104
if initialData == nil {
81105
return errors.New("Cannot fill a nil file pointer")
82106
}
@@ -120,13 +144,51 @@ func DownloadPackage(URL string, initialData *os.File, totalSize int64, handleRe
120144
}
121145
defer response.Body.Close()
122146

147+
// Handle the progress update handler, by creating a ProgressProxyReader;
148+
// only if it's needed (i.e. we actually have an external progressChangedHandler)
149+
progressProxyReader := response.Body
150+
downloadedSoFar := initialSize
151+
if progressChangedHandler != nil {
152+
progressProxyReader = ProgressProxyReader{response.Body, func(progressDelta int64) {
153+
// WARNING: This is using a closure on downloadedSoFar!
154+
downloadedSoFar += progressDelta
155+
progressChangedHandler(totalSize, downloadedSoFar)
156+
},
157+
}
158+
}
159+
160+
// Use the external handleResultFunc, if available, or the default one otherwise
123161
if handleResultFunc == nil {
124-
_, err = io.Copy(initialData, response.Body)
125-
} else {
126-
err = handleResultFunc(response.Body, initialData, int(initialSize))
162+
handleResultFunc = DefaultDownloadHandlerFunc
127163
}
164+
165+
err = handleResultFunc(progressProxyReader, initialData, int(initialSize))
128166
if err != nil {
129167
return fmt.Errorf("Cannot read response body from %s : %s", URL, err)
130168
}
131169
return nil
132170
}
171+
172+
// FIXME: Move outside? perhaps in commons?
173+
// HandleProgressUpdateFunc defines a function able to handle the progressDelta, in bytes
174+
type HandleProgressUpdateFunc func(progressDelta int64)
175+
176+
// It's proxy reader, intercepting reads to post progress updates, implement io.Reader
177+
type ProgressProxyReader struct {
178+
io.Reader
179+
handleProgressUpdateFunc HandleProgressUpdateFunc
180+
}
181+
182+
func (r ProgressProxyReader) Read(p []byte) (n int, err error) {
183+
n, err = r.Reader.Read(p)
184+
r.handleProgressUpdateFunc(int64(n))
185+
return
186+
}
187+
188+
// Close the reader when it implements io.Closer
189+
func (r ProgressProxyReader) Close() (err error) {
190+
if closer, ok := r.Reader.(io.Closer); ok {
191+
return closer.Close()
192+
}
193+
return
194+
}

0 commit comments

Comments
 (0)