mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-31 03:51:29 +02:00
ui: detect outdated daemon via WailsUIReady probe
Add a no-op WailsUIReady RPC the UI probes once at startup. A reachable daemon that returns Unimplemented predates this UI and is too old to drive it; the probe distinguishes that from an unreachable daemon. On an outdated daemon the UI fires a localized OS notification, since the main window may not open to show an in-app error. Proto regenerated with protoc 33.1 to keep the v6.33.1 header.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -149,6 +149,11 @@ service DaemonService {
|
||||
|
||||
// ExposeService exposes a local port via the NetBird reverse proxy
|
||||
rpc ExposeService(ExposeServiceRequest) returns (stream ExposeServiceEvent) {}
|
||||
|
||||
// WailsUIReady is a no-op probe the Wails UI calls once at startup. The UI
|
||||
// only cares whether the daemon implements it: an Unimplemented response
|
||||
// means the daemon predates this UI and is too old to drive it.
|
||||
rpc WailsUIReady(WailsUIReadyRequest) returns (WailsUIReadyResponse) {}
|
||||
}
|
||||
|
||||
|
||||
@@ -785,6 +790,10 @@ message LogoutRequest {
|
||||
|
||||
message LogoutResponse {}
|
||||
|
||||
message WailsUIReadyRequest {}
|
||||
|
||||
message WailsUIReadyResponse {}
|
||||
|
||||
message GetFeaturesRequest{}
|
||||
|
||||
message GetFeaturesResponse{
|
||||
|
||||
@@ -63,6 +63,7 @@ const (
|
||||
DaemonService_StopCPUProfile_FullMethodName = "/daemon.DaemonService/StopCPUProfile"
|
||||
DaemonService_GetInstallerResult_FullMethodName = "/daemon.DaemonService/GetInstallerResult"
|
||||
DaemonService_ExposeService_FullMethodName = "/daemon.DaemonService/ExposeService"
|
||||
DaemonService_WailsUIReady_FullMethodName = "/daemon.DaemonService/WailsUIReady"
|
||||
)
|
||||
|
||||
// DaemonServiceClient is the client API for DaemonService service.
|
||||
@@ -164,6 +165,10 @@ type DaemonServiceClient interface {
|
||||
GetInstallerResult(ctx context.Context, in *InstallerResultRequest, opts ...grpc.CallOption) (*InstallerResultResponse, error)
|
||||
// ExposeService exposes a local port via the NetBird reverse proxy
|
||||
ExposeService(ctx context.Context, in *ExposeServiceRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ExposeServiceEvent], error)
|
||||
// WailsUIReady is a no-op probe the Wails UI calls once at startup. The UI
|
||||
// only cares whether the daemon implements it: an Unimplemented response
|
||||
// means the daemon predates this UI and is too old to drive it.
|
||||
WailsUIReady(ctx context.Context, in *WailsUIReadyRequest, opts ...grpc.CallOption) (*WailsUIReadyResponse, error)
|
||||
}
|
||||
|
||||
type daemonServiceClient struct {
|
||||
@@ -650,6 +655,16 @@ func (c *daemonServiceClient) ExposeService(ctx context.Context, in *ExposeServi
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type DaemonService_ExposeServiceClient = grpc.ServerStreamingClient[ExposeServiceEvent]
|
||||
|
||||
func (c *daemonServiceClient) WailsUIReady(ctx context.Context, in *WailsUIReadyRequest, opts ...grpc.CallOption) (*WailsUIReadyResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(WailsUIReadyResponse)
|
||||
err := c.cc.Invoke(ctx, DaemonService_WailsUIReady_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// DaemonServiceServer is the server API for DaemonService service.
|
||||
// All implementations must embed UnimplementedDaemonServiceServer
|
||||
// for forward compatibility.
|
||||
@@ -749,6 +764,10 @@ type DaemonServiceServer interface {
|
||||
GetInstallerResult(context.Context, *InstallerResultRequest) (*InstallerResultResponse, error)
|
||||
// ExposeService exposes a local port via the NetBird reverse proxy
|
||||
ExposeService(*ExposeServiceRequest, grpc.ServerStreamingServer[ExposeServiceEvent]) error
|
||||
// WailsUIReady is a no-op probe the Wails UI calls once at startup. The UI
|
||||
// only cares whether the daemon implements it: an Unimplemented response
|
||||
// means the daemon predates this UI and is too old to drive it.
|
||||
WailsUIReady(context.Context, *WailsUIReadyRequest) (*WailsUIReadyResponse, error)
|
||||
mustEmbedUnimplementedDaemonServiceServer()
|
||||
}
|
||||
|
||||
@@ -891,6 +910,9 @@ func (UnimplementedDaemonServiceServer) GetInstallerResult(context.Context, *Ins
|
||||
func (UnimplementedDaemonServiceServer) ExposeService(*ExposeServiceRequest, grpc.ServerStreamingServer[ExposeServiceEvent]) error {
|
||||
return status.Error(codes.Unimplemented, "method ExposeService not implemented")
|
||||
}
|
||||
func (UnimplementedDaemonServiceServer) WailsUIReady(context.Context, *WailsUIReadyRequest) (*WailsUIReadyResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method WailsUIReady not implemented")
|
||||
}
|
||||
func (UnimplementedDaemonServiceServer) mustEmbedUnimplementedDaemonServiceServer() {}
|
||||
func (UnimplementedDaemonServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
@@ -1676,6 +1698,24 @@ func _DaemonService_ExposeService_Handler(srv interface{}, stream grpc.ServerStr
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type DaemonService_ExposeServiceServer = grpc.ServerStreamingServer[ExposeServiceEvent]
|
||||
|
||||
func _DaemonService_WailsUIReady_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(WailsUIReadyRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(DaemonServiceServer).WailsUIReady(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: DaemonService_WailsUIReady_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(DaemonServiceServer).WailsUIReady(ctx, req.(*WailsUIReadyRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// DaemonService_ServiceDesc is the grpc.ServiceDesc for DaemonService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@@ -1843,6 +1883,10 @@ var DaemonService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "GetInstallerResult",
|
||||
Handler: _DaemonService_GetInstallerResult_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "WailsUIReady",
|
||||
Handler: _DaemonService_WailsUIReady_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
|
||||
@@ -2121,6 +2121,12 @@ func (s *Server) GetFeatures(ctx context.Context, msg *proto.GetFeaturesRequest)
|
||||
return features, nil
|
||||
}
|
||||
|
||||
// WailsUIReady is a no-op the Wails UI probes at startup; merely answering it
|
||||
// (rather than returning Unimplemented) tells the UI this daemon is new enough.
|
||||
func (s *Server) WailsUIReady(context.Context, *proto.WailsUIReadyRequest) (*proto.WailsUIReadyResponse, error) {
|
||||
return &proto.WailsUIReadyResponse{}, nil
|
||||
}
|
||||
|
||||
// checkDisableAdvancedView reports the MDM-policy directive for the
|
||||
// upcoming UI's advanced-view section. Tristate: returns nil when no
|
||||
// MDM directive is set so the UI applies its own default; returns
|
||||
|
||||
19
client/ui/frontend/src/lib/compat.ts
Normal file
19
client/ui/frontend/src/lib/compat.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Compat } from "@bindings/services";
|
||||
|
||||
let cached: boolean | null = null;
|
||||
|
||||
/**
|
||||
* isDaemonCompatible probes whether the running daemon implements the WailsUIReady
|
||||
* RPC. A false result means the daemon predates this UI (Unimplemented) and is too
|
||||
* old to drive it. The Go side returns an error instead when the daemon is simply
|
||||
* unreachable, so a throw here is NOT an outdated daemon — treat it as "unknown"
|
||||
* and let the normal connection flow report it.
|
||||
*
|
||||
* The result is cached for the session: daemon identity does not change without a
|
||||
* UI restart, and a freshly started daemon is reachable again under the same socket.
|
||||
*/
|
||||
export async function isDaemonCompatible(): Promise<boolean> {
|
||||
if (cached !== null) return cached;
|
||||
cached = await Compat.DaemonReady();
|
||||
return cached;
|
||||
}
|
||||
@@ -107,6 +107,12 @@
|
||||
"tray.menu.quit": {
|
||||
"message": "NetBird beenden"
|
||||
},
|
||||
"notify.daemonOutdated.title": {
|
||||
"message": "NetBird-Dienst ist veraltet"
|
||||
},
|
||||
"notify.daemonOutdated.body": {
|
||||
"message": "Aktualisieren Sie den NetBird-Dienst, um diese App zu verwenden."
|
||||
},
|
||||
"notify.update.title": {
|
||||
"message": "NetBird-Update verfügbar"
|
||||
},
|
||||
|
||||
@@ -143,6 +143,14 @@
|
||||
"message": "Quit NetBird",
|
||||
"description": "Tray menu item that fully exits the app and removes the tray icon. Keep short."
|
||||
},
|
||||
"notify.daemonOutdated.title": {
|
||||
"message": "NetBird service is outdated",
|
||||
"description": "Title of the OS desktop notification shown when the running daemon is too old for this UI."
|
||||
},
|
||||
"notify.daemonOutdated.body": {
|
||||
"message": "Update the NetBird service to use this app.",
|
||||
"description": "Body of the desktop notification telling the user to upgrade the daemon."
|
||||
},
|
||||
"notify.update.title": {
|
||||
"message": "NetBird update available",
|
||||
"description": "Title of the OS desktop notification shown when an app update is available."
|
||||
|
||||
@@ -107,6 +107,12 @@
|
||||
"tray.menu.quit": {
|
||||
"message": "Salir de NetBird"
|
||||
},
|
||||
"notify.daemonOutdated.title": {
|
||||
"message": "El servicio de NetBird está desactualizado"
|
||||
},
|
||||
"notify.daemonOutdated.body": {
|
||||
"message": "Actualice el servicio de NetBird para usar esta aplicación."
|
||||
},
|
||||
"notify.update.title": {
|
||||
"message": "Actualización de NetBird disponible"
|
||||
},
|
||||
|
||||
@@ -107,6 +107,12 @@
|
||||
"tray.menu.quit": {
|
||||
"message": "Quitter NetBird"
|
||||
},
|
||||
"notify.daemonOutdated.title": {
|
||||
"message": "Le service NetBird est obsolète"
|
||||
},
|
||||
"notify.daemonOutdated.body": {
|
||||
"message": "Mettez à jour le service NetBird pour utiliser cette application."
|
||||
},
|
||||
"notify.update.title": {
|
||||
"message": "Mise à jour de NetBird disponible"
|
||||
},
|
||||
|
||||
@@ -107,6 +107,12 @@
|
||||
"tray.menu.quit": {
|
||||
"message": "NetBird bezárása"
|
||||
},
|
||||
"notify.daemonOutdated.title": {
|
||||
"message": "A NetBird szolgáltatás elavult"
|
||||
},
|
||||
"notify.daemonOutdated.body": {
|
||||
"message": "Frissítsd a NetBird szolgáltatást az alkalmazás használatához."
|
||||
},
|
||||
"notify.update.title": {
|
||||
"message": "NetBird frissítés elérhető"
|
||||
},
|
||||
|
||||
@@ -107,6 +107,12 @@
|
||||
"tray.menu.quit": {
|
||||
"message": "Esci da NetBird"
|
||||
},
|
||||
"notify.daemonOutdated.title": {
|
||||
"message": "Il servizio NetBird è obsoleto"
|
||||
},
|
||||
"notify.daemonOutdated.body": {
|
||||
"message": "Aggiorna il servizio NetBird per usare questa app."
|
||||
},
|
||||
"notify.update.title": {
|
||||
"message": "Aggiornamento NetBird disponibile"
|
||||
},
|
||||
|
||||
@@ -107,6 +107,12 @@
|
||||
"tray.menu.quit": {
|
||||
"message": "Sair do NetBird"
|
||||
},
|
||||
"notify.daemonOutdated.title": {
|
||||
"message": "O serviço NetBird está desatualizado"
|
||||
},
|
||||
"notify.daemonOutdated.body": {
|
||||
"message": "Atualize o serviço NetBird para usar este aplicativo."
|
||||
},
|
||||
"notify.update.title": {
|
||||
"message": "Atualização do NetBird disponível"
|
||||
},
|
||||
|
||||
@@ -107,6 +107,12 @@
|
||||
"tray.menu.quit": {
|
||||
"message": "Выйти из NetBird"
|
||||
},
|
||||
"notify.daemonOutdated.title": {
|
||||
"message": "Служба NetBird устарела"
|
||||
},
|
||||
"notify.daemonOutdated.body": {
|
||||
"message": "Обновите службу NetBird, чтобы использовать это приложение."
|
||||
},
|
||||
"notify.update.title": {
|
||||
"message": "Доступно обновление NetBird"
|
||||
},
|
||||
|
||||
@@ -107,6 +107,12 @@
|
||||
"tray.menu.quit": {
|
||||
"message": "退出 NetBird"
|
||||
},
|
||||
"notify.daemonOutdated.title": {
|
||||
"message": "NetBird 服务版本过旧"
|
||||
},
|
||||
"notify.daemonOutdated.body": {
|
||||
"message": "请更新 NetBird 服务以使用此应用。"
|
||||
},
|
||||
"notify.update.title": {
|
||||
"message": "NetBird 有可用更新"
|
||||
},
|
||||
|
||||
@@ -64,6 +64,7 @@ type registeredServices struct {
|
||||
update *services.Update
|
||||
daemonFeed *services.DaemonFeed
|
||||
notifier *notifications.NotificationService
|
||||
compat *services.Compat
|
||||
profileSwitcher *services.ProfileSwitcher
|
||||
bundle *i18n.Bundle
|
||||
prefStore *preferences.Store
|
||||
@@ -103,6 +104,7 @@ func main() {
|
||||
update := services.NewUpdate(conn, updaterHolder)
|
||||
daemonFeed := services.NewDaemonFeed(conn, app.Event, updaterHolder, debugLog)
|
||||
notifier := notifications.New()
|
||||
compat := services.NewCompat(conn)
|
||||
// macOS shows no toast until permission is requested. Run it after
|
||||
// ApplicationStarted so the notifier's Startup has initialised the
|
||||
// notification-center delegate. No-op on Linux/Windows (stubs report
|
||||
@@ -132,6 +134,7 @@ func main() {
|
||||
update: update,
|
||||
daemonFeed: daemonFeed,
|
||||
notifier: notifier,
|
||||
compat: compat,
|
||||
profileSwitcher: profileSwitcher,
|
||||
bundle: bundle,
|
||||
prefStore: prefStore,
|
||||
@@ -190,6 +193,10 @@ func main() {
|
||||
// Mint). ApplicationStarted fires after the startup loop, so the bus is up.
|
||||
app.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(*application.ApplicationEvent) {
|
||||
daemonFeed.Watch(context.Background())
|
||||
// Probe daemon compatibility once the notifier bus is up; an outdated
|
||||
// daemon may keep the main window from showing, so the OS toast is the
|
||||
// only reliable signal the user gets.
|
||||
go notifyIfDaemonOutdated(compat, notifier, localizer)
|
||||
})
|
||||
|
||||
if err := app.Run(); err != nil {
|
||||
@@ -322,6 +329,7 @@ func registerServices(app *application.App, conn *Conn, s registeredServices) {
|
||||
app.RegisterService(application.NewService(services.NewAutostart(app.Autostart)))
|
||||
app.RegisterService(application.NewService(services.NewVersion()))
|
||||
app.RegisterService(application.NewService(services.NewUILog()))
|
||||
app.RegisterService(application.NewService(s.compat))
|
||||
}
|
||||
|
||||
// newMainWindow creates the hidden main window, sized to the user's last view
|
||||
|
||||
40
client/ui/services/compat.go
Normal file
40
client/ui/services/compat.go
Normal file
@@ -0,0 +1,40 @@
|
||||
//go:build !android && !ios && !freebsd && !js
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/netbirdio/netbird/client/proto"
|
||||
)
|
||||
|
||||
// Compat answers whether the running daemon is new enough to drive this UI.
|
||||
type Compat struct {
|
||||
conn DaemonConn
|
||||
}
|
||||
|
||||
func NewCompat(conn DaemonConn) *Compat {
|
||||
return &Compat{conn: conn}
|
||||
}
|
||||
|
||||
// DaemonReady probes the WailsUIReady RPC once. A true result means the daemon
|
||||
// implements it and is compatible. An Unimplemented response means the daemon
|
||||
// predates this UI and is too old; the caller should surface an upgrade prompt.
|
||||
// Any other error (daemon not running, transport failure) is returned so the
|
||||
// frontend can tell "outdated" apart from "not reachable".
|
||||
func (c *Compat) DaemonReady(ctx context.Context) (bool, error) {
|
||||
client, err := c.conn.Client()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if _, err := client.WailsUIReady(ctx, &proto.WailsUIReadyRequest{}); err != nil {
|
||||
if st, ok := status.FromError(err); ok && st.Code() == codes.Unimplemented {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
@@ -3,10 +3,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/wailsapp/wails/v3/pkg/services/notifications"
|
||||
|
||||
"github.com/netbirdio/netbird/client/ui/services"
|
||||
)
|
||||
|
||||
const notifyIDDaemonOutdated = "netbird-daemon-outdated"
|
||||
|
||||
// sendFn fits both NotificationService.SendNotification and SendNotificationWithActions.
|
||||
type sendFn func(notifications.NotificationOptions) error
|
||||
|
||||
@@ -31,3 +37,22 @@ func safeSendNotification(send sendFn, what string, opts notifications.Notificat
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// notifyIfDaemonOutdated probes the daemon once and fires an OS toast when it
|
||||
// is reachable but too old for this UI. A probe error means the daemon isn't
|
||||
// reachable (not outdated), so it is left to the normal connection flow.
|
||||
func notifyIfDaemonOutdated(compat *services.Compat, notifier *notifications.NotificationService, loc *Localizer) {
|
||||
ready, err := compat.DaemonReady(context.Background())
|
||||
if err != nil {
|
||||
log.Debugf("daemon compatibility probe: %v", err)
|
||||
return
|
||||
}
|
||||
if ready {
|
||||
return
|
||||
}
|
||||
_ = safeSendNotification(notifier.SendNotification, "daemon-outdated", notifications.NotificationOptions{
|
||||
ID: notifyIDDaemonOutdated,
|
||||
Title: loc.T("notify.daemonOutdated.title"),
|
||||
Body: loc.T("notify.daemonOutdated.body"),
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user