mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-12 03:39:55 +00:00
[client/ui] Distinguish "daemon not running" tray state
The status stream emits a synthetic StatusDaemonUnavailable when the gRPC client or stream cannot be established, fired once per outage and cleared on the next real snapshot. The tray maps it to a "Not running" status label, switches the icon to the error variant, hides Connect/Disconnect (neither would work without the daemon), and disables Settings, Networks and Create Debug Bundle so the user is not routed to pages that would just fail to load.
This commit is contained in:
@@ -29,6 +29,12 @@ const (
|
||||
// started) installing an update — Mode 2 enforced flow. The UI opens the
|
||||
// progress window in response.
|
||||
EventUpdateProgress = "netbird:update:progress"
|
||||
|
||||
// StatusDaemonUnavailable is the synthetic Status the UI emits when the
|
||||
// daemon's gRPC socket is unreachable (daemon not running, socket
|
||||
// permission, etc.). Real daemon statuses come straight from
|
||||
// internal.Status* — none of those collide with this label.
|
||||
StatusDaemonUnavailable = "DaemonUnavailable"
|
||||
)
|
||||
|
||||
// Emitter is what peers.Watch needs from the host application: a simple
|
||||
@@ -198,13 +204,28 @@ func (s *Peers) statusStreamLoop(ctx context.Context) {
|
||||
Clock: backoff.SystemClock,
|
||||
}, ctx)
|
||||
|
||||
// unavailable tracks whether we've already signalled the daemon as
|
||||
// unreachable. The synthetic event is emitted once per outage so the
|
||||
// tray flips to the "Daemon not running" state, but the exponential
|
||||
// backoff retries don't re-fire it on every attempt.
|
||||
unavailable := false
|
||||
emitUnavailable := func() {
|
||||
if unavailable {
|
||||
return
|
||||
}
|
||||
unavailable = true
|
||||
s.emitter.Emit(EventStatus, Status{Status: StatusDaemonUnavailable})
|
||||
}
|
||||
|
||||
op := func() error {
|
||||
cli, err := s.conn.Client()
|
||||
if err != nil {
|
||||
emitUnavailable()
|
||||
return fmt.Errorf("get client: %w", err)
|
||||
}
|
||||
stream, err := cli.SubscribeStatus(ctx, &proto.StatusRequest{GetFullPeerStatus: true})
|
||||
if err != nil {
|
||||
emitUnavailable()
|
||||
return fmt.Errorf("subscribe status: %w", err)
|
||||
}
|
||||
for {
|
||||
@@ -213,8 +234,10 @@ func (s *Peers) statusStreamLoop(ctx context.Context) {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
emitUnavailable()
|
||||
return fmt.Errorf("status stream recv: %w", err)
|
||||
}
|
||||
unavailable = false
|
||||
st := statusFromProto(resp)
|
||||
log.Infof("backend event: status status=%q peers=%d", st.Status, len(st.Peers))
|
||||
s.emitter.Emit(EventStatus, st)
|
||||
|
||||
@@ -26,8 +26,9 @@ const (
|
||||
trayTooltip = "NetBird"
|
||||
|
||||
// Top-level menu entries.
|
||||
menuStatusDisconnected = "Disconnected"
|
||||
menuOpenNetBird = "Open NetBird"
|
||||
menuStatusDisconnected = "Disconnected"
|
||||
menuStatusDaemonUnavailable = "Not running"
|
||||
menuOpenNetBird = "Open NetBird"
|
||||
menuConnect = "Connect"
|
||||
menuDisconnect = "Disconnect"
|
||||
menuExitNode = "Exit Node"
|
||||
@@ -112,6 +113,8 @@ type Tray struct {
|
||||
downItem *application.MenuItem
|
||||
exitNodeItem *application.MenuItem
|
||||
networksItem *application.MenuItem
|
||||
settingsItem *application.MenuItem
|
||||
debugItem *application.MenuItem
|
||||
updateItem *application.MenuItem
|
||||
daemonVersionItem *application.MenuItem
|
||||
|
||||
@@ -201,8 +204,8 @@ func (t *Tray) buildMenu() *application.Menu {
|
||||
// block-inbound, auto-connect, notifications) and profile switching
|
||||
// all live in the in-window Settings page now. The tray menu only
|
||||
// surfaces the day-to-day actions.
|
||||
menu.Add(menuSettings).OnClick(func(*application.Context) { t.openRoute("/settings") })
|
||||
menu.Add(menuCreateDebugBundle).OnClick(func(*application.Context) { t.openRoute("/debug") })
|
||||
t.settingsItem = menu.Add(menuSettings).OnClick(func(*application.Context) { t.openRoute("/settings") })
|
||||
t.debugItem = menu.Add(menuCreateDebugBundle).OnClick(func(*application.Context) { t.openRoute("/debug") })
|
||||
|
||||
menu.AddSeparator()
|
||||
|
||||
@@ -432,20 +435,40 @@ func (t *Tray) applyStatus(st services.Status) {
|
||||
t.applyIcon()
|
||||
needsLogin := strings.EqualFold(st.Status, statusNeedsLogin) ||
|
||||
strings.EqualFold(st.Status, statusSessionExpired)
|
||||
daemonUnavailable := strings.EqualFold(st.Status, services.StatusDaemonUnavailable)
|
||||
if t.statusItem != nil {
|
||||
// When the daemon needs re-authentication the status row turns
|
||||
// into the actionable Login entry — Connect would only fail.
|
||||
t.statusItem.SetLabel(st.Status)
|
||||
// When the daemon socket is unreachable, swap the label to make
|
||||
// the cause obvious; Connect/Disconnect would just fail.
|
||||
label := st.Status
|
||||
if daemonUnavailable {
|
||||
label = menuStatusDaemonUnavailable
|
||||
}
|
||||
t.statusItem.SetLabel(label)
|
||||
t.statusItem.SetEnabled(needsLogin)
|
||||
}
|
||||
if t.upItem != nil {
|
||||
t.upItem.SetHidden(connected || needsLogin)
|
||||
t.upItem.SetEnabled(!connected && !needsLogin)
|
||||
t.upItem.SetHidden(connected || needsLogin || daemonUnavailable)
|
||||
t.upItem.SetEnabled(!connected && !needsLogin && !daemonUnavailable)
|
||||
}
|
||||
if t.downItem != nil {
|
||||
t.downItem.SetHidden(!connected)
|
||||
t.downItem.SetEnabled(connected)
|
||||
}
|
||||
// Settings, Networks and Debug Bundle all drive daemon RPCs from
|
||||
// their respective frontend routes — disable them while the daemon
|
||||
// socket is unreachable so the user doesn't land on a page that
|
||||
// would only fail to load.
|
||||
if t.networksItem != nil {
|
||||
t.networksItem.SetEnabled(!daemonUnavailable)
|
||||
}
|
||||
if t.settingsItem != nil {
|
||||
t.settingsItem.SetEnabled(!daemonUnavailable)
|
||||
}
|
||||
if t.debugItem != nil {
|
||||
t.debugItem.SetEnabled(!daemonUnavailable)
|
||||
}
|
||||
}
|
||||
if exitNodesChanged {
|
||||
t.rebuildExitNodes(exitNodes)
|
||||
@@ -517,7 +540,8 @@ func (t *Tray) iconForState() (icon, dark []byte) {
|
||||
t.mu.Unlock()
|
||||
|
||||
connecting := strings.EqualFold(statusLabel, "Connecting")
|
||||
errored := strings.EqualFold(statusLabel, "Error")
|
||||
errored := strings.EqualFold(statusLabel, "Error") ||
|
||||
strings.EqualFold(statusLabel, services.StatusDaemonUnavailable)
|
||||
|
||||
if runtime.GOOS == "darwin" {
|
||||
switch {
|
||||
|
||||
Reference in New Issue
Block a user