Skip to content

Commit

Permalink
gRPC: Added CheckForArduinoCLIUpdates RPC call (#2573)
Browse files Browse the repository at this point in the history
* gRPC: Added CheckForArduinoCLIUpdates RPC call

* Enable force check
  • Loading branch information
cmaglie committed Mar 26, 2024
1 parent af85d57 commit 2d66dfa
Show file tree
Hide file tree
Showing 8 changed files with 955 additions and 712 deletions.
7 changes: 7 additions & 0 deletions commands/daemon/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"github.com/arduino/arduino-cli/commands/lib"
"github.com/arduino/arduino-cli/commands/monitor"
"github.com/arduino/arduino-cli/commands/sketch"
"github.com/arduino/arduino-cli/commands/updatecheck"
"github.com/arduino/arduino-cli/commands/upload"
"github.com/arduino/arduino-cli/internal/i18n"
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
Expand Down Expand Up @@ -583,6 +584,12 @@ func (s *ArduinoCoreServerImpl) Monitor(stream rpc.ArduinoCoreService_MonitorSer
return nil
}

// CheckForArduinoCLIUpdates FIXMEDOC
func (s *ArduinoCoreServerImpl) CheckForArduinoCLIUpdates(ctx context.Context, req *rpc.CheckForArduinoCLIUpdatesRequest) (*rpc.CheckForArduinoCLIUpdatesResponse, error) {
resp, err := updatecheck.CheckForArduinoCLIUpdates(ctx, req)
return resp, convertErrorToRPCStatus(err)
}

// CleanDownloadCacheDirectory FIXMEDOC
func (s *ArduinoCoreServerImpl) CleanDownloadCacheDirectory(ctx context.Context, req *rpc.CleanDownloadCacheDirectoryRequest) (*rpc.CleanDownloadCacheDirectoryResponse, error) {
resp, err := cache.CleanDownloadCacheDirectory(ctx, req)
Expand Down
114 changes: 114 additions & 0 deletions commands/updatecheck/check_for_updates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// This file is part of arduino-cli.
//
// Copyright 2024 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 updatecheck

import (
"context"
"strings"
"time"

"github.com/arduino/arduino-cli/internal/arduino/httpclient"
"github.com/arduino/arduino-cli/internal/cli/configuration"
"github.com/arduino/arduino-cli/internal/cli/feedback"
"github.com/arduino/arduino-cli/internal/inventory"
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
"github.com/arduino/arduino-cli/version"
semver "go.bug.st/relaxed-semver"
)

func CheckForArduinoCLIUpdates(ctx context.Context, req *rpc.CheckForArduinoCLIUpdatesRequest) (*rpc.CheckForArduinoCLIUpdatesResponse, error) {
currentVersion, err := semver.Parse(version.VersionInfo.VersionString)
if err != nil {
return nil, err
}

if !shouldCheckForUpdate(currentVersion) && !req.GetForceCheck() {
return &rpc.CheckForArduinoCLIUpdatesResponse{}, nil
}

defer func() {
// Always save the last time we checked for updates at the end
inventory.Store.Set("updater.last_check_time", time.Now())
inventory.WriteStore()
}()

latestVersion, err := semver.Parse(getLatestRelease())
if err != nil {
return nil, err
}

if currentVersion.GreaterThanOrEqual(latestVersion) {
// Current version is already good enough
return &rpc.CheckForArduinoCLIUpdatesResponse{}, nil
}

return &rpc.CheckForArduinoCLIUpdatesResponse{
NewestVersion: latestVersion.String(),
}, nil
}

// shouldCheckForUpdate return true if it actually makes sense to check for new updates,
// false in all other cases.
func shouldCheckForUpdate(currentVersion *semver.Version) bool {
if strings.Contains(currentVersion.String(), "git-snapshot") || strings.Contains(currentVersion.String(), "nightly") {
// This is a dev build, no need to check for updates
return false
}

if !configuration.Settings.GetBool("updater.enable_notification") {
// Don't check if the user disabled the notification
return false
}

if inventory.Store.IsSet("updater.last_check_time") && time.Since(inventory.Store.GetTime("updater.last_check_time")).Hours() < 24 {
// Checked less than 24 hours ago, let's wait
return false
}

// Don't check when running on CI or on non interactive consoles
return !feedback.IsCI() && feedback.IsInteractive() && feedback.HasConsole()
}

// getLatestRelease queries the official Arduino download server for the latest release,
// if there are no errors or issues a version string is returned, in all other case an empty string.
func getLatestRelease() string {
client, err := httpclient.New()
if err != nil {
return ""
}

// We just use this URL to check if there's a new release available and
// never show it to the user, so it's fine to use the Linux one for all OSs.
URL := "https://downloads.arduino.cc/arduino-cli/arduino-cli_latest_Linux_64bit.tar.gz"
res, err := client.Head(URL)
if err != nil {
// Yes, we ignore it
return ""
}

// Get redirected URL
location := res.Request.URL.String()

// The location header points to the latest release of the CLI, it's supposed to be formatted like this:
// https://downloads.arduino.cc/arduino-cli/arduino-cli_0.18.3_Linux_64bit.tar.gz
// so we split it to get the version, if there are not enough splits something must have gone wrong.
split := strings.Split(location, "_")
if len(split) < 2 {
return ""
}

return split[1]
}
75 changes: 43 additions & 32 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@
package cli

import (
"context"
"fmt"
"io"
"os"
"strings"

"github.com/arduino/arduino-cli/commands/updatecheck"
"github.com/arduino/arduino-cli/internal/cli/board"
"github.com/arduino/arduino-cli/internal/cli/burnbootloader"
"github.com/arduino/arduino-cli/internal/cli/cache"
Expand All @@ -44,6 +46,7 @@ import (
"github.com/arduino/arduino-cli/internal/cli/version"
"github.com/arduino/arduino-cli/internal/i18n"
"github.com/arduino/arduino-cli/internal/inventory"
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
versioninfo "github.com/arduino/arduino-cli/version"
"github.com/fatih/color"
"github.com/mattn/go-colorable"
Expand All @@ -54,24 +57,54 @@ import (
)

var (
verbose bool
outputFormat string
configFile string
updaterMessageChan chan *semver.Version = make(chan *semver.Version)
verbose bool
outputFormat string
configFile string
)

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

var updaterMessageChan chan *semver.Version

// ArduinoCli is the root command
arduinoCli := &cobra.Command{
Use: "arduino-cli",
Short: tr("Arduino CLI."),
Long: tr("Arduino Command Line Interface (arduino-cli)."),
Example: fmt.Sprintf(" %s <%s> [%s...]", os.Args[0], tr("command"), tr("flags")),
PersistentPreRun: preRun,
PersistentPostRun: postRun,
Use: "arduino-cli",
Short: tr("Arduino CLI."),
Long: tr("Arduino Command Line Interface (arduino-cli)."),
Example: fmt.Sprintf(" %s <%s> [%s...]", os.Args[0], tr("command"), tr("flags")),
PersistentPreRun: func(cmd *cobra.Command, args []string) {
preRun(cmd, args)

if cmd.Name() != "version" {
updaterMessageChan = make(chan *semver.Version)
go func() {
res, err := updatecheck.CheckForArduinoCLIUpdates(context.Background(), &rpc.CheckForArduinoCLIUpdatesRequest{})
if err != nil {
logrus.Warnf("Error checking for updates: %v", err)
updaterMessageChan <- nil
return
}
if v := res.GetNewestVersion(); v == "" {
updaterMessageChan <- nil
} else if latest, err := semver.Parse(v); err != nil {
logrus.Warnf("Error parsing version: %v", err)
} else {
logrus.Infof("New version available: %s", v)
updaterMessageChan <- latest
}
}()
}
},
PersistentPostRun: func(cmd *cobra.Command, args []string) {
if updaterMessageChan != nil {
if latestVersion := <-updaterMessageChan; latestVersion != nil {
// Notify the user a new version is available
updater.NotifyNewVersionIsAvailable(latestVersion.String())
}
}
},
}

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

updaterMessageChan = make(chan *semver.Version)
go func() {
if cmd.Name() == "version" {
// The version command checks by itself if there's a new available version
updaterMessageChan <- nil
}
// Starts checking for updates
currentVersion, err := semver.Parse(versioninfo.VersionInfo.VersionString)
if err != nil {
updaterMessageChan <- nil
}
updaterMessageChan <- updater.CheckForUpdate(currentVersion)
}()

//
// Prepare logging
//
Expand Down Expand Up @@ -251,11 +270,3 @@ func preRun(cmd *cobra.Command, args []string) {
})
}
}

func postRun(cmd *cobra.Command, args []string) {
latestVersion := <-updaterMessageChan
if latestVersion != nil {
// Notify the user a new version is available
updater.NotifyNewVersionIsAvailable(latestVersion.String())
}
}
91 changes: 0 additions & 91 deletions internal/cli/updater/updater.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,53 +17,15 @@ package updater

import (
"fmt"
"strings"
"time"

"github.com/arduino/arduino-cli/internal/arduino/httpclient"
"github.com/arduino/arduino-cli/internal/cli/configuration"
"github.com/arduino/arduino-cli/internal/cli/feedback"
"github.com/arduino/arduino-cli/internal/i18n"
"github.com/arduino/arduino-cli/internal/inventory"
"github.com/arduino/arduino-cli/version"
"github.com/fatih/color"
semver "go.bug.st/relaxed-semver"
)

var tr = i18n.Tr

// CheckForUpdate returns the latest available version if greater than
// the one running and it makes sense to check for an update, nil in all other cases
func CheckForUpdate(currentVersion *semver.Version) *semver.Version {
if !shouldCheckForUpdate(currentVersion) {
return nil
}

return ForceCheckForUpdate(currentVersion)
}

// ForceCheckForUpdate always returns the latest available version if greater than
// the one running, nil in all other cases
func ForceCheckForUpdate(currentVersion *semver.Version) *semver.Version {
defer func() {
// Always save the last time we checked for updates at the end
inventory.Store.Set("updater.last_check_time", time.Now())
inventory.WriteStore()
}()

latestVersion, err := semver.Parse(getLatestRelease())
if err != nil {
return nil
}

if currentVersion.GreaterThanOrEqual(latestVersion) {
// Current version is already good enough
return nil
}

return latestVersion
}

// NotifyNewVersionIsAvailable prints information about the new latestVersion
func NotifyNewVersionIsAvailable(latestVersion string) {
msg := fmt.Sprintf("\n\n%s %s → %s\n%s",
Expand All @@ -73,56 +35,3 @@ func NotifyNewVersionIsAvailable(latestVersion string) {
color.YellowString("https://arduino.github.io/arduino-cli/latest/installation/#latest-packages"))
feedback.Warning(msg)
}

// shouldCheckForUpdate return true if it actually makes sense to check for new updates,
// false in all other cases.
func shouldCheckForUpdate(currentVersion *semver.Version) bool {
if strings.Contains(currentVersion.String(), "git-snapshot") || strings.Contains(currentVersion.String(), "nightly") {
// This is a dev build, no need to check for updates
return false
}

if !configuration.Settings.GetBool("updater.enable_notification") {
// Don't check if the user disabled the notification
return false
}

if inventory.Store.IsSet("updater.last_check_time") && time.Since(inventory.Store.GetTime("updater.last_check_time")).Hours() < 24 {
// Checked less than 24 hours ago, let's wait
return false
}

// Don't check when running on CI or on non interactive consoles
return !feedback.IsCI() && feedback.IsInteractive() && feedback.HasConsole()
}

// getLatestRelease queries the official Arduino download server for the latest release,
// if there are no errors or issues a version string is returned, in all other case an empty string.
func getLatestRelease() string {
client, err := httpclient.New()
if err != nil {
return ""
}

// We just use this URL to check if there's a new release available and
// never show it to the user, so it's fine to use the Linux one for all OSs.
URL := "https://downloads.arduino.cc/arduino-cli/arduino-cli_latest_Linux_64bit.tar.gz"
res, err := client.Head(URL)
if err != nil {
// Yes, we ignore it
return ""
}

// Get redirected URL
location := res.Request.URL.String()

// The location header points to the latest release of the CLI, it's supposed to be formatted like this:
// https://downloads.arduino.cc/arduino-cli/arduino-cli_0.18.3_Linux_64bit.tar.gz
// so we split it to get the version, if there are not enough splits something must have gone wrong.
split := strings.Split(location, "_")
if len(split) < 2 {
return ""
}

return split[1]
}
Loading

0 comments on commit 2d66dfa

Please sign in to comment.