Skip to content

Commit cd6aad4

Browse files
committed
gRPC: Added CheckForArduinoCLIUpdates RPC call
1 parent af85d57 commit cd6aad4

File tree

8 files changed

+955
-712
lines changed

8 files changed

+955
-712
lines changed

Diff for: commands/daemon/daemon.go

+7
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"github.com/arduino/arduino-cli/commands/lib"
3232
"github.com/arduino/arduino-cli/commands/monitor"
3333
"github.com/arduino/arduino-cli/commands/sketch"
34+
"github.com/arduino/arduino-cli/commands/updatecheck"
3435
"github.com/arduino/arduino-cli/commands/upload"
3536
"github.com/arduino/arduino-cli/internal/i18n"
3637
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
@@ -583,6 +584,12 @@ func (s *ArduinoCoreServerImpl) Monitor(stream rpc.ArduinoCoreService_MonitorSer
583584
return nil
584585
}
585586

587+
// CheckForArduinoCLIUpdates FIXMEDOC
588+
func (s *ArduinoCoreServerImpl) CheckForArduinoCLIUpdates(ctx context.Context, req *rpc.CheckForArduinoCLIUpdatesRequest) (*rpc.CheckForArduinoCLIUpdatesResponse, error) {
589+
resp, err := updatecheck.CheckForArduinoCLIUpdates(ctx, req)
590+
return resp, convertErrorToRPCStatus(err)
591+
}
592+
586593
// CleanDownloadCacheDirectory FIXMEDOC
587594
func (s *ArduinoCoreServerImpl) CleanDownloadCacheDirectory(ctx context.Context, req *rpc.CleanDownloadCacheDirectoryRequest) (*rpc.CleanDownloadCacheDirectoryResponse, error) {
588595
resp, err := cache.CleanDownloadCacheDirectory(ctx, req)

Diff for: commands/updatecheck/check_for_updates.go

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// This file is part of arduino-cli.
2+
//
3+
// Copyright 2024 ARDUINO SA (http://www.arduino.cc/)
4+
//
5+
// This software is released under the GNU General Public License version 3,
6+
// which covers the main part of arduino-cli.
7+
// The terms of this license can be found at:
8+
// https://www.gnu.org/licenses/gpl-3.0.en.html
9+
//
10+
// You can be released from the requirements of the above licenses by purchasing
11+
// a commercial license. Buying such a license is mandatory if you want to
12+
// modify or otherwise use the software for commercial activities involving the
13+
// Arduino software without disclosing the source code of your own applications.
14+
// To purchase a commercial license, send an email to [email protected].
15+
16+
package updatecheck
17+
18+
import (
19+
"context"
20+
"strings"
21+
"time"
22+
23+
"github.com/arduino/arduino-cli/internal/arduino/httpclient"
24+
"github.com/arduino/arduino-cli/internal/cli/configuration"
25+
"github.com/arduino/arduino-cli/internal/cli/feedback"
26+
"github.com/arduino/arduino-cli/internal/inventory"
27+
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
28+
"github.com/arduino/arduino-cli/version"
29+
semver "go.bug.st/relaxed-semver"
30+
)
31+
32+
func CheckForArduinoCLIUpdates(ctx context.Context, req *rpc.CheckForArduinoCLIUpdatesRequest) (*rpc.CheckForArduinoCLIUpdatesResponse, error) {
33+
currentVersion, err := semver.Parse(version.VersionInfo.VersionString)
34+
if err != nil {
35+
return nil, err
36+
}
37+
38+
if !shouldCheckForUpdate(currentVersion) {
39+
return &rpc.CheckForArduinoCLIUpdatesResponse{}, nil
40+
}
41+
42+
defer func() {
43+
// Always save the last time we checked for updates at the end
44+
inventory.Store.Set("updater.last_check_time", time.Now())
45+
inventory.WriteStore()
46+
}()
47+
48+
latestVersion, err := semver.Parse(getLatestRelease())
49+
if err != nil {
50+
return nil, err
51+
}
52+
53+
if currentVersion.GreaterThanOrEqual(latestVersion) {
54+
// Current version is already good enough
55+
return &rpc.CheckForArduinoCLIUpdatesResponse{}, nil
56+
}
57+
58+
return &rpc.CheckForArduinoCLIUpdatesResponse{
59+
NewestVersion: latestVersion.String(),
60+
}, nil
61+
}
62+
63+
// shouldCheckForUpdate return true if it actually makes sense to check for new updates,
64+
// false in all other cases.
65+
func shouldCheckForUpdate(currentVersion *semver.Version) bool {
66+
if strings.Contains(currentVersion.String(), "git-snapshot") || strings.Contains(currentVersion.String(), "nightly") {
67+
// This is a dev build, no need to check for updates
68+
return false
69+
}
70+
71+
if !configuration.Settings.GetBool("updater.enable_notification") {
72+
// Don't check if the user disabled the notification
73+
return false
74+
}
75+
76+
if inventory.Store.IsSet("updater.last_check_time") && time.Since(inventory.Store.GetTime("updater.last_check_time")).Hours() < 24 {
77+
// Checked less than 24 hours ago, let's wait
78+
return false
79+
}
80+
81+
// Don't check when running on CI or on non interactive consoles
82+
return !feedback.IsCI() && feedback.IsInteractive() && feedback.HasConsole()
83+
}
84+
85+
// getLatestRelease queries the official Arduino download server for the latest release,
86+
// if there are no errors or issues a version string is returned, in all other case an empty string.
87+
func getLatestRelease() string {
88+
client, err := httpclient.New()
89+
if err != nil {
90+
return ""
91+
}
92+
93+
// We just use this URL to check if there's a new release available and
94+
// never show it to the user, so it's fine to use the Linux one for all OSs.
95+
URL := "https://downloads.arduino.cc/arduino-cli/arduino-cli_latest_Linux_64bit.tar.gz"
96+
res, err := client.Head(URL)
97+
if err != nil {
98+
// Yes, we ignore it
99+
return ""
100+
}
101+
102+
// Get redirected URL
103+
location := res.Request.URL.String()
104+
105+
// The location header points to the latest release of the CLI, it's supposed to be formatted like this:
106+
// https://downloads.arduino.cc/arduino-cli/arduino-cli_0.18.3_Linux_64bit.tar.gz
107+
// so we split it to get the version, if there are not enough splits something must have gone wrong.
108+
split := strings.Split(location, "_")
109+
if len(split) < 2 {
110+
return ""
111+
}
112+
113+
return split[1]
114+
}

Diff for: internal/cli/cli.go

+43-32
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@
1616
package cli
1717

1818
import (
19+
"context"
1920
"fmt"
2021
"io"
2122
"os"
2223
"strings"
2324

25+
"github.com/arduino/arduino-cli/commands/updatecheck"
2426
"github.com/arduino/arduino-cli/internal/cli/board"
2527
"github.com/arduino/arduino-cli/internal/cli/burnbootloader"
2628
"github.com/arduino/arduino-cli/internal/cli/cache"
@@ -44,6 +46,7 @@ import (
4446
"github.com/arduino/arduino-cli/internal/cli/version"
4547
"github.com/arduino/arduino-cli/internal/i18n"
4648
"github.com/arduino/arduino-cli/internal/inventory"
49+
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
4750
versioninfo "github.com/arduino/arduino-cli/version"
4851
"github.com/fatih/color"
4952
"github.com/mattn/go-colorable"
@@ -54,24 +57,54 @@ import (
5457
)
5558

5659
var (
57-
verbose bool
58-
outputFormat string
59-
configFile string
60-
updaterMessageChan chan *semver.Version = make(chan *semver.Version)
60+
verbose bool
61+
outputFormat string
62+
configFile string
6163
)
6264

6365
// NewCommand creates a new ArduinoCli command root
6466
func NewCommand() *cobra.Command {
6567
cobra.AddTemplateFunc("tr", i18n.Tr)
6668

69+
var updaterMessageChan chan *semver.Version
70+
6771
// ArduinoCli is the root command
6872
arduinoCli := &cobra.Command{
69-
Use: "arduino-cli",
70-
Short: tr("Arduino CLI."),
71-
Long: tr("Arduino Command Line Interface (arduino-cli)."),
72-
Example: fmt.Sprintf(" %s <%s> [%s...]", os.Args[0], tr("command"), tr("flags")),
73-
PersistentPreRun: preRun,
74-
PersistentPostRun: postRun,
73+
Use: "arduino-cli",
74+
Short: tr("Arduino CLI."),
75+
Long: tr("Arduino Command Line Interface (arduino-cli)."),
76+
Example: fmt.Sprintf(" %s <%s> [%s...]", os.Args[0], tr("command"), tr("flags")),
77+
PersistentPreRun: func(cmd *cobra.Command, args []string) {
78+
preRun(cmd, args)
79+
80+
if cmd.Name() != "version" {
81+
updaterMessageChan = make(chan *semver.Version)
82+
go func() {
83+
res, err := updatecheck.CheckForArduinoCLIUpdates(context.Background(), &rpc.CheckForArduinoCLIUpdatesRequest{})
84+
if err != nil {
85+
logrus.Warnf("Error checking for updates: %v", err)
86+
updaterMessageChan <- nil
87+
return
88+
}
89+
if v := res.GetNewestVersion(); v == "" {
90+
updaterMessageChan <- nil
91+
} else if latest, err := semver.Parse(v); err != nil {
92+
logrus.Warnf("Error parsing version: %v", err)
93+
} else {
94+
logrus.Infof("New version available: %s", v)
95+
updaterMessageChan <- latest
96+
}
97+
}()
98+
}
99+
},
100+
PersistentPostRun: func(cmd *cobra.Command, args []string) {
101+
if updaterMessageChan != nil {
102+
if latestVersion := <-updaterMessageChan; latestVersion != nil {
103+
// Notify the user a new version is available
104+
updater.NotifyNewVersionIsAvailable(latestVersion.String())
105+
}
106+
}
107+
},
75108
}
76109

77110
arduinoCli.SetUsageTemplate(getUsageTemplate())
@@ -160,20 +193,6 @@ func preRun(cmd *cobra.Command, args []string) {
160193
feedback.SetOut(colorable.NewColorableStdout())
161194
feedback.SetErr(colorable.NewColorableStderr())
162195

163-
updaterMessageChan = make(chan *semver.Version)
164-
go func() {
165-
if cmd.Name() == "version" {
166-
// The version command checks by itself if there's a new available version
167-
updaterMessageChan <- nil
168-
}
169-
// Starts checking for updates
170-
currentVersion, err := semver.Parse(versioninfo.VersionInfo.VersionString)
171-
if err != nil {
172-
updaterMessageChan <- nil
173-
}
174-
updaterMessageChan <- updater.CheckForUpdate(currentVersion)
175-
}()
176-
177196
//
178197
// Prepare logging
179198
//
@@ -251,11 +270,3 @@ func preRun(cmd *cobra.Command, args []string) {
251270
})
252271
}
253272
}
254-
255-
func postRun(cmd *cobra.Command, args []string) {
256-
latestVersion := <-updaterMessageChan
257-
if latestVersion != nil {
258-
// Notify the user a new version is available
259-
updater.NotifyNewVersionIsAvailable(latestVersion.String())
260-
}
261-
}

Diff for: internal/cli/updater/updater.go

-91
Original file line numberDiff line numberDiff line change
@@ -17,53 +17,15 @@ package updater
1717

1818
import (
1919
"fmt"
20-
"strings"
21-
"time"
2220

23-
"github.com/arduino/arduino-cli/internal/arduino/httpclient"
24-
"github.com/arduino/arduino-cli/internal/cli/configuration"
2521
"github.com/arduino/arduino-cli/internal/cli/feedback"
2622
"github.com/arduino/arduino-cli/internal/i18n"
27-
"github.com/arduino/arduino-cli/internal/inventory"
2823
"github.com/arduino/arduino-cli/version"
2924
"github.com/fatih/color"
30-
semver "go.bug.st/relaxed-semver"
3125
)
3226

3327
var tr = i18n.Tr
3428

35-
// CheckForUpdate returns the latest available version if greater than
36-
// the one running and it makes sense to check for an update, nil in all other cases
37-
func CheckForUpdate(currentVersion *semver.Version) *semver.Version {
38-
if !shouldCheckForUpdate(currentVersion) {
39-
return nil
40-
}
41-
42-
return ForceCheckForUpdate(currentVersion)
43-
}
44-
45-
// ForceCheckForUpdate always returns the latest available version if greater than
46-
// the one running, nil in all other cases
47-
func ForceCheckForUpdate(currentVersion *semver.Version) *semver.Version {
48-
defer func() {
49-
// Always save the last time we checked for updates at the end
50-
inventory.Store.Set("updater.last_check_time", time.Now())
51-
inventory.WriteStore()
52-
}()
53-
54-
latestVersion, err := semver.Parse(getLatestRelease())
55-
if err != nil {
56-
return nil
57-
}
58-
59-
if currentVersion.GreaterThanOrEqual(latestVersion) {
60-
// Current version is already good enough
61-
return nil
62-
}
63-
64-
return latestVersion
65-
}
66-
6729
// NotifyNewVersionIsAvailable prints information about the new latestVersion
6830
func NotifyNewVersionIsAvailable(latestVersion string) {
6931
msg := fmt.Sprintf("\n\n%s %s → %s\n%s",
@@ -73,56 +35,3 @@ func NotifyNewVersionIsAvailable(latestVersion string) {
7335
color.YellowString("https://arduino.github.io/arduino-cli/latest/installation/#latest-packages"))
7436
feedback.Warning(msg)
7537
}
76-
77-
// shouldCheckForUpdate return true if it actually makes sense to check for new updates,
78-
// false in all other cases.
79-
func shouldCheckForUpdate(currentVersion *semver.Version) bool {
80-
if strings.Contains(currentVersion.String(), "git-snapshot") || strings.Contains(currentVersion.String(), "nightly") {
81-
// This is a dev build, no need to check for updates
82-
return false
83-
}
84-
85-
if !configuration.Settings.GetBool("updater.enable_notification") {
86-
// Don't check if the user disabled the notification
87-
return false
88-
}
89-
90-
if inventory.Store.IsSet("updater.last_check_time") && time.Since(inventory.Store.GetTime("updater.last_check_time")).Hours() < 24 {
91-
// Checked less than 24 hours ago, let's wait
92-
return false
93-
}
94-
95-
// Don't check when running on CI or on non interactive consoles
96-
return !feedback.IsCI() && feedback.IsInteractive() && feedback.HasConsole()
97-
}
98-
99-
// getLatestRelease queries the official Arduino download server for the latest release,
100-
// if there are no errors or issues a version string is returned, in all other case an empty string.
101-
func getLatestRelease() string {
102-
client, err := httpclient.New()
103-
if err != nil {
104-
return ""
105-
}
106-
107-
// We just use this URL to check if there's a new release available and
108-
// never show it to the user, so it's fine to use the Linux one for all OSs.
109-
URL := "https://downloads.arduino.cc/arduino-cli/arduino-cli_latest_Linux_64bit.tar.gz"
110-
res, err := client.Head(URL)
111-
if err != nil {
112-
// Yes, we ignore it
113-
return ""
114-
}
115-
116-
// Get redirected URL
117-
location := res.Request.URL.String()
118-
119-
// The location header points to the latest release of the CLI, it's supposed to be formatted like this:
120-
// https://downloads.arduino.cc/arduino-cli/arduino-cli_0.18.3_Linux_64bit.tar.gz
121-
// so we split it to get the version, if there are not enough splits something must have gone wrong.
122-
split := strings.Split(location, "_")
123-
if len(split) < 2 {
124-
return ""
125-
}
126-
127-
return split[1]
128-
}

0 commit comments

Comments
 (0)