report daemon-down as DaemonUnavailable on initial Peers.Get and gate UI

- Peers.Get returns Status{Status: DaemonUnavailable} on Unavailable
  instead of an error so the React useStatus initial refresh picks up
  the same string the live event stream emits — the overlay no longer
  depends on receiving the synthetic event during boot.
- ProfileContext.refresh swallows Unavailable so the redundant
  "Load Profiles Failed" popup does not overlap the overlay.
- Tray Profiles submenu is disabled while the daemon is unavailable,
  matching the existing settings/debug/connect gating.
- gRPC client uses a 5s ConnectParams MaxDelay; the default 120s cap
  was keeping the SubChannel in backoff for tens of seconds after the
  daemon came back, masking the recovery.
This commit is contained in:
Zoltan Papp
2026-05-18 12:33:46 +02:00
parent f84b1df857
commit 6b44d65cac
4 changed files with 42 additions and 2 deletions

View File

@@ -56,9 +56,17 @@ export const ProfileProvider = ({ children }: { children: ReactNode }) => {
setActiveProfile(active.profileName || "default");
setProfiles(list);
} catch (e) {
// Daemon-down is already surfaced globally by
// DaemonUnavailableOverlay; a second popup on top of it is
// pure noise. Every profile RPC routes through the same gRPC
// conn, so the Unavailable code is the reliable marker.
const msg = e instanceof Error ? e.message : String(e);
if (msg.includes("code = Unavailable")) {
return;
}
await Dialogs.Error({
Title: i18next.t("profile.error.loadTitle"),
Message: e instanceof Error ? e.message : String(e),
Message: msg,
});
} finally {
setLoaded(true);

View File

@@ -7,8 +7,10 @@ import (
"runtime"
"strings"
"sync"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/backoff"
"google.golang.org/grpc/credentials/insecure"
"github.com/netbirdio/netbird/client/proto"
@@ -39,6 +41,19 @@ func (c *Conn) Client() (proto.DaemonServiceClient, error) {
strings.TrimPrefix(c.addr, "tcp://"),
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithUserAgent(desktop.GetUIUserAgent()),
// Without ConnectParams the SubChannel uses gRPC's default 120s
// MaxDelay, so after a couple of failed dials the UI waits 30-60s
// before noticing a freshly-started daemon. The Wails UI is a
// desktop client expecting prompt reconnects, not a high-fanout
// backend, so a 5s cap is a better trade-off than the default.
grpc.WithConnectParams(grpc.ConnectParams{
Backoff: backoff.Config{
BaseDelay: 1 * time.Second,
Multiplier: 1.6,
Jitter: 0.2,
MaxDelay: 5 * time.Second,
},
}),
)
if err != nil {
return nil, fmt.Errorf("dial daemon: %w", err)

View File

@@ -253,14 +253,25 @@ func (s *Peers) ServiceShutdown() error {
return nil
}
// Get returns the current daemon status snapshot.
// Get returns the current daemon status snapshot. When the daemon socket
// is unreachable (process down, socket missing, permission denied) it
// returns Status{Status: StatusDaemonUnavailable} instead of an error so
// the frontend's initial useStatus().refresh() picks up the same string
// the live event stream emits — the React overlay and per-screen gating
// then key off a single status enum without a parallel "error" path.
func (s *Peers) Get(ctx context.Context) (Status, error) {
cli, err := s.conn.Client()
if err != nil {
if isDaemonUnreachable(err) {
return Status{Status: StatusDaemonUnavailable}, nil
}
return Status{}, err
}
resp, err := cli.Status(ctx, &proto.StatusRequest{GetFullPeerStatus: true})
if err != nil {
if isDaemonUnreachable(err) {
return Status{Status: StatusDaemonUnavailable}, nil
}
return Status{}, err
}
return statusFromProto(resp), nil

View File

@@ -214,6 +214,9 @@ func (t *Tray) reapplyMenuState() {
if t.debugItem != nil {
t.debugItem.SetEnabled(!daemonUnavailable)
}
if t.profileSubmenuItem != nil {
t.profileSubmenuItem.SetEnabled(!daemonUnavailable)
}
if daemonVersion != "" && t.daemonVersionItem != nil {
t.daemonVersionItem.SetLabel(t.loc.T("tray.menu.daemonVersion", "version", daemonVersion))
}
@@ -511,6 +514,9 @@ func (t *Tray) applyStatus(st services.Status) {
if t.debugItem != nil {
t.debugItem.SetEnabled(!daemonUnavailable)
}
if t.profileSubmenuItem != nil {
t.profileSubmenuItem.SetEnabled(!daemonUnavailable)
}
// Refresh the Profiles submenu on every status-text transition: the
// daemon does not emit an active-profile event, so the startup race
// (UI loads profiles before autoconnect picks the persisted profile)