mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-15 23:06:38 +00:00
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
350 lines
8.7 KiB
Go
350 lines
8.7 KiB
Go
//go:build !(linux && 386)
|
|
|
|
//go:generate fyne bundle -o quickactions_assets.go assets/connected.png
|
|
//go:generate fyne bundle -o quickactions_assets.go -append assets/disconnected.png
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
_ "embed"
|
|
"fmt"
|
|
"runtime"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"fyne.io/fyne/v2"
|
|
"fyne.io/fyne/v2/canvas"
|
|
"fyne.io/fyne/v2/container"
|
|
"fyne.io/fyne/v2/layout"
|
|
"fyne.io/fyne/v2/widget"
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/netbirdio/netbird/client/internal"
|
|
"github.com/netbirdio/netbird/client/proto"
|
|
)
|
|
|
|
type quickActionsUiState struct {
|
|
connectionStatus string
|
|
isToggleButtonEnabled bool
|
|
isConnectionChanged bool
|
|
toggleAction func()
|
|
}
|
|
|
|
func newQuickActionsUiState() quickActionsUiState {
|
|
return quickActionsUiState{
|
|
connectionStatus: string(internal.StatusIdle),
|
|
isToggleButtonEnabled: false,
|
|
isConnectionChanged: false,
|
|
}
|
|
}
|
|
|
|
type clientConnectionStatusProvider interface {
|
|
connectionStatus(ctx context.Context) (string, error)
|
|
}
|
|
|
|
type daemonClientConnectionStatusProvider struct {
|
|
client proto.DaemonServiceClient
|
|
}
|
|
|
|
func (d daemonClientConnectionStatusProvider) connectionStatus(ctx context.Context) (string, error) {
|
|
childCtx, cancel := context.WithTimeout(ctx, 400*time.Millisecond)
|
|
defer cancel()
|
|
status, err := d.client.Status(childCtx, &proto.StatusRequest{})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return status.Status, nil
|
|
}
|
|
|
|
type clientCommand interface {
|
|
execute() error
|
|
}
|
|
|
|
type connectCommand struct {
|
|
connectClient func() error
|
|
}
|
|
|
|
func (c connectCommand) execute() error {
|
|
return c.connectClient()
|
|
}
|
|
|
|
type disconnectCommand struct {
|
|
disconnectClient func() error
|
|
}
|
|
|
|
func (c disconnectCommand) execute() error {
|
|
return c.disconnectClient()
|
|
}
|
|
|
|
type quickActionsViewModel struct {
|
|
provider clientConnectionStatusProvider
|
|
connect clientCommand
|
|
disconnect clientCommand
|
|
uiChan chan quickActionsUiState
|
|
isWatchingConnectionStatus atomic.Bool
|
|
}
|
|
|
|
func newQuickActionsViewModel(ctx context.Context, provider clientConnectionStatusProvider, connect, disconnect clientCommand, uiChan chan quickActionsUiState) {
|
|
viewModel := quickActionsViewModel{
|
|
provider: provider,
|
|
connect: connect,
|
|
disconnect: disconnect,
|
|
uiChan: uiChan,
|
|
}
|
|
|
|
viewModel.isWatchingConnectionStatus.Store(true)
|
|
|
|
// base UI status
|
|
uiChan <- newQuickActionsUiState()
|
|
|
|
// this retrieves the current connection status
|
|
// and pushes the UI state that reflects it via uiChan
|
|
go viewModel.watchConnectionStatus(ctx)
|
|
}
|
|
|
|
func (q *quickActionsViewModel) updateUiState(ctx context.Context) {
|
|
uiState := newQuickActionsUiState()
|
|
connectionStatus, err := q.provider.connectionStatus(ctx)
|
|
|
|
if err != nil {
|
|
log.Errorf("Status: Error - %v", err)
|
|
q.uiChan <- uiState
|
|
return
|
|
}
|
|
|
|
if connectionStatus == string(internal.StatusConnected) {
|
|
uiState.toggleAction = func() {
|
|
q.executeCommand(q.disconnect)
|
|
}
|
|
} else {
|
|
uiState.toggleAction = func() {
|
|
q.executeCommand(q.connect)
|
|
}
|
|
}
|
|
|
|
uiState.isToggleButtonEnabled = true
|
|
uiState.connectionStatus = connectionStatus
|
|
q.uiChan <- uiState
|
|
}
|
|
|
|
func (q *quickActionsViewModel) watchConnectionStatus(ctx context.Context) {
|
|
ticker := time.NewTicker(1000 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
if q.isWatchingConnectionStatus.Load() {
|
|
q.updateUiState(ctx)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (q *quickActionsViewModel) executeCommand(command clientCommand) {
|
|
uiState := newQuickActionsUiState()
|
|
// newQuickActionsUiState starts with Idle connection status,
|
|
// and all that's necessary here is to just disable the toggle button.
|
|
uiState.connectionStatus = ""
|
|
|
|
q.uiChan <- uiState
|
|
|
|
q.isWatchingConnectionStatus.Store(false)
|
|
|
|
err := command.execute()
|
|
|
|
if err != nil {
|
|
log.Errorf("Status: Error - %v", err)
|
|
q.isWatchingConnectionStatus.Store(true)
|
|
} else {
|
|
uiState = newQuickActionsUiState()
|
|
uiState.isConnectionChanged = true
|
|
q.uiChan <- uiState
|
|
}
|
|
}
|
|
|
|
func getSystemTrayName() string {
|
|
os := runtime.GOOS
|
|
switch os {
|
|
case "darwin":
|
|
return "menu bar"
|
|
default:
|
|
return "system tray"
|
|
}
|
|
}
|
|
|
|
func (s *serviceClient) getNetBirdImage(name string, content []byte) *canvas.Image {
|
|
imageSize := fyne.NewSize(64, 64)
|
|
|
|
resource := fyne.NewStaticResource(name, content)
|
|
image := canvas.NewImageFromResource(resource)
|
|
image.FillMode = canvas.ImageFillContain
|
|
image.SetMinSize(imageSize)
|
|
image.Resize(imageSize)
|
|
|
|
return image
|
|
}
|
|
|
|
type quickActionsUiComponents struct {
|
|
content *fyne.Container
|
|
toggleConnectionButton *widget.Button
|
|
connectedLabelText, disconnectedLabelText string
|
|
connectedImage, disconnectedImage *canvas.Image
|
|
connectedCircleRes, disconnectedCircleRes fyne.Resource
|
|
}
|
|
|
|
// applyQuickActionsUiState applies a single UI state to the quick actions window.
|
|
// It closes the window and returns true if the connection status has changed,
|
|
// in which case the caller should stop processing further states.
|
|
func (s *serviceClient) applyQuickActionsUiState(
|
|
uiState quickActionsUiState,
|
|
components quickActionsUiComponents,
|
|
) bool {
|
|
if uiState.isConnectionChanged {
|
|
fyne.DoAndWait(func() {
|
|
s.wQuickActions.Close()
|
|
})
|
|
return true
|
|
}
|
|
|
|
var logo *canvas.Image
|
|
var buttonText string
|
|
var buttonIcon fyne.Resource
|
|
|
|
if uiState.connectionStatus == string(internal.StatusConnected) {
|
|
buttonText = components.connectedLabelText
|
|
buttonIcon = components.connectedCircleRes
|
|
logo = components.connectedImage
|
|
} else if uiState.connectionStatus == string(internal.StatusIdle) {
|
|
buttonText = components.disconnectedLabelText
|
|
buttonIcon = components.disconnectedCircleRes
|
|
logo = components.disconnectedImage
|
|
}
|
|
|
|
fyne.DoAndWait(func() {
|
|
if buttonText != "" {
|
|
components.toggleConnectionButton.SetText(buttonText)
|
|
}
|
|
|
|
if buttonIcon != nil {
|
|
components.toggleConnectionButton.SetIcon(buttonIcon)
|
|
}
|
|
|
|
if uiState.isToggleButtonEnabled {
|
|
components.toggleConnectionButton.Enable()
|
|
} else {
|
|
components.toggleConnectionButton.Disable()
|
|
}
|
|
|
|
components.toggleConnectionButton.OnTapped = func() {
|
|
if uiState.toggleAction != nil {
|
|
go uiState.toggleAction()
|
|
}
|
|
}
|
|
|
|
components.toggleConnectionButton.Refresh()
|
|
|
|
// the second position in the content's object array is the NetBird logo.
|
|
if logo != nil {
|
|
components.content.Objects[1] = logo
|
|
components.content.Refresh()
|
|
}
|
|
})
|
|
|
|
return false
|
|
}
|
|
|
|
// showQuickActionsUI displays a simple window with the NetBird logo and a connection toggle button.
|
|
func (s *serviceClient) showQuickActionsUI() {
|
|
s.wQuickActions = s.app.NewWindow("NetBird")
|
|
vmCtx, vmCancel := context.WithCancel(s.ctx)
|
|
s.wQuickActions.SetOnClosed(vmCancel)
|
|
|
|
client, err := s.getSrvClient(defaultFailTimeout)
|
|
|
|
connCmd := connectCommand{
|
|
connectClient: func() error {
|
|
return s.menuUpClick(s.ctx)
|
|
},
|
|
}
|
|
|
|
disConnCmd := disconnectCommand{
|
|
disconnectClient: func() error {
|
|
return s.menuDownClick()
|
|
},
|
|
}
|
|
|
|
if err != nil {
|
|
log.Errorf("get service client: %v", err)
|
|
return
|
|
}
|
|
|
|
uiChan := make(chan quickActionsUiState, 1)
|
|
newQuickActionsViewModel(vmCtx, daemonClientConnectionStatusProvider{client: client}, connCmd, disConnCmd, uiChan)
|
|
|
|
connectedImage := s.getNetBirdImage("netbird.png", iconAbout)
|
|
disconnectedImage := s.getNetBirdImage("netbird-disconnected.png", iconAboutDisconnected)
|
|
|
|
connectedCircle := canvas.NewImageFromResource(resourceConnectedPng)
|
|
disconnectedCircle := canvas.NewImageFromResource(resourceDisconnectedPng)
|
|
|
|
connectedLabelText := "Disconnect"
|
|
disconnectedLabelText := "Connect"
|
|
|
|
toggleConnectionButton := widget.NewButtonWithIcon(disconnectedLabelText, disconnectedCircle.Resource, func() {
|
|
// This button's tap function will be set when an ui state arrives via the uiChan channel.
|
|
})
|
|
|
|
// Button starts disabled until the first ui state arrives.
|
|
toggleConnectionButton.Disable()
|
|
|
|
hintLabelText := fmt.Sprintf("You can always access NetBird from your %s.", getSystemTrayName())
|
|
hintLabel := widget.NewLabel(hintLabelText)
|
|
|
|
content := container.NewVBox(
|
|
layout.NewSpacer(),
|
|
disconnectedImage,
|
|
layout.NewSpacer(),
|
|
container.NewCenter(toggleConnectionButton),
|
|
layout.NewSpacer(),
|
|
container.NewCenter(hintLabel),
|
|
)
|
|
|
|
// this watches for ui state updates.
|
|
go func() {
|
|
|
|
for {
|
|
select {
|
|
case <-vmCtx.Done():
|
|
return
|
|
case uiState, ok := <-uiChan:
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
closed := s.applyQuickActionsUiState(
|
|
uiState,
|
|
quickActionsUiComponents{
|
|
content,
|
|
toggleConnectionButton,
|
|
connectedLabelText, disconnectedLabelText,
|
|
connectedImage, disconnectedImage,
|
|
connectedCircle.Resource, disconnectedCircle.Resource,
|
|
},
|
|
)
|
|
if closed {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
s.wQuickActions.SetContent(content)
|
|
s.wQuickActions.Resize(fyne.NewSize(400, 200))
|
|
s.wQuickActions.SetFixedSize(true)
|
|
s.wQuickActions.Show()
|
|
}
|