mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-12 09:01:55 -04:00
- **Wails v3 application** (`client/ui`) with a React + TypeScript + Tailwind frontend replacing the Fyne UI: main connection view, exit-node switcher, networks/peers browser with detail panels, profile management, settings (general, network, SSH, security, troubleshooting, appearance), debug-bundle creation, and a first-run welcome flow. - **Internationalization**: go-i18n bundle with 9 locales (en, de, es, fr, hu, it, pt, ru, zh-CN) shared between the tray and the frontend. - **New system tray** implementation with per-platform theme-aware icons, including a native XEmbed host for Linux (`xembed_tray_linux.c`) and a Linux theme watcher. - **Session handling**: auth session watcher (`client/internal/auth/sessionwatch`), pending login flow, session-expiration dialog and tray notifications, and `netbird login` improvements. - **Daemon API extensions** (`daemon.proto`): status stream subscription, event stream, networks/exit-node selection endpoints, and richer full status — with probe throttling on the daemon side to protect against UI-driven request storms. - **UI preferences store** persisted per profile, autostart management via the daemon (single source of truth in HKCU on Windows). - **Build system**: Taskfile-based builds per platform (macOS, Linux, Windows), Docker cross-compilation images, MSIX/NSIS/nfpm/AppImage packaging, and a new `frontend-ui` CI workflow. Co-authored-by: Zoltan Papp <zoltan.pmail@gmail.com> Co-authored-by: Eduard Gert <kontakt@eduardgert.de> Co-authored-by: braginini <bangvalo@gmail.com> Co-authored-by: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Co-authored-by: riccardom <riccardomanfrin@gmail.com>
177 lines
5.6 KiB
Go
177 lines
5.6 KiB
Go
//go:build linux && !(linux && 386)
|
|
|
|
package main
|
|
|
|
// In-process org.kde.StatusNotifierWatcher for minimal WMs (Fluxbox, OpenBox,
|
|
// i3) that ship no watcher. When an XEmbed tray exists (_NET_SYSTEM_TRAY_S0),
|
|
// an in-process XEmbed host bridges the SNI icon into it.
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/godbus/dbus/v5"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
const (
|
|
watcherName = "org.kde.StatusNotifierWatcher"
|
|
watcherPath = "/StatusNotifierWatcher"
|
|
watcherIface = "org.kde.StatusNotifierWatcher"
|
|
|
|
// The UI is often autostarted before the panel on minimal WMs, so a single
|
|
// startup probe would miss a tray that appears a second later.
|
|
watcherProbeInterval = 500 * time.Millisecond
|
|
watcherProbeTimeout = 10 * time.Second
|
|
)
|
|
|
|
type statusNotifierWatcher struct {
|
|
conn *dbus.Conn
|
|
items []string
|
|
hosts map[string]*xembedHost
|
|
hostsMu sync.Mutex
|
|
}
|
|
|
|
// RegisterStatusNotifierItem is the D-Bus method called by tray clients.
|
|
// sender is injected by godbus and is not part of the D-Bus signature.
|
|
func (w *statusNotifierWatcher) RegisterStatusNotifierItem(sender dbus.Sender, service string) *dbus.Error {
|
|
for _, s := range w.items {
|
|
if s == service {
|
|
return nil
|
|
}
|
|
}
|
|
w.items = append(w.items, service)
|
|
log.Debugf("StatusNotifierWatcher: registered item %q from %s", service, sender)
|
|
|
|
go w.tryStartXembedHost(string(sender), dbus.ObjectPath(service))
|
|
return nil
|
|
}
|
|
|
|
// RegisterStatusNotifierHost is required by the protocol but unused here.
|
|
func (w *statusNotifierWatcher) RegisterStatusNotifierHost(service string) *dbus.Error {
|
|
log.Debugf("StatusNotifierWatcher: host registered %q", service)
|
|
return nil
|
|
}
|
|
|
|
// tryStartXembedHost is a no-op when no XEmbed tray manager is available.
|
|
func (w *statusNotifierWatcher) tryStartXembedHost(busName string, objPath dbus.ObjectPath) {
|
|
w.hostsMu.Lock()
|
|
defer w.hostsMu.Unlock()
|
|
|
|
if _, exists := w.hosts[busName]; exists {
|
|
return
|
|
}
|
|
|
|
// Private session bus so our signal subscriptions don't reach Wails'
|
|
// signal handler, which panics on unexpected signals.
|
|
sessionConn, err := dbus.SessionBusPrivate()
|
|
if err != nil {
|
|
log.Debugf("StatusNotifierWatcher: cannot open private session bus for XEmbed host: %v", err)
|
|
return
|
|
}
|
|
if err := sessionConn.Auth(nil); err != nil {
|
|
log.Debugf("StatusNotifierWatcher: XEmbed host auth failed: %v", err)
|
|
closeBus(sessionConn)
|
|
return
|
|
}
|
|
if err := sessionConn.Hello(); err != nil {
|
|
log.Debugf("StatusNotifierWatcher: XEmbed host Hello failed: %v", err)
|
|
closeBus(sessionConn)
|
|
return
|
|
}
|
|
|
|
host, err := newXembedHost(sessionConn, busName, objPath)
|
|
if err != nil {
|
|
log.Debugf("StatusNotifierWatcher: XEmbed host not started: %v", err)
|
|
closeBus(sessionConn)
|
|
return
|
|
}
|
|
|
|
w.hosts[busName] = host
|
|
go host.run()
|
|
log.Infof("StatusNotifierWatcher: XEmbed tray icon created for %s", busName)
|
|
}
|
|
|
|
// startStatusNotifierWatcher claims org.kde.StatusNotifierWatcher only as a
|
|
// bridge to an XEmbed tray on minimal WMs. The watcher is a stub that never
|
|
// relays items to a real StatusNotifierHost, so claiming the name on a desktop
|
|
// with a real host (e.g. Hyprland + Waybar) would dead-end every other tray
|
|
// app's icon. It gates on the actual presence of an XEmbed tray rather than
|
|
// GetNameOwner, which can't win a login-order race; without one it stays off
|
|
// the bus so the real watcher owns the name. The XEmbed tray may come up after
|
|
// the UI, so it re-probes for a grace period rather than deciding once.
|
|
// Safe to call unconditionally.
|
|
func startStatusNotifierWatcher() {
|
|
go func() {
|
|
deadline := time.Now().Add(watcherProbeTimeout)
|
|
for {
|
|
if xembedTrayAvailable() {
|
|
claimStatusNotifierWatcher()
|
|
return
|
|
}
|
|
if time.Now().After(deadline) {
|
|
log.Debugf("StatusNotifierWatcher: no XEmbed tray appeared within %s, leaving the watcher to the desktop", watcherProbeTimeout)
|
|
return
|
|
}
|
|
time.Sleep(watcherProbeInterval)
|
|
}
|
|
}()
|
|
}
|
|
|
|
// claimStatusNotifierWatcher takes ownership of org.kde.StatusNotifierWatcher
|
|
// on a private session bus and exports the stub watcher. The GetNameOwner /
|
|
// DoNotQueue guards back off if a real watcher already holds the name.
|
|
func claimStatusNotifierWatcher() {
|
|
conn, err := dbus.SessionBusPrivate()
|
|
if err != nil {
|
|
log.Debugf("StatusNotifierWatcher: cannot open private session bus: %v", err)
|
|
return
|
|
}
|
|
if err := conn.Auth(nil); err != nil {
|
|
log.Debugf("StatusNotifierWatcher: auth failed: %v", err)
|
|
closeBus(conn)
|
|
return
|
|
}
|
|
if err := conn.Hello(); err != nil {
|
|
log.Debugf("StatusNotifierWatcher: Hello failed: %v", err)
|
|
closeBus(conn)
|
|
return
|
|
}
|
|
|
|
var owner string
|
|
callErr := conn.BusObject().Call("org.freedesktop.DBus.GetNameOwner", 0, watcherName).Store(&owner)
|
|
if callErr == nil && owner != "" {
|
|
log.Debugf("StatusNotifierWatcher: already owned by %s, skipping", owner)
|
|
closeBus(conn)
|
|
return
|
|
}
|
|
|
|
reply, err := conn.RequestName(watcherName, dbus.NameFlagDoNotQueue)
|
|
if err != nil || reply != dbus.RequestNameReplyPrimaryOwner {
|
|
log.Debugf("StatusNotifierWatcher: could not claim name (reply=%v err=%v)", reply, err)
|
|
closeBus(conn)
|
|
return
|
|
}
|
|
|
|
w := &statusNotifierWatcher{
|
|
conn: conn,
|
|
hosts: make(map[string]*xembedHost),
|
|
}
|
|
if err := conn.ExportAll(w, dbus.ObjectPath(watcherPath), watcherIface); err != nil {
|
|
log.Errorf("StatusNotifierWatcher: export failed: %v", err)
|
|
closeBus(conn)
|
|
return
|
|
}
|
|
|
|
log.Infof("StatusNotifierWatcher: active on session bus (enables tray on minimal WMs)")
|
|
// Connection kept open for the process lifetime.
|
|
}
|
|
|
|
// closeBus closes a private session bus opened on a back-off path, logging a
|
|
// warning rather than swallowing the error.
|
|
func closeBus(conn *dbus.Conn) {
|
|
if err := conn.Close(); err != nil {
|
|
log.Warnf("StatusNotifierWatcher: closing session bus failed: %v", err)
|
|
}
|
|
}
|