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
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
@@ -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"
@@ -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)
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
@@ -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"
@@ -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"
@@ -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())
@@ -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
//
@@ -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
@@ -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",
@@ -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]
}
22 changes: 12 additions & 10 deletions internal/cli/version/version.go
Original file line number Diff line number Diff line change
@@ -16,17 +16,18 @@
package version

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

"github.com/arduino/arduino-cli/commands/updatecheck"
"github.com/arduino/arduino-cli/internal/cli/feedback"
"github.com/arduino/arduino-cli/internal/cli/updater"
"github.com/arduino/arduino-cli/internal/i18n"
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
"github.com/arduino/arduino-cli/version"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
semver "go.bug.st/relaxed-semver"
)

var tr = i18n.Tr
@@ -55,20 +56,21 @@ func runVersionCommand(cmd *cobra.Command, args []string) {
return
}

currentVersion, err := semver.Parse(info.VersionString)
latestVersion := ""
res, err := updatecheck.CheckForArduinoCLIUpdates(context.Background(), &rpc.CheckForArduinoCLIUpdatesRequest{})
if err != nil {
feedback.Fatal(fmt.Sprintf("Error parsing current version: %s", err), feedback.ErrGeneric)
feedback.Warning("Failed to check for updates: " + err.Error())
} else {
latestVersion = res.GetNewestVersion()
}
latestVersion := updater.CheckForUpdate(currentVersion)

if feedback.GetFormat() != feedback.Text && latestVersion != nil {
// Set this only we managed to get the latest version
info.LatestVersion = latestVersion.String()
if feedback.GetFormat() != feedback.Text {
info.LatestVersion = latestVersion
}

feedback.PrintResult(info)

if feedback.GetFormat() == feedback.Text && latestVersion != nil {
updater.NotifyNewVersionIsAvailable(latestVersion.String())
if feedback.GetFormat() == feedback.Text && latestVersion != "" {
updater.NotifyNewVersionIsAvailable(latestVersion)
}
}
1,303 changes: 724 additions & 579 deletions rpc/cc/arduino/cli/commands/v1/commands.pb.go

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions rpc/cc/arduino/cli/commands/v1/commands.proto
Original file line number Diff line number Diff line change
@@ -189,6 +189,10 @@ service ArduinoCoreService {
// Query the debugger information given a specific configuration.
rpc GetDebugConfig(GetDebugConfigRequest) returns (GetDebugConfigResponse) {}

// Check for updates to the Arduino CLI.
rpc CheckForArduinoCLIUpdates(CheckForArduinoCLIUpdatesRequest)
returns (CheckForArduinoCLIUpdatesResponse);

// Clean the download cache directory (where archives are downloaded).
rpc CleanDownloadCacheDirectory(CleanDownloadCacheDirectoryRequest)
returns (CleanDownloadCacheDirectoryResponse);
@@ -419,6 +423,18 @@ message SetSketchDefaultsResponse {
string default_programmer = 4;
}

message CheckForArduinoCLIUpdatesRequest {
// Force the check, even if the configuration says not to check for
// updates.
bool force_check = 1;
}

message CheckForArduinoCLIUpdatesResponse {
// The latest version of Arduino CLI available, if bigger than the
// current version.
string newest_version = 1;
}

message CleanDownloadCacheDirectoryRequest {
// The Arduino Core Service instance.
Instance instance = 1;
39 changes: 39 additions & 0 deletions rpc/cc/arduino/cli/commands/v1/commands_grpc.pb.go