Files
netbird/client/ui/main.go
Zoltan Papp 803144e569 [client/ui] Unify profile-switching logic in ProfileSwitcher service
Both the tray and the React Profiles page previously had separate
switching logic: the tray applied a status-aware reconnect policy
(Down for error states, Up only when previously Connected/Connecting),
while the React page always called Switch + Up unconditionally with no
Down for LoginFailed/NeedsLogin/SessionExpired.

Introduce a single ProfileSwitcher service that encapsulates the full
reconnect policy. SwitchActive queries the current daemon status, calls
Switch, and launches Down/Up in a background goroutine so the caller
returns immediately after the Switch RPC completes. Both the tray and
the React Profiles page now delegate to this service.

Export the daemon status string constants (StatusConnected, etc.) from
the services package so tray.go no longer duplicates them as private
constants.
2026-05-13 15:46:00 +02:00

167 lines
5.6 KiB
Go

//go:build !android && !ios && !freebsd && !js
package main
import (
"context"
"embed"
"flag"
"log"
"strings"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/events"
"github.com/wailsapp/wails/v3/pkg/services/notifications"
"github.com/netbirdio/netbird/client/ui/services"
"github.com/netbirdio/netbird/util"
)
//go:embed all:frontend/dist
var assets embed.FS
// stringList is a flag.Value that collects repeated string flags. The first
// time the user passes -log-file the seeded default ("console") is dropped;
// subsequent passes append. Lets the user replace or extend the log target
// list without a separate "reset" flag.
type stringList struct {
values []string
userSet bool
}
func (s *stringList) String() string {
return strings.Join(s.values, ",")
}
func (s *stringList) Set(v string) error {
if !s.userSet {
s.values = nil
s.userSet = true
}
s.values = append(s.values, v)
return nil
}
func init() {
application.RegisterEvent[services.Status](services.EventStatus)
application.RegisterEvent[services.SystemEvent](services.EventSystem)
application.RegisterEvent[services.UpdateAvailable](services.EventUpdateAvailable)
application.RegisterEvent[services.UpdateProgress](services.EventUpdateProgress)
}
func main() {
daemonAddr := flag.String("daemon-addr", DaemonAddr(), "Daemon gRPC address: unix:///path or tcp://host:port")
logFiles := &stringList{values: []string{"console"}}
flag.Var(logFiles, "log-file", "Log destination. Repeat to log to multiple targets at once, e.g. `--log-file console --log-file Y:/netbird-ui.log`. Each value is one of: console, syslog, or a file path. File destinations are rotated by lumberjack (same as the daemon). Defaults to console.")
logLevel := flag.String("log-level", "info", "Log level: trace|debug|info|warn|error.")
flag.Parse()
if err := util.InitLog(*logLevel, logFiles.values...); err != nil {
log.Fatalf("init log: %v", err)
}
conn := NewConn(*daemonAddr)
// tray is captured in the SingleInstance callback below; the var is
// declared before app.New so the closure has a stable reference.
var tray *Tray
app := application.New(application.Options{
// Windows uses Name as the AppUserModelID for toast notifications
// (see notifications_windows.go: cfg.Name -> wn.appName -> AppID)
// and as the registry path under HKCU\Software\Classes\AppUserModelId\.
// Must match the System.AppUserModel.ID value the MSI sets on the
// Start Menu shortcut (client/netbird.wxs) and the AppUserModelId
// key the installer pre-populates with the toast activator CLSID;
// otherwise toasts show under a different identity and the MSI's
// CustomActivator registry value is orphaned.
Name: "NetBird",
Description: "NetBird desktop client",
Icon: iconWindow,
Assets: application.AssetOptions{
Handler: application.AssetFileServerFS(assets),
},
Mac: application.MacOptions{
ApplicationShouldTerminateAfterLastWindowClosed: false,
},
Linux: application.LinuxOptions{
ProgramName: "netbird",
},
SingleInstance: &application.SingleInstanceOptions{
UniqueID: "io.netbird.ui",
OnSecondInstanceLaunch: func(_ application.SecondInstanceData) {
if tray != nil {
tray.ShowWindow()
}
},
},
})
connection := services.NewConnection(conn)
settings := services.NewSettings(conn)
profiles := services.NewProfiles(conn)
peers := services.NewPeers(conn, app.Event)
update := services.NewUpdate(conn)
notifier := notifications.New()
profileSwitcher := services.NewProfileSwitcher(profiles, connection, peers)
app.RegisterService(application.NewService(connection))
app.RegisterService(application.NewService(settings))
app.RegisterService(application.NewService(services.NewNetworks(conn)))
app.RegisterService(application.NewService(services.NewForwarding(conn)))
app.RegisterService(application.NewService(profiles))
app.RegisterService(application.NewService(services.NewDebug(conn)))
app.RegisterService(application.NewService(update))
app.RegisterService(application.NewService(peers))
app.RegisterService(application.NewService(notifier))
app.RegisterService(application.NewService(profileSwitcher))
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "NetBird",
Width: 960,
Height: 640,
Hidden: true,
BackgroundColour: application.NewRGB(24, 26, 29),
URL: "/",
Mac: application.MacWindow{
InvisibleTitleBarHeight: 38,
Backdrop: application.MacBackdropTranslucent,
TitleBar: application.MacTitleBarHiddenInset,
},
Linux: application.LinuxWindow{
Icon: iconWindow,
},
})
// Intercept the window close to hide instead of quit. The user reaches
// "really quit" via tray -> Quit.
window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
e.Cancel()
window.Hide()
})
// Register an in-process StatusNotifierWatcher so the tray works on
// minimal WMs (Fluxbox, OpenBox, i3, dwm, vanilla GNOME without the
// AppIndicator extension) that don't ship one themselves. No-op on
// non-Linux platforms. Must run before NewTray so the Wails systray's
// RegisterStatusNotifierItem call hits a watcher we control.
startStatusNotifierWatcher()
tray = NewTray(app, window, TrayServices{
Connection: connection,
Settings: settings,
Profiles: profiles,
Peers: peers,
Notifier: notifier,
Update: update,
ProfileSwitcher: profileSwitcher,
})
listenForShowSignal(context.Background(), tray)
peers.Watch(context.Background())
if err := app.Run(); err != nil {
log.Fatal(err)
}
}