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
4 changed files with 272 additions and 26 deletions

View File

@@ -85,21 +85,22 @@ func main() {
// Create the service client (this also builds the settings or networks UI if requested). // Create the service client (this also builds the settings or networks UI if requested).
client := newServiceClient(&newServiceClientArgs{ client := newServiceClient(&newServiceClientArgs{
addr: flags.daemonAddr, addr: flags.daemonAddr,
logFile: logFile, logFile: logFile,
app: a, app: a,
showSettings: flags.showSettings, showSettings: flags.showSettings,
showNetworks: flags.showNetworks, showNetworks: flags.showNetworks,
showLoginURL: flags.showLoginURL, showLoginURL: flags.showLoginURL,
showDebug: flags.showDebug, showDebug: flags.showDebug,
showProfiles: flags.showProfiles, showProfiles: flags.showProfiles,
showQuickActions: flags.showQuickActions,
}) })
// Watch for theme/settings changes to update the icon. // Watch for theme/settings changes to update the icon.
go watchSettingsChanges(a, client) go watchSettingsChanges(a, client)
// Run in window mode if any UI flag was set. // 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() a.Run()
return return
} }
@@ -111,23 +112,29 @@ func main() {
return return
} }
if running { 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 return
} }
client.setupSignalHandler(client.ctx)
client.setDefaultFonts() client.setDefaultFonts()
systray.Run(client.onTrayReady, client.onTrayExit) systray.Run(client.onTrayReady, client.onTrayExit)
} }
type cliFlags struct { type cliFlags struct {
daemonAddr string daemonAddr string
showSettings bool showSettings bool
showNetworks bool showNetworks bool
showProfiles bool showProfiles bool
showDebug bool showDebug bool
showLoginURL bool showLoginURL bool
errorMsg string showQuickActions bool
saveLogsInFile bool errorMsg string
saveLogsInFile bool
} }
// parseFlags reads and returns all needed command-line flags. // 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.showNetworks, "networks", false, "run networks window")
flag.BoolVar(&flags.showProfiles, "profiles", false, "run profiles window") flag.BoolVar(&flags.showProfiles, "profiles", false, "run profiles window")
flag.BoolVar(&flags.showDebug, "debug", false, "run debug 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.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.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") flag.BoolVar(&flags.showLoginURL, "login-url", false, "show login URL in a popup window")
@@ -287,6 +295,7 @@ type serviceClient struct {
showNetworks bool showNetworks bool
wNetworks fyne.Window wNetworks fyne.Window
wProfiles fyne.Window wProfiles fyne.Window
wQuickActions fyne.Window
eventManager *event.Manager eventManager *event.Manager
@@ -304,14 +313,15 @@ type menuHandler struct {
} }
type newServiceClientArgs struct { type newServiceClientArgs struct {
addr string addr string
logFile string logFile string
app fyne.App app fyne.App
showSettings bool showSettings bool
showNetworks bool showNetworks bool
showDebug bool showDebug bool
showLoginURL bool showLoginURL bool
showProfiles bool showProfiles bool
showQuickActions bool
} }
// newServiceClient instance constructor // newServiceClient instance constructor
@@ -347,6 +357,8 @@ func newServiceClient(args *newServiceClientArgs) *serviceClient {
s.showDebugUI() s.showDebugUI()
case args.showProfiles: case args.showProfiles:
s.showProfilesUI() s.showProfilesUI()
case args.showQuickActions:
s.showQuickActionsUI()
} }
return s 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 {
}