Files
netbird/client/ui/update.go
2025-12-19 19:57:39 +01:00

141 lines
3.8 KiB
Go

//go:build !(linux && 386)
package main
import (
"context"
"errors"
"fmt"
"strings"
"time"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/proto"
)
func (s *serviceClient) showUpdateProgress(ctx context.Context, version string) {
log.Infof("show installer progress window: %s", version)
s.wUpdateProgress = s.app.NewWindow("Automatically updating client")
statusLabel := widget.NewLabel("Updating...")
infoLabel := widget.NewLabel(fmt.Sprintf("Your client version is older than the auto-update version set in Management.\nUpdating client to: %s.", version))
content := container.NewVBox(infoLabel, statusLabel)
s.wUpdateProgress.SetContent(content)
s.wUpdateProgress.CenterOnScreen()
s.wUpdateProgress.SetFixedSize(true)
s.wUpdateProgress.SetCloseIntercept(func() {
// this is empty to lock window until result known
})
s.wUpdateProgress.RequestFocus()
s.wUpdateProgress.Show()
updateWindowCtx, cancel := context.WithTimeout(ctx, 15*time.Minute)
// Initialize dot updater
updateText := dotUpdater()
// Channel to receive the result from RPC call
resultErrCh := make(chan error, 1)
resultOkCh := make(chan struct{}, 1)
// Start RPC call in background
go func() {
conn, err := s.getSrvClient(defaultFailTimeout)
if err != nil {
log.Infof("backend not reachable, upgrade in progress: %v", err)
close(resultOkCh)
return
}
resp, err := conn.GetInstallerResult(updateWindowCtx, &proto.InstallerResultRequest{})
if err != nil {
log.Infof("backend stopped responding, upgrade in progress: %v", err)
close(resultOkCh)
return
}
if !resp.Success {
resultErrCh <- mapInstallError(resp.ErrorMsg)
return
}
// Success
close(resultOkCh)
}()
// Update UI with dots and wait for result
go func() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
defer cancel()
// allow closing update window after 10 sec
timerResetCloseInterceptor := time.NewTimer(10 * time.Second)
defer timerResetCloseInterceptor.Stop()
for {
select {
case <-updateWindowCtx.Done():
s.showInstallerResult(statusLabel, updateWindowCtx.Err())
return
case err := <-resultErrCh:
s.showInstallerResult(statusLabel, err)
return
case <-resultOkCh:
log.Info("backend exited, upgrade in progress, closing all UI")
killParentUIProcess()
s.app.Quit()
return
case <-ticker.C:
statusLabel.SetText(updateText())
case <-timerResetCloseInterceptor.C:
s.wUpdateProgress.SetCloseIntercept(nil)
}
}
}()
}
func (s *serviceClient) showInstallerResult(statusLabel *widget.Label, err error) {
s.wUpdateProgress.SetCloseIntercept(nil)
switch {
case errors.Is(err, context.DeadlineExceeded):
log.Warn("update watcher timed out")
statusLabel.SetText("Update timed out. Please try again.")
case errors.Is(err, context.Canceled):
log.Info("update watcher canceled")
statusLabel.SetText("Update canceled.")
case err != nil:
log.Errorf("update failed: %v", err)
statusLabel.SetText("Update failed: " + err.Error())
default:
s.wUpdateProgress.Close()
}
}
// dotUpdater returns a closure that cycles through dots for a loading animation.
func dotUpdater() func() string {
dotCount := 0
return func() string {
dotCount = (dotCount + 1) % 4
return fmt.Sprintf("%s%s", "Updating", strings.Repeat(".", dotCount))
}
}
func mapInstallError(msg string) error {
msg = strings.ToLower(strings.TrimSpace(msg))
switch {
case strings.Contains(msg, "deadline exceeded"), strings.Contains(msg, "timeout"):
return context.DeadlineExceeded
case strings.Contains(msg, "canceled"), strings.Contains(msg, "cancelled"):
return context.Canceled
case msg == "":
return errors.New("unknown update error")
default:
return errors.New(msg)
}
}