Skip to content

gRPC: Added CheckForArduinoCLIUpdates RPC call #2573

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 [email protected].

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
Loading