Compare commits

..

1 Commits

Author SHA1 Message Date
Viktor Liu
5fadbeb769 Open quick settings window if netbird-ui is already running 2025-10-20 15:49:28 +02:00
9 changed files with 316 additions and 49 deletions

View File

@@ -85,21 +85,22 @@ func main() {
// Create the service client (this also builds the settings or networks UI if requested).
client := newServiceClient(&newServiceClientArgs{
addr: flags.daemonAddr,
logFile: logFile,
app: a,
showSettings: flags.showSettings,
showNetworks: flags.showNetworks,
showLoginURL: flags.showLoginURL,
showDebug: flags.showDebug,
showProfiles: flags.showProfiles,
addr: flags.daemonAddr,
logFile: logFile,
app: a,
showSettings: flags.showSettings,
showNetworks: flags.showNetworks,
showLoginURL: flags.showLoginURL,
showDebug: flags.showDebug,
showProfiles: flags.showProfiles,
showQuickActions: flags.showQuickActions,
})
// Watch for theme/settings changes to update the icon.
go watchSettingsChanges(a, client)
// Run in window mode if any UI flag was set.
if flags.showSettings || flags.showNetworks || flags.showDebug || flags.showLoginURL || flags.showProfiles {
if flags.showSettings || flags.showNetworks || flags.showDebug || flags.showLoginURL || flags.showProfiles || flags.showQuickActions {
a.Run()
return
}
@@ -111,23 +112,29 @@ func main() {
return
}
if running {
log.Warnf("another process is running with pid %d, exiting", pid)
log.Infof("another process is running with pid %d, sending signal to show window", pid)
if err := sendShowWindowSignal(pid); err != nil {
log.Errorf("send signal to running instance: %v", err)
}
return
}
client.setupSignalHandler(client.ctx)
client.setDefaultFonts()
systray.Run(client.onTrayReady, client.onTrayExit)
}
type cliFlags struct {
daemonAddr string
showSettings bool
showNetworks bool
showProfiles bool
showDebug bool
showLoginURL bool
errorMsg string
saveLogsInFile bool
daemonAddr string
showSettings bool
showNetworks bool
showProfiles bool
showDebug bool
showLoginURL bool
showQuickActions bool
errorMsg string
saveLogsInFile bool
}
// parseFlags reads and returns all needed command-line flags.
@@ -143,6 +150,7 @@ func parseFlags() *cliFlags {
flag.BoolVar(&flags.showNetworks, "networks", false, "run networks window")
flag.BoolVar(&flags.showProfiles, "profiles", false, "run profiles window")
flag.BoolVar(&flags.showDebug, "debug", false, "run debug window")
flag.BoolVar(&flags.showQuickActions, "quick-actions", false, "run quick actions window")
flag.StringVar(&flags.errorMsg, "error-msg", "", "displays an error message window")
flag.BoolVar(&flags.saveLogsInFile, "use-log-file", false, fmt.Sprintf("save logs in a file: %s/netbird-ui-PID.log", os.TempDir()))
flag.BoolVar(&flags.showLoginURL, "login-url", false, "show login URL in a popup window")
@@ -287,6 +295,7 @@ type serviceClient struct {
showNetworks bool
wNetworks fyne.Window
wProfiles fyne.Window
wQuickActions fyne.Window
eventManager *event.Manager
@@ -304,14 +313,15 @@ type menuHandler struct {
}
type newServiceClientArgs struct {
addr string
logFile string
app fyne.App
showSettings bool
showNetworks bool
showDebug bool
showLoginURL bool
showProfiles bool
addr string
logFile string
app fyne.App
showSettings bool
showNetworks bool
showDebug bool
showLoginURL bool
showProfiles bool
showQuickActions bool
}
// newServiceClient instance constructor
@@ -347,6 +357,8 @@ func newServiceClient(args *newServiceClientArgs) *serviceClient {
s.showDebugUI()
case args.showProfiles:
s.showProfilesUI()
case args.showQuickActions:
s.showQuickActionsUI()
}
return s

101
client/ui/quickactions.go Normal file
View File

@@ -0,0 +1,101 @@
//go:build !(linux && 386)
package main
import (
"context"
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/proto"
)
// showQuickActionsUI displays a simple window with connect/disconnect controls.
func (s *serviceClient) showQuickActionsUI() {
s.wQuickActions = s.app.NewWindow("NetBird")
s.wQuickActions.SetOnClosed(s.cancel)
statusLabel := widget.NewLabel("Status: Checking...")
connectBtn := widget.NewButton("Connect", nil)
disconnectBtn := widget.NewButton("Disconnect", nil)
updateUI := func() {
client, err := s.getSrvClient(defaultFailTimeout)
if err != nil {
log.Errorf("get service client: %v", err)
statusLabel.SetText("Status: Error connecting to daemon")
connectBtn.Disable()
disconnectBtn.Disable()
return
}
status, err := client.Status(context.Background(), &proto.StatusRequest{})
if err != nil {
log.Errorf("get status: %v", err)
statusLabel.SetText("Status: Error")
connectBtn.Disable()
disconnectBtn.Disable()
return
}
if status.Status == string(peer.StatusConnected) {
statusLabel.SetText("Status: Connected")
connectBtn.Disable()
disconnectBtn.Enable()
} else {
statusLabel.SetText("Status: Disconnected")
connectBtn.Enable()
disconnectBtn.Disable()
}
}
connectBtn.OnTapped = func() {
connectBtn.Disable()
statusLabel.SetText("Status: Connecting...")
go func() {
if err := s.menuUpClick(); err != nil {
log.Errorf("connect failed: %v", err)
statusLabel.SetText(fmt.Sprintf("Status: Error - %v", err))
}
updateUI()
}()
}
disconnectBtn.OnTapped = func() {
disconnectBtn.Disable()
statusLabel.SetText("Status: Disconnecting...")
go func() {
if err := s.menuDownClick(); err != nil {
log.Errorf("disconnect failed: %v", err)
statusLabel.SetText(fmt.Sprintf("Status: Error - %v", err))
}
updateUI()
}()
}
content := container.NewVBox(
layout.NewSpacer(),
statusLabel,
layout.NewSpacer(),
container.NewHBox(
layout.NewSpacer(),
connectBtn,
disconnectBtn,
layout.NewSpacer(),
),
layout.NewSpacer(),
)
s.wQuickActions.SetContent(content)
s.wQuickActions.Resize(fyne.NewSize(300, 150))
s.wQuickActions.SetFixedSize(true)
s.wQuickActions.Show()
updateUI()
}

76
client/ui/signal_unix.go Normal file
View File

@@ -0,0 +1,76 @@
//go:build !windows && !(linux && 386)
package main
import (
"context"
"os"
"os/exec"
"os/signal"
"syscall"
log "github.com/sirupsen/logrus"
)
// setupSignalHandler sets up a signal handler to listen for SIGUSR1.
// When received, it opens the quick actions window.
func (s *serviceClient) setupSignalHandler(ctx context.Context) {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGUSR1)
go func() {
for {
select {
case <-ctx.Done():
return
case <-sigChan:
log.Info("received SIGUSR1 signal, opening quick actions window")
s.openQuickActions()
}
}
}()
}
// openQuickActions opens the quick actions window by spawning a new process.
func (s *serviceClient) openQuickActions() {
proc, err := os.Executable()
if err != nil {
log.Errorf("get executable path: %v", err)
return
}
cmd := exec.CommandContext(s.ctx, proc,
"--quick-actions=true",
"--daemon-addr="+s.addr,
)
if out := s.attachOutput(cmd); out != nil {
defer func() {
if err := out.Close(); err != nil {
log.Errorf("close log file %s: %v", s.logFile, err)
}
}()
}
log.Infof("running command: %s --quick-actions=true --daemon-addr=%s", proc, s.addr)
if err := cmd.Start(); err != nil {
log.Errorf("start quick actions window: %v", err)
return
}
go func() {
if err := cmd.Wait(); err != nil {
log.Debugf("quick actions window exited: %v", err)
}
}()
}
// sendShowWindowSignal sends SIGUSR1 to the specified PID.
func sendShowWindowSignal(pid int32) error {
process, err := os.FindProcess(int(pid))
if err != nil {
return err
}
return process.Signal(syscall.SIGUSR1)
}

View File

@@ -0,0 +1,57 @@
//go:build windows
package main
import (
"context"
"os"
"os/exec"
log "github.com/sirupsen/logrus"
)
// setupSignalHandler sets up signal handling for Windows.
// Windows doesn't support SIGUSR1, so this is currently a no-op.
// Future enhancement: implement Windows-specific IPC (named events, named pipes, etc.)
func (s *serviceClient) setupSignalHandler(ctx context.Context) {
// TODO: see how debug bundle is generated on signal in windows
log.Debug("signal handler not yet implemented for Windows")
}
// openQuickActions opens the quick actions window by spawning a new process.
func (s *serviceClient) openQuickActions() {
proc, err := os.Executable()
if err != nil {
log.Errorf("get executable path: %v", err)
return
}
cmd := exec.CommandContext(s.ctx, proc,
"--quick-actions=true",
"--daemon-addr="+s.addr,
)
if out := s.attachOutput(cmd); out != nil {
defer func() {
if err := out.Close(); err != nil {
log.Errorf("close log file %s: %v", s.logFile, err)
}
}()
}
log.Infof("running command: %s --quick-actions=true --daemon-addr=%s", proc, s.addr)
if err := cmd.Start(); err != nil {
log.Errorf("start quick actions window: %v", err)
return
}
go func() {
if err := cmd.Wait(); err != nil {
log.Debugf("quick actions window exited: %v", err)
}
}()
}
func sendShowWindowSignal(pid int32) error {
}

View File

@@ -11,7 +11,6 @@ import (
"time"
"github.com/google/uuid"
"github.com/gorilla/mux"
grpcMiddleware "github.com/grpc-ecosystem/go-grpc-middleware/v2"
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/realip"
log "github.com/sirupsen/logrus"
@@ -19,11 +18,12 @@ import (
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/keepalive"
"github.com/netbirdio/management-integrations/integrations"
"github.com/netbirdio/netbird/encryption"
"github.com/netbirdio/netbird/formatter/hook"
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
"github.com/netbirdio/netbird/management/server"
"github.com/netbirdio/netbird/management/server/activity"
activitystore "github.com/netbirdio/netbird/management/server/activity/store"
nbContext "github.com/netbirdio/netbird/management/server/context"
nbhttp "github.com/netbirdio/netbird/management/server/http"
"github.com/netbirdio/netbird/management/server/store"
@@ -31,8 +31,6 @@ import (
mgmtProto "github.com/netbirdio/netbird/shared/management/proto"
)
const apiPrefix = "/api"
var (
kaep = keepalive.EnforcementPolicy{
MinTime: 15 * time.Second,
@@ -70,18 +68,32 @@ func (s *BaseServer) Store() store.Store {
func (s *BaseServer) EventStore() activity.Store {
return Create(s, func() activity.Store {
store, err := activitystore.NewSqlStore(context.Background(), s.config.Datadir, s.config.DataStoreEncryptionKey)
integrationMetrics, err := integrations.InitIntegrationMetrics(context.Background(), s.Metrics())
if err != nil {
log.Fatalf("failed to initialize integration metrics: %v", err)
}
eventStore, key, err := integrations.InitEventStore(context.Background(), s.config.Datadir, s.config.DataStoreEncryptionKey, integrationMetrics)
if err != nil {
log.Fatalf("failed to initialize event store: %v", err)
}
return store
if s.config.DataStoreEncryptionKey != key {
log.WithContext(context.Background()).Infof("update config with activity store key")
s.config.DataStoreEncryptionKey = key
err := updateMgmtConfig(context.Background(), nbconfig.MgmtConfigPath, s.config)
if err != nil {
log.Fatalf("failed to update config with activity store: %v", err)
}
}
return eventStore
})
}
func (s *BaseServer) APIHandler() http.Handler {
return Create(s, func() http.Handler {
httpAPIHandler, err := nbhttp.NewAPIHandler(s.Router(), s.AccountManager(), s.NetworksManager(), s.ResourcesManager(), s.RoutesManager(), s.GroupsManager(), s.GeoLocationManager(), s.AuthManager(), s.Metrics(), s.PermissionsManager(), s.SettingsManager())
httpAPIHandler, err := nbhttp.NewAPIHandler(context.Background(), s.AccountManager(), s.NetworksManager(), s.ResourcesManager(), s.RoutesManager(), s.GroupsManager(), s.GeoLocationManager(), s.AuthManager(), s.Metrics(), s.IntegratedValidator(), s.ProxyController(), s.PermissionsManager(), s.PeersManager(), s.SettingsManager())
if err != nil {
log.Fatalf("failed to create API handler: %v", err)
}
@@ -89,15 +101,6 @@ func (s *BaseServer) APIHandler() http.Handler {
})
}
func (s *BaseServer) Router() *mux.Router {
return Create(s, func() *mux.Router {
rootRouter := mux.NewRouter()
prefix := apiPrefix
router := rootRouter.PathPrefix(prefix).Subrouter()
return router
})
}
func (s *BaseServer) GRPCServer() *grpc.Server {
return Create(s, func() *grpc.Server {
trustedPeers := s.config.ReverseProxy.TrustedPeers

View File

@@ -26,8 +26,7 @@ func (s *BaseServer) IntegratedValidator() integrated_validator.IntegratedValida
context.Background(),
s.PeersManager(),
s.SettingsManager(),
s.EventStore(),
nil)
s.EventStore())
if err != nil {
log.Errorf("failed to create integrated peer validator: %v", err)
}

View File

@@ -35,7 +35,7 @@ func (s *BaseServer) GeoLocationManager() geolocation.Geolocation {
func (s *BaseServer) PermissionsManager() permissions.Manager {
return Create(s, func() permissions.Manager {
return permissions.NewManager(s.Store())
return integrations.InitPermissionsManager(s.Store())
})
}

View File

@@ -936,8 +936,7 @@ func (s *GRPCServer) GetPKCEAuthorizationFlow(ctx context.Context, req *proto.En
},
}
// flowInfoResp := s.integratedPeerValidator.ValidateFlowResponse(ctx, peerKey.String(), initInfoFlow)
flowInfoResp := initInfoFlow
flowInfoResp := s.integratedPeerValidator.ValidateFlowResponse(ctx, peerKey.String(), initInfoFlow)
encryptedResp, err := encryption.EncryptMessage(peerKey, s.wgKey, flowInfoResp)
if err != nil {

View File

@@ -1,14 +1,19 @@
package http
import (
"context"
"fmt"
"net/http"
"github.com/gorilla/mux"
"github.com/rs/cors"
"github.com/netbirdio/management-integrations/integrations"
"github.com/netbirdio/netbird/management/server/account"
"github.com/netbirdio/netbird/management/server/settings"
"github.com/netbirdio/netbird/management/server/integrations/port_forwarding"
"github.com/netbirdio/netbird/management/server/permissions"
"github.com/netbirdio/netbird/management/server/auth"
@@ -25,15 +30,19 @@ import (
"github.com/netbirdio/netbird/management/server/http/handlers/setup_keys"
"github.com/netbirdio/netbird/management/server/http/handlers/users"
"github.com/netbirdio/netbird/management/server/http/middleware"
"github.com/netbirdio/netbird/management/server/integrations/integrated_validator"
nbnetworks "github.com/netbirdio/netbird/management/server/networks"
"github.com/netbirdio/netbird/management/server/networks/resources"
"github.com/netbirdio/netbird/management/server/networks/routers"
nbpeers "github.com/netbirdio/netbird/management/server/peers"
"github.com/netbirdio/netbird/management/server/telemetry"
)
const apiPrefix = "/api"
// NewAPIHandler creates the Management service HTTP API handler registering all the available endpoints.
func NewAPIHandler(
router *mux.Router,
ctx context.Context,
accountManager account.Manager,
networksManager nbnetworks.Manager,
resourceManager resources.Manager,
@@ -42,7 +51,10 @@ func NewAPIHandler(
LocationManager geolocation.Geolocation,
authManager auth.Manager,
appMetrics telemetry.AppMetrics,
integratedValidator integrated_validator.IntegratedValidator,
proxyController port_forwarding.Controller,
permissionsManager permissions.Manager,
peersManager nbpeers.Manager,
settingsManager settings.Manager,
) (http.Handler, error) {
@@ -55,10 +67,18 @@ func NewAPIHandler(
corsMiddleware := cors.AllowAll()
rootRouter := mux.NewRouter()
metricsMiddleware := appMetrics.HTTPMiddleware()
prefix := apiPrefix
router := rootRouter.PathPrefix(prefix).Subrouter()
router.Use(metricsMiddleware.Handler, corsMiddleware.Handler, authMiddleware.Handler)
if _, err := integrations.RegisterHandlers(ctx, prefix, router, accountManager, integratedValidator, appMetrics.GetMeter(), permissionsManager, peersManager, proxyController, settingsManager); err != nil {
return nil, fmt.Errorf("register integrations endpoints: %w", err)
}
accounts.AddEndpoints(accountManager, settingsManager, router)
peers.AddEndpoints(accountManager, router)
users.AddEndpoints(accountManager, router)
@@ -72,5 +92,5 @@ func NewAPIHandler(
events.AddEndpoints(accountManager, router)
networks.AddEndpoints(networksManager, resourceManager, routerManager, groupsManager, accountManager, router)
return router, nil
return rootRouter, nil
}