Skip to content
Draft
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
55 changes: 39 additions & 16 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,20 @@ 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.OperationInProgressCode {
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()})
render.EncodeResponse(w, http.StatusBadRequest, models.ErrorResponse{
Code: string(code),
Details: err.Error(),
})
return
}

if len(pkgs) == 0 {
render.EncodeResponse(w, http.StatusNoContent, nil)
return
Expand Down Expand Up @@ -79,27 +84,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.OperationInProgressCode {
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.OperationInProgressCode {
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 +146,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.UnknownErrorCode {
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"`
}
97 changes: 40 additions & 57 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 @@ -74,94 +73,78 @@ func (s *Service) ListUpgradablePackages(ctx context.Context, matcher func(updat
// UpgradePackages upgrades the specified packages using the `apt-get upgrade` command.
// It publishes events to subscribers during the upgrade process.
// It returns an error if the upgrade is already in progress or if the upgrade command fails.
func (s *Service) UpgradePackages(ctx context.Context, names []string) (<-chan update.Event, error) {
func (s *Service) UpgradePackages(ctx context.Context, names []string) (iter.Seq[update.Event], error) {
if !s.lock.TryLock() {
return nil, update.ErrOperationAlreadyInProgress
}
eventsCh := make(chan update.Event, 100)

go func() {
return func(yield func(update.Event) bool) {
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"}
stream := runUpgradeCommand(ctx, names)
for line, err := range stream {
if !yield(update.NewDataEvent(update.StartEvent, "Upgrade is starting")) {
return
}
for line, err := range runUpgradeCommand(ctx, names) {
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)
_ = yield(update.NewErrorEvent(fmt.Errorf("error running upgrade command: %w", err)))
return
}
if !yield(update.NewDataEvent(update.UpgradeLineEvent, line)) {
return
}
eventsCh <- update.Event{Type: update.UpgradeLineEvent, Data: line}
}
eventsCh <- update.Event{Type: update.StartEvent, Data: "apt cleaning cache is starting"}

if !yield(update.NewDataEvent(update.StartEvent, "apt cleaning cache is starting")) {
return
}
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)
_ = yield(update.NewErrorEvent(fmt.Errorf("error running apt clean command: %w", err)))
return
}
eventsCh <- update.Event{Type: update.UpgradeLineEvent, Data: line}
if !yield(update.NewDataEvent(update.UpgradeLineEvent, line)) {
return
}
}

if !yield(update.NewDataEvent(update.UpgradeLineEvent, "Stop and destroy docker containers and images ....")) {
return
}
// 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 ..."}
streamCleanup := cleanupDockerContainers(ctx)
for line, err := range streamCleanup {
for line, err := range cleanupDockerContainers(ctx) {
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 if !yield(update.NewDataEvent(update.UpgradeLineEvent, line)) {
return
}
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 ..."}
streamDocker := pullDockerImages(ctx)
for line, err := range streamDocker {
if !yield(update.NewDataEvent(update.UpgradeLineEvent, "Pulling the latest docker images ...")) {
return
}
for line, err := range pullDockerImages(ctx) {
if err != nil {
eventsCh <- update.Event{
Type: update.ErrorEvent,
Err: err,
Data: "Error upgrading docker images",
}
slog.Error("error upgrading docker images", "error", err)
_ = yield(update.NewErrorEvent(fmt.Errorf("error pulling docker images: %w", err)))
return
}
eventsCh <- update.Event{Type: update.UpgradeLineEvent, Data: line}
}
eventsCh <- update.Event{Type: update.RestartEvent, Data: "Upgrade completed. Restarting ..."}

err := restartServices(ctx)
if err != nil {
eventsCh <- update.Event{
Type: update.ErrorEvent,
Err: err,
Data: "Error restart services after upgrade",
if !yield(update.NewDataEvent(update.UpgradeLineEvent, line)) {
return
}
slog.Error("failed to restart services", "error", err)
}
if !yield(update.NewDataEvent(update.RestartEvent, "Upgrade completed. Restarting ...")) {
return
}
}()

return eventsCh, nil
if err := restartServices(ctx); err != nil {
_ = yield(update.NewErrorEvent(fmt.Errorf("error restarting services after upgrade: %w", err)))
return
}
}, nil
}

// runDpkgConfigureCommand is need in case an upgrade was interrupted in the middle
Expand Down
Loading