diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index 0043f228e..8e044fa1d 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -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 diff --git a/client/ui/quickactions.go b/client/ui/quickactions.go new file mode 100644 index 000000000..f9ae29cf1 --- /dev/null +++ b/client/ui/quickactions.go @@ -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() +} diff --git a/client/ui/signal_unix.go b/client/ui/signal_unix.go new file mode 100644 index 000000000..99de99f0f --- /dev/null +++ b/client/ui/signal_unix.go @@ -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) +} diff --git a/client/ui/signal_windows.go b/client/ui/signal_windows.go new file mode 100644 index 000000000..f4eed4f6a --- /dev/null +++ b/client/ui/signal_windows.go @@ -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 { +}