Skip to content
Open
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
8 changes: 7 additions & 1 deletion cmd/arduino-app-cli/system/system.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,13 @@ func newUpdateCmd() *cobra.Command {

events := updater.Subscribe()
for event := range events {
feedback.Printf("[%s] %s", event.Type.String(), event.Data)
if event.Type == update.ErrorEvent {
// TODO: add colors to error messages
err := event.GetError()
feedback.Printf("Error: %s [%s]", err.Error(), update.GetUpdateErrorCode(err))
} else {
feedback.Printf("[%s] %s", event.Type.String(), event.GetData())
}

if event.Type == update.DoneEvent {
break
Expand Down
56 changes: 39 additions & 17 deletions internal/api/handlers/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
package handlers

import (
"errors"
"net/http"
"strings"

Expand All @@ -43,14 +42,19 @@ func HandleCheckUpgradable(updater *update.Manager) http.HandlerFunc {

pkgs, err := updater.ListUpgradablePackages(r.Context(), filterFunc)
if err != nil {
if errors.Is(err, update.ErrOperationAlreadyInProgress) {
render.EncodeResponse(w, http.StatusConflict, models.ErrorResponse{Details: err.Error()})
code := update.GetUpdateErrorCode(err)
if code == update.ErrOperationAlreadyInProgress.Code {
render.EncodeResponse(w, http.StatusConflict, models.ErrorResponse{
Code: string(code),
Details: err.Error(),
})
return
}
render.EncodeResponse(w, http.StatusBadRequest, models.ErrorResponse{Details: "Error checking for upgradable packages: " + err.Error()})
return
render.EncodeResponse(w, http.StatusBadRequest, models.ErrorResponse{
Code: string(code),
Details: err.Error(),
})
}

if len(pkgs) == 0 {
render.EncodeResponse(w, http.StatusNoContent, nil)
return
Expand Down Expand Up @@ -79,27 +83,40 @@ func HandleUpdateApply(updater *update.Manager) http.HandlerFunc {

pkgs, err := updater.ListUpgradablePackages(r.Context(), filterFunc)
if err != nil {
if errors.Is(err, update.ErrOperationAlreadyInProgress) {
render.EncodeResponse(w, http.StatusConflict, models.ErrorResponse{Details: err.Error()})
code := update.GetUpdateErrorCode(err)
if code == update.ErrOperationAlreadyInProgress.Code {
render.EncodeResponse(w, http.StatusConflict, models.ErrorResponse{
Code: string(code),
Details: err.Error(),
})
return
}
slog.Error("Unable to get upgradable packages", slog.String("error", err.Error()))
render.EncodeResponse(w, http.StatusInternalServerError, models.ErrorResponse{Details: "Error checking for upgradable packages"})
render.EncodeResponse(w, http.StatusInternalServerError, models.ErrorResponse{
Code: string(code),
Details: err.Error(),
})
return
}

if len(pkgs) == 0 {
render.EncodeResponse(w, http.StatusNoContent, models.ErrorResponse{Details: "System is up to date, no upgradable packages found"})
render.EncodeResponse(w, http.StatusNoContent, nil)
return
}

err = updater.UpgradePackages(r.Context(), pkgs)
if err != nil {
if errors.Is(err, update.ErrOperationAlreadyInProgress) {
render.EncodeResponse(w, http.StatusConflict, models.ErrorResponse{Details: err.Error()})
code := update.GetUpdateErrorCode(err)
if code == update.ErrOperationAlreadyInProgress.Code {
render.EncodeResponse(w, http.StatusConflict, models.ErrorResponse{
Code: string(code),
Details: err.Error(),
})
return
}
render.EncodeResponse(w, http.StatusInternalServerError, models.ErrorResponse{Details: "Error upgrading packages"})
render.EncodeResponse(w, http.StatusInternalServerError, models.ErrorResponse{
Code: string(code),
Details: err.Error(),
})
return
}

Expand Down Expand Up @@ -128,14 +145,19 @@ func HandleUpdateEvents(updater *update.Manager) http.HandlerFunc {
return
}
if event.Type == update.ErrorEvent {
err := event.GetError()
code := render.InternalServiceErr
if c := update.GetUpdateErrorCode(err); c != update.UnknownError {
code = render.SSEErrCode(string(c))
}
sseStream.SendError(render.SSEErrorData{
Code: render.InternalServiceErr,
Message: event.Data,
Code: code,
Message: err.Error(),
})
} else {
sseStream.Send(render.SSEEvent{
Type: event.Type.String(),
Data: event.Data,
Data: event.GetData(),
})
}

Expand Down
1 change: 1 addition & 0 deletions internal/api/models/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@
package models

type ErrorResponse struct {
Code string `json:"code,omitempty"`
Details string `json:"details"`
}
59 changes: 17 additions & 42 deletions internal/update/apt/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import (
"regexp"
"strings"
"sync"
"time"

"github.com/arduino/go-paths-helper"
"go.bug.st/f"
Expand Down Expand Up @@ -84,79 +83,55 @@ func (s *Service) UpgradePackages(ctx context.Context, names []string) (<-chan u
defer s.lock.Unlock()
defer close(eventsCh)

ctx, cancel := context.WithTimeout(ctx, 10*time.Minute)
defer cancel()

eventsCh <- update.Event{Type: update.StartEvent, Data: "Upgrade is starting"}
eventsCh <- update.NewDataEvent(update.StartEvent, "Upgrade is starting")
stream := runUpgradeCommand(ctx, names)
for line, err := range stream {
if err != nil {
eventsCh <- update.Event{
Type: update.ErrorEvent,
Err: err,
Data: "Error running upgrade command",
}
slog.Error("error processing upgrade command output", "error", err)
eventsCh <- update.NewErrorEvent(fmt.Errorf("error running upgrade command: %w", err))
return
}
eventsCh <- update.Event{Type: update.UpgradeLineEvent, Data: line}
eventsCh <- update.NewDataEvent(update.UpgradeLineEvent, line)
}
eventsCh <- update.Event{Type: update.StartEvent, Data: "apt cleaning cache is starting"}

eventsCh <- update.NewDataEvent(update.StartEvent, "apt cleaning cache is starting")
for line, err := range runAptCleanCommand(ctx) {
if err != nil {
eventsCh <- update.Event{
Type: update.ErrorEvent,
Err: err,
Data: "Error running apt clean command",
}
slog.Error("error processing apt clean command output", "error", err)
eventsCh <- update.NewErrorEvent(fmt.Errorf("error running apt clean command: %w", err))
return
}
eventsCh <- update.Event{Type: update.UpgradeLineEvent, Data: line}
eventsCh <- update.NewDataEvent(update.UpgradeLineEvent, line)
}
// TEMPORARY PATCH: stopping and destroying docker containers and images since IDE does not implement it yet.
// TODO: Remove this workaround once IDE implements it.
// Tracking issue: https://github.com/arduino/arduino-app-cli/issues/623
eventsCh <- update.Event{Type: update.UpgradeLineEvent, Data: "Stop and destroy docker containers and images ..."}

eventsCh <- update.NewDataEvent(update.UpgradeLineEvent, "Apt upgrade completed successfully.")
streamCleanup := cleanupDockerContainers(ctx)
for line, err := range streamCleanup {
if err != nil {
// TODO: maybe we should retun an error or a better feedback to the user?
// currently, we just log the error and continue considenring not blocking
slog.Error("Error stopping and destroying docker containers", "error", err)
slog.Warn("Error stopping and destroying docker containers", "error", err)
} else {
eventsCh <- update.NewDataEvent(update.UpgradeLineEvent, line)
}
eventsCh <- update.Event{Type: update.UpgradeLineEvent, Data: line}
}

// TEMPORARY PATCH: Install the latest docker images and show the logs to the users.
// TODO: Remove this workaround once docker image versions are no longer hardcoded in arduino-app-cli.
// Tracking issue: https://github.com/arduino/arduino-app-cli/issues/600
// Currently, we need to launch `arduino-app-cli system init` to pull the latest docker images because
// the version of the docker images are hardcoded in the (new downloaded) version of the arduino-app-cli.
eventsCh <- update.Event{Type: update.UpgradeLineEvent, Data: "Pulling the latest docker images ..."}
eventsCh <- update.NewDataEvent(update.UpgradeLineEvent, "Pulling the latest docker images ...")
streamDocker := pullDockerImages(ctx)
for line, err := range streamDocker {
if err != nil {
eventsCh <- update.Event{
Type: update.ErrorEvent,
Err: err,
Data: "Error upgrading docker images",
}
slog.Error("error upgrading docker images", "error", err)
eventsCh <- update.NewErrorEvent(fmt.Errorf("error pulling docker images: %w", err))
return
}
eventsCh <- update.Event{Type: update.UpgradeLineEvent, Data: line}
eventsCh <- update.NewDataEvent(update.UpgradeLineEvent, line)
}
eventsCh <- update.Event{Type: update.RestartEvent, Data: "Upgrade completed. Restarting ..."}
eventsCh <- update.NewDataEvent(update.RestartEvent, "Upgrade completed. Restarting ...")

err := restartServices(ctx)
if err != nil {
eventsCh <- update.Event{
Type: update.ErrorEvent,
Err: err,
Data: "Error restart services after upgrade",
}
slog.Error("failed to restart services", "error", err)
eventsCh <- update.NewErrorEvent(fmt.Errorf("error restarting services after upgrade: %w", err))
return
}
}()
Expand Down
62 changes: 14 additions & 48 deletions internal/update/arduino/arduino.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ package arduino
import (
"context"
"errors"
"fmt"
"log/slog"
"sync"
"time"

"github.com/arduino/arduino-cli/commands"
"github.com/arduino/arduino-cli/commands/cmderrors"
Expand Down Expand Up @@ -134,42 +134,31 @@ func (a *ArduinoPlatformUpdater) UpgradePackages(ctx context.Context, names []st
downloadProgressCB := func(curr *rpc.DownloadProgress) {
data := helpers.ArduinoCLIDownloadProgressToString(curr)
slog.Debug("Download progress", slog.String("download_progress", data))
eventsCh <- update.Event{Type: update.UpgradeLineEvent, Data: data}
eventsCh <- update.NewDataEvent(update.UpgradeLineEvent, data)
}
taskProgressCB := func(msg *rpc.TaskProgress) {
data := helpers.ArduinoCLITaskProgressToString(msg)
slog.Debug("Task progress", slog.String("task_progress", data))
eventsCh <- update.Event{Type: update.UpgradeLineEvent, Data: data}
eventsCh <- update.NewDataEvent(update.UpgradeLineEvent, data)
}

go func() {
defer a.lock.Unlock()
defer close(eventsCh)

ctx, cancel := context.WithTimeout(ctx, 10*time.Minute)
defer cancel()

eventsCh <- update.Event{Type: update.StartEvent, Data: "Upgrade is starting"}
eventsCh <- update.NewDataEvent(update.StartEvent, "Upgrade is starting")

logrus.SetLevel(logrus.ErrorLevel) // Reduce the log level of arduino-cli
srv := commands.NewArduinoCoreServer()

if err := setConfig(ctx, srv); err != nil {
eventsCh <- update.Event{
Type: update.ErrorEvent,
Err: err,
Data: "Error setting additional URLs",
}
eventsCh <- update.NewErrorEvent(fmt.Errorf("error setting config: %w", err))
return
}

var inst *rpc.Instance
if resp, err := srv.Create(ctx, &rpc.CreateRequest{}); err != nil {
eventsCh <- update.Event{
Type: update.ErrorEvent,
Err: err,
Data: "Error creating Arduino instance",
}
eventsCh <- update.NewErrorEvent(fmt.Errorf("error creating arduino-cli instance: %w", err))
return
} else {
inst = resp.GetInstance()
Expand All @@ -185,19 +174,11 @@ func (a *ArduinoPlatformUpdater) UpgradePackages(ctx context.Context, names []st
{
stream, _ := commands.UpdateIndexStreamResponseToCallbackFunction(ctx, downloadProgressCB)
if err := srv.UpdateIndex(&rpc.UpdateIndexRequest{Instance: inst}, stream); err != nil {
eventsCh <- update.Event{
Type: update.ErrorEvent,
Err: err,
Data: "Error updating index",
}
eventsCh <- update.NewErrorEvent(fmt.Errorf("error updating index: %w", err))
return
}
if err := srv.Init(&rpc.InitRequest{Instance: inst}, commands.InitStreamResponseToCallbackFunction(ctx, nil)); err != nil {
eventsCh <- update.Event{
Type: update.ErrorEvent,
Err: err,
Data: "Error initializing Arduino instance",
}
eventsCh <- update.NewErrorEvent(fmt.Errorf("error initializing instance: %w", err))
return
}
}
Expand All @@ -219,17 +200,13 @@ func (a *ArduinoPlatformUpdater) UpgradePackages(ctx context.Context, names []st
); err != nil {
var alreadyPresent *cmderrors.PlatformAlreadyAtTheLatestVersionError
if errors.As(err, &alreadyPresent) {
eventsCh <- update.Event{Type: update.UpgradeLineEvent, Data: alreadyPresent.Error()}
eventsCh <- update.NewDataEvent(update.UpgradeLineEvent, alreadyPresent.Error())
return
}

var notFound *cmderrors.PlatformNotFoundError
if !errors.As(err, &notFound) {
eventsCh <- update.Event{
Type: update.ErrorEvent,
Err: err,
Data: "Error upgrading platform",
}
eventsCh <- update.NewErrorEvent(fmt.Errorf("error upgrading platform: %w", err))
return
}
// If the platform is not found, we will try to install it
Expand All @@ -246,23 +223,16 @@ func (a *ArduinoPlatformUpdater) UpgradePackages(ctx context.Context, names []st
),
)
if err != nil {
eventsCh <- update.Event{
Type: update.ErrorEvent,
Err: err,
Data: "Error installing platform",
}
eventsCh <- update.NewErrorEvent(fmt.Errorf("error installing platform: %w", err))
return
}
} else if respCB().GetPlatform() == nil {
eventsCh <- update.Event{
Type: update.ErrorEvent,
Data: "platform upgrade failed",
}
eventsCh <- update.NewErrorEvent(fmt.Errorf("platform upgrade failed"))
return
}

cbw := orchestrator.NewCallbackWriter(func(line string) {
eventsCh <- update.Event{Type: update.UpgradeLineEvent, Data: line}
eventsCh <- update.NewDataEvent(update.UpgradeLineEvent, line)
})

err := srv.BurnBootloader(
Expand All @@ -274,11 +244,7 @@ func (a *ArduinoPlatformUpdater) UpgradePackages(ctx context.Context, names []st
commands.BurnBootloaderToServerStreams(ctx, cbw, cbw),
)
if err != nil {
eventsCh <- update.Event{
Type: update.ErrorEvent,
Err: err,
Data: "Error burning bootloader",
}
eventsCh <- update.NewErrorEvent(fmt.Errorf("error burning bootloader: %w", err))
return
}
}()
Expand Down
Loading