mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-08 06:29:08 +02:00
[client] refactor auto update workflow (#5448)
Auto-update logic moved out of the UI into a dedicated updatemanager.Manager service that runs in the connection layer. The UI no longer polls or checks for updates independently. The update manager supports three modes driven by the management server's auto-update policy: No policy set by mgm: checks GitHub for the latest version and notifies the user (previous behavior, now centralized) mgm enforces update: the "About" menu triggers installation directly instead of just downloading the file — user still initiates the action mgm forces update: installation proceeds automatically without user interaction updateManager lifecycle is now owned by daemon, giving the daemon server direct control via a new TriggerUpdate RPC Introduces EngineServices struct to group external service dependencies passed to NewEngine, reducing its argument count from 11 to 4
This commit is contained in:
+30
-14
@@ -34,7 +34,6 @@ import (
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
protobuf "google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/netbirdio/netbird/client/iface"
|
||||
"github.com/netbirdio/netbird/client/internal"
|
||||
@@ -308,10 +307,11 @@ type serviceClient struct {
|
||||
sshJWTCacheTTL int
|
||||
|
||||
connected bool
|
||||
update *version.Update
|
||||
daemonVersion string
|
||||
updateIndicationLock sync.Mutex
|
||||
isUpdateIconActive bool
|
||||
isEnforcedUpdate bool
|
||||
lastNotifiedVersion string
|
||||
settingsEnabled bool
|
||||
profilesEnabled bool
|
||||
showNetworks bool
|
||||
@@ -367,7 +367,6 @@ func newServiceClient(args *newServiceClientArgs) *serviceClient {
|
||||
|
||||
showAdvancedSettings: args.showSettings,
|
||||
showNetworks: args.showNetworks,
|
||||
update: version.NewUpdateAndStart("nb/client-ui"),
|
||||
}
|
||||
|
||||
s.eventHandler = newEventHandler(s)
|
||||
@@ -828,7 +827,7 @@ func (s *serviceClient) handleSSOLogin(ctx context.Context, loginResp *proto.Log
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *serviceClient) menuUpClick(ctx context.Context, wannaAutoUpdate bool) error {
|
||||
func (s *serviceClient) menuUpClick(ctx context.Context) error {
|
||||
systray.SetTemplateIcon(iconConnectingMacOS, s.icConnecting)
|
||||
conn, err := s.getSrvClient(defaultFailTimeout)
|
||||
if err != nil {
|
||||
@@ -850,9 +849,7 @@ func (s *serviceClient) menuUpClick(ctx context.Context, wannaAutoUpdate bool) e
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := s.conn.Up(s.ctx, &proto.UpRequest{
|
||||
AutoUpdate: protobuf.Bool(wannaAutoUpdate),
|
||||
}); err != nil {
|
||||
if _, err := s.conn.Up(s.ctx, &proto.UpRequest{}); err != nil {
|
||||
return fmt.Errorf("start connection: %w", err)
|
||||
}
|
||||
|
||||
@@ -933,13 +930,13 @@ func (s *serviceClient) updateStatus() error {
|
||||
systrayIconState = false
|
||||
}
|
||||
|
||||
// the updater struct notify by the upgrades available only, but if meanwhile the daemon has successfully
|
||||
// updated must reset the mUpdate visibility state
|
||||
// if the daemon version changed (e.g. after a successful update), reset the update indication
|
||||
if s.daemonVersion != status.DaemonVersion {
|
||||
s.mUpdate.Hide()
|
||||
if s.daemonVersion != "" {
|
||||
s.mUpdate.Hide()
|
||||
s.isUpdateIconActive = false
|
||||
}
|
||||
s.daemonVersion = status.DaemonVersion
|
||||
|
||||
s.isUpdateIconActive = s.update.SetDaemonVersion(status.DaemonVersion)
|
||||
if !s.isUpdateIconActive {
|
||||
if systrayIconState {
|
||||
systray.SetTemplateIcon(iconConnectedMacOS, s.icConnected)
|
||||
@@ -1091,7 +1088,6 @@ func (s *serviceClient) onTrayReady() {
|
||||
// update exit node menu in case service is already connected
|
||||
go s.updateExitNodes()
|
||||
|
||||
s.update.SetOnUpdateListener(s.onUpdateAvailable)
|
||||
go func() {
|
||||
s.getSrvConfig()
|
||||
time.Sleep(100 * time.Millisecond) // To prevent race condition caused by systray not being fully initialized and ignoring setIcon
|
||||
@@ -1135,6 +1131,13 @@ func (s *serviceClient) onTrayReady() {
|
||||
}
|
||||
}
|
||||
})
|
||||
s.eventManager.AddHandler(func(event *proto.SystemEvent) {
|
||||
if newVersion, ok := event.Metadata["new_version_available"]; ok {
|
||||
_, enforced := event.Metadata["enforced"]
|
||||
log.Infof("received new_version_available event: version=%s enforced=%v", newVersion, enforced)
|
||||
s.onUpdateAvailable(newVersion, enforced)
|
||||
}
|
||||
})
|
||||
|
||||
go s.eventManager.Start(s.ctx)
|
||||
go s.eventHandler.listen(s.ctx)
|
||||
@@ -1507,10 +1510,18 @@ func protoConfigToConfig(cfg *proto.GetConfigResponse) *profilemanager.Config {
|
||||
return &config
|
||||
}
|
||||
|
||||
func (s *serviceClient) onUpdateAvailable() {
|
||||
func (s *serviceClient) onUpdateAvailable(newVersion string, enforced bool) {
|
||||
s.updateIndicationLock.Lock()
|
||||
defer s.updateIndicationLock.Unlock()
|
||||
|
||||
s.isEnforcedUpdate = enforced
|
||||
if enforced {
|
||||
s.mUpdate.SetTitle("Install version " + newVersion)
|
||||
} else {
|
||||
s.lastNotifiedVersion = ""
|
||||
s.mUpdate.SetTitle("Download latest version")
|
||||
}
|
||||
|
||||
s.mUpdate.Show()
|
||||
s.isUpdateIconActive = true
|
||||
|
||||
@@ -1519,6 +1530,11 @@ func (s *serviceClient) onUpdateAvailable() {
|
||||
} else {
|
||||
systray.SetTemplateIcon(iconUpdateDisconnectedMacOS, s.icUpdateDisconnected)
|
||||
}
|
||||
|
||||
if enforced && s.lastNotifiedVersion != newVersion {
|
||||
s.lastNotifiedVersion = newVersion
|
||||
s.app.SendNotification(fyne.NewNotification("Update available", "A new version "+newVersion+" is ready to install"))
|
||||
}
|
||||
}
|
||||
|
||||
// onSessionExpire sends a notification to the user when the session expires.
|
||||
|
||||
@@ -107,12 +107,7 @@ func (e *Manager) handleEvent(event *proto.SystemEvent) {
|
||||
handlers := slices.Clone(e.handlers)
|
||||
e.mu.Unlock()
|
||||
|
||||
// critical events are always shown
|
||||
if !enabled && event.Severity != proto.SystemEvent_CRITICAL {
|
||||
return
|
||||
}
|
||||
|
||||
if event.UserMessage != "" {
|
||||
if event.UserMessage != "" && (enabled || event.Severity == proto.SystemEvent_CRITICAL) {
|
||||
title := e.getEventTitle(event)
|
||||
body := event.UserMessage
|
||||
id := event.Metadata["id"]
|
||||
|
||||
@@ -82,7 +82,7 @@ func (h *eventHandler) handleConnectClick() {
|
||||
go func() {
|
||||
defer connectCancel()
|
||||
|
||||
if err := h.client.menuUpClick(connectCtx, true); err != nil {
|
||||
if err := h.client.menuUpClick(connectCtx); err != nil {
|
||||
st, ok := status.FromError(err)
|
||||
if errors.Is(err, context.Canceled) || (ok && st.Code() == codes.Canceled) {
|
||||
log.Debugf("connect operation cancelled by user")
|
||||
@@ -211,9 +211,42 @@ func (h *eventHandler) handleGitHubClick() {
|
||||
}
|
||||
|
||||
func (h *eventHandler) handleUpdateClick() {
|
||||
if err := openURL(version.DownloadUrl()); err != nil {
|
||||
log.Errorf("failed to open download URL: %v", err)
|
||||
h.client.updateIndicationLock.Lock()
|
||||
enforced := h.client.isEnforcedUpdate
|
||||
h.client.updateIndicationLock.Unlock()
|
||||
|
||||
if !enforced {
|
||||
if err := openURL(version.DownloadUrl()); err != nil {
|
||||
log.Errorf("failed to open download URL: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// prevent blocking against a busy server
|
||||
h.client.mUpdate.Disable()
|
||||
go func() {
|
||||
defer h.client.mUpdate.Enable()
|
||||
conn, err := h.client.getSrvClient(defaultFailTimeout)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get service client for update: %v", err)
|
||||
_ = openURL(version.DownloadUrl())
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := conn.TriggerUpdate(h.client.ctx, &proto.TriggerUpdateRequest{})
|
||||
if err != nil {
|
||||
log.Errorf("TriggerUpdate failed: %v", err)
|
||||
_ = openURL(version.DownloadUrl())
|
||||
return
|
||||
}
|
||||
if !resp.Success {
|
||||
log.Errorf("TriggerUpdate failed: %s", resp.ErrorMsg)
|
||||
_ = openURL(version.DownloadUrl())
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("update triggered via daemon")
|
||||
}()
|
||||
}
|
||||
|
||||
func (h *eventHandler) handleNetworksClick() {
|
||||
|
||||
@@ -397,7 +397,7 @@ type profileMenu struct {
|
||||
logoutSubItem *subItem
|
||||
profilesState []Profile
|
||||
downClickCallback func() error
|
||||
upClickCallback func(context.Context, bool) error
|
||||
upClickCallback func(context.Context) error
|
||||
getSrvClientCallback func(timeout time.Duration) (proto.DaemonServiceClient, error)
|
||||
loadSettingsCallback func()
|
||||
app fyne.App
|
||||
@@ -411,7 +411,7 @@ type newProfileMenuArgs struct {
|
||||
profileMenuItem *systray.MenuItem
|
||||
emailMenuItem *systray.MenuItem
|
||||
downClickCallback func() error
|
||||
upClickCallback func(context.Context, bool) error
|
||||
upClickCallback func(context.Context) error
|
||||
getSrvClientCallback func(timeout time.Duration) (proto.DaemonServiceClient, error)
|
||||
loadSettingsCallback func()
|
||||
app fyne.App
|
||||
@@ -579,7 +579,7 @@ func (p *profileMenu) refresh() {
|
||||
connectCtx, connectCancel := context.WithCancel(p.ctx)
|
||||
p.serviceClient.connectCancel = connectCancel
|
||||
|
||||
if err := p.upClickCallback(connectCtx, false); err != nil {
|
||||
if err := p.upClickCallback(connectCtx); err != nil {
|
||||
log.Errorf("failed to handle up click after switching profile: %v", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -267,7 +267,7 @@ func (s *serviceClient) showQuickActionsUI() {
|
||||
|
||||
connCmd := connectCommand{
|
||||
connectClient: func() error {
|
||||
return s.menuUpClick(s.ctx, false)
|
||||
return s.menuUpClick(s.ctx)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user