mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-16 07:16:38 +00:00
[client] Add quick actions window (#4717)
* Open quick settings window if netbird-ui is already running * [client-ui] fix connection status comparison * [client-ui] modularize quick actions code * [client-ui] add netbird-disconnected logo * [client-ui] change quickactions UI It now displays the NetBird logo and a single button with a round icon * [client-ui] add hint message to quick actions screen This also updates fyne to v2.7.0 * [client-ui] remove unnecessary default clause * [client-ui] remove commented code * [client-ui] remove unused dependency * [client-ui] close quick actions on connection change * [client-ui] add function to get image from embed resources * [client] Return error when calling sendShowWindowSignal from Windows * [client-ui] Add commentary on empty OnTapped function for toggleConnectionButton * [client-ui] Fix tests * [client-ui] Add context to menuUpClick call * [client-ui] Pass serviceClient app as parameter To use its clipboard rather than the window's when showing the upload success dialog * [client-ui] Replace for select with for range chan * [client-ui] Replace settings change listener channel Settings now accept a function callback * [client-ui] Add missing iconAboutDisconnected to icons_windows.go * [client] Add quick actions signal handler for Windows with named events * [client] Run go mod tidy * [client] Remove line break * [client] Log unexpected status in separate function * [client-ui] Refactor quick actions window To address racing conditions, it also replaces usage of pause and resume channels with an atomic bool. * [client-ui] use derived context from ServiceClient * [client] Update signal_windows log message Also, format error when trying to set event on sendShowWindowSignal * go mod tidy * [client-ui] Add struct to pass fewer parameters to applyQuickActionsUiState function * [client] Add missing import --------- Co-authored-by: Viktor Liu <viktor@netbird.io>
This commit is contained in:
BIN
client/ui/assets/netbird-disconnected.ico
Normal file
BIN
client/ui/assets/netbird-disconnected.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
BIN
client/ui/assets/netbird-disconnected.png
Normal file
BIN
client/ui/assets/netbird-disconnected.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.4 KiB |
@@ -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")
|
||||
@@ -158,11 +166,9 @@ func initLogFile() (string, error) {
|
||||
|
||||
// watchSettingsChanges listens for Fyne theme/settings changes and updates the client icon.
|
||||
func watchSettingsChanges(a fyne.App, client *serviceClient) {
|
||||
settingsChangeChan := make(chan fyne.Settings)
|
||||
a.Settings().AddChangeListener(settingsChangeChan)
|
||||
for range settingsChangeChan {
|
||||
a.Settings().AddListener(func(settings fyne.Settings) {
|
||||
client.updateIcon()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// showErrorMessage displays an error message in a simple window.
|
||||
@@ -287,6 +293,7 @@ type serviceClient struct {
|
||||
showNetworks bool
|
||||
wNetworks fyne.Window
|
||||
wProfiles fyne.Window
|
||||
wQuickActions fyne.Window
|
||||
|
||||
eventManager *event.Manager
|
||||
|
||||
@@ -306,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
|
||||
@@ -349,6 +357,8 @@ func newServiceClient(args *newServiceClientArgs) *serviceClient {
|
||||
s.showDebugUI()
|
||||
case args.showProfiles:
|
||||
s.showProfilesUI()
|
||||
case args.showQuickActions:
|
||||
s.showQuickActionsUI()
|
||||
}
|
||||
|
||||
return s
|
||||
|
||||
@@ -500,7 +500,7 @@ func (s *serviceClient) createDebugBundleFromCollection(
|
||||
if uploadFailureReason != "" {
|
||||
showUploadFailedDialog(progress.window, localPath, uploadFailureReason)
|
||||
} else {
|
||||
showUploadSuccessDialog(progress.window, localPath, uploadedKey)
|
||||
showUploadSuccessDialog(s.app, progress.window, localPath, uploadedKey)
|
||||
}
|
||||
} else {
|
||||
showBundleCreatedDialog(progress.window, localPath)
|
||||
@@ -565,7 +565,7 @@ func (s *serviceClient) handleDebugCreation(
|
||||
if uploadFailureReason != "" {
|
||||
showUploadFailedDialog(w, localPath, uploadFailureReason)
|
||||
} else {
|
||||
showUploadSuccessDialog(w, localPath, uploadedKey)
|
||||
showUploadSuccessDialog(s.app, w, localPath, uploadedKey)
|
||||
}
|
||||
} else {
|
||||
showBundleCreatedDialog(w, localPath)
|
||||
@@ -665,7 +665,7 @@ func showUploadFailedDialog(w fyne.Window, localPath, failureReason string) {
|
||||
}
|
||||
|
||||
// showUploadSuccessDialog displays a dialog when upload succeeds
|
||||
func showUploadSuccessDialog(w fyne.Window, localPath, uploadedKey string) {
|
||||
func showUploadSuccessDialog(a fyne.App, w fyne.Window, localPath, uploadedKey string) {
|
||||
log.Infof("Upload key: %s", uploadedKey)
|
||||
keyEntry := widget.NewEntry()
|
||||
keyEntry.SetText(uploadedKey)
|
||||
@@ -683,7 +683,7 @@ func showUploadSuccessDialog(w fyne.Window, localPath, uploadedKey string) {
|
||||
customDialog := dialog.NewCustom("Upload Successful", "OK", content, w)
|
||||
|
||||
copyBtn := createButtonWithAction("Copy key", func() {
|
||||
w.Clipboard().SetContent(uploadedKey)
|
||||
a.Clipboard().SetContent(uploadedKey)
|
||||
log.Info("Upload key copied to clipboard")
|
||||
})
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@ import (
|
||||
//go:embed assets/netbird.png
|
||||
var iconAbout []byte
|
||||
|
||||
//go:embed assets/netbird-disconnected.png
|
||||
var iconAboutDisconnected []byte
|
||||
|
||||
//go:embed assets/netbird-systemtray-connected.png
|
||||
var iconConnected []byte
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@ import (
|
||||
//go:embed assets/netbird.ico
|
||||
var iconAbout []byte
|
||||
|
||||
//go:embed assets/netbird-disconnected.ico
|
||||
var iconAboutDisconnected []byte
|
||||
|
||||
//go:embed assets/netbird-systemtray-connected.ico
|
||||
var iconConnected []byte
|
||||
|
||||
|
||||
349
client/ui/quickactions.go
Normal file
349
client/ui/quickactions.go
Normal file
@@ -0,0 +1,349 @@
|
||||
//go:build !(linux && 386)
|
||||
|
||||
//go:generate fyne bundle -o quickactions_assets.go assets/connected.png
|
||||
//go:generate fyne bundle -o quickactions_assets.go -append assets/disconnected.png
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
"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"
|
||||
"github.com/netbirdio/netbird/client/proto"
|
||||
)
|
||||
|
||||
type quickActionsUiState struct {
|
||||
connectionStatus string
|
||||
isToggleButtonEnabled bool
|
||||
isConnectionChanged bool
|
||||
toggleAction func()
|
||||
}
|
||||
|
||||
func newQuickActionsUiState() quickActionsUiState {
|
||||
return quickActionsUiState{
|
||||
connectionStatus: string(internal.StatusIdle),
|
||||
isToggleButtonEnabled: false,
|
||||
isConnectionChanged: false,
|
||||
}
|
||||
}
|
||||
|
||||
type clientConnectionStatusProvider interface {
|
||||
connectionStatus(ctx context.Context) (string, error)
|
||||
}
|
||||
|
||||
type daemonClientConnectionStatusProvider struct {
|
||||
client proto.DaemonServiceClient
|
||||
}
|
||||
|
||||
func (d daemonClientConnectionStatusProvider) connectionStatus(ctx context.Context) (string, error) {
|
||||
childCtx, cancel := context.WithTimeout(ctx, 400*time.Millisecond)
|
||||
defer cancel()
|
||||
status, err := d.client.Status(childCtx, &proto.StatusRequest{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return status.Status, nil
|
||||
}
|
||||
|
||||
type clientCommand interface {
|
||||
execute() error
|
||||
}
|
||||
|
||||
type connectCommand struct {
|
||||
connectClient func() error
|
||||
}
|
||||
|
||||
func (c connectCommand) execute() error {
|
||||
return c.connectClient()
|
||||
}
|
||||
|
||||
type disconnectCommand struct {
|
||||
disconnectClient func() error
|
||||
}
|
||||
|
||||
func (c disconnectCommand) execute() error {
|
||||
return c.disconnectClient()
|
||||
}
|
||||
|
||||
type quickActionsViewModel struct {
|
||||
provider clientConnectionStatusProvider
|
||||
connect clientCommand
|
||||
disconnect clientCommand
|
||||
uiChan chan quickActionsUiState
|
||||
isWatchingConnectionStatus atomic.Bool
|
||||
}
|
||||
|
||||
func newQuickActionsViewModel(ctx context.Context, provider clientConnectionStatusProvider, connect, disconnect clientCommand, uiChan chan quickActionsUiState) {
|
||||
viewModel := quickActionsViewModel{
|
||||
provider: provider,
|
||||
connect: connect,
|
||||
disconnect: disconnect,
|
||||
uiChan: uiChan,
|
||||
}
|
||||
|
||||
viewModel.isWatchingConnectionStatus.Store(true)
|
||||
|
||||
// base UI status
|
||||
uiChan <- newQuickActionsUiState()
|
||||
|
||||
// this retrieves the current connection status
|
||||
// and pushes the UI state that reflects it via uiChan
|
||||
go viewModel.watchConnectionStatus(ctx)
|
||||
}
|
||||
|
||||
func (q *quickActionsViewModel) updateUiState(ctx context.Context) {
|
||||
uiState := newQuickActionsUiState()
|
||||
connectionStatus, err := q.provider.connectionStatus(ctx)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Status: Error - %v", err)
|
||||
q.uiChan <- uiState
|
||||
return
|
||||
}
|
||||
|
||||
if connectionStatus == string(internal.StatusConnected) {
|
||||
uiState.toggleAction = func() {
|
||||
q.executeCommand(q.disconnect)
|
||||
}
|
||||
} else {
|
||||
uiState.toggleAction = func() {
|
||||
q.executeCommand(q.connect)
|
||||
}
|
||||
}
|
||||
|
||||
uiState.isToggleButtonEnabled = true
|
||||
uiState.connectionStatus = connectionStatus
|
||||
q.uiChan <- uiState
|
||||
}
|
||||
|
||||
func (q *quickActionsViewModel) watchConnectionStatus(ctx context.Context) {
|
||||
ticker := time.NewTicker(1000 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
if q.isWatchingConnectionStatus.Load() {
|
||||
q.updateUiState(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (q *quickActionsViewModel) executeCommand(command clientCommand) {
|
||||
uiState := newQuickActionsUiState()
|
||||
// newQuickActionsUiState starts with Idle connection status,
|
||||
// and all that's necessary here is to just disable the toggle button.
|
||||
uiState.connectionStatus = ""
|
||||
|
||||
q.uiChan <- uiState
|
||||
|
||||
q.isWatchingConnectionStatus.Store(false)
|
||||
|
||||
err := command.execute()
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Status: Error - %v", err)
|
||||
q.isWatchingConnectionStatus.Store(true)
|
||||
} else {
|
||||
uiState = newQuickActionsUiState()
|
||||
uiState.isConnectionChanged = true
|
||||
q.uiChan <- uiState
|
||||
}
|
||||
}
|
||||
|
||||
func getSystemTrayName() string {
|
||||
os := runtime.GOOS
|
||||
switch os {
|
||||
case "darwin":
|
||||
return "menu bar"
|
||||
default:
|
||||
return "system tray"
|
||||
}
|
||||
}
|
||||
|
||||
func (s *serviceClient) getNetBirdImage(name string, content []byte) *canvas.Image {
|
||||
imageSize := fyne.NewSize(64, 64)
|
||||
|
||||
resource := fyne.NewStaticResource(name, content)
|
||||
image := canvas.NewImageFromResource(resource)
|
||||
image.FillMode = canvas.ImageFillContain
|
||||
image.SetMinSize(imageSize)
|
||||
image.Resize(imageSize)
|
||||
|
||||
return image
|
||||
}
|
||||
|
||||
type quickActionsUiComponents struct {
|
||||
content *fyne.Container
|
||||
toggleConnectionButton *widget.Button
|
||||
connectedLabelText, disconnectedLabelText string
|
||||
connectedImage, disconnectedImage *canvas.Image
|
||||
connectedCircleRes, disconnectedCircleRes fyne.Resource
|
||||
}
|
||||
|
||||
// applyQuickActionsUiState applies a single UI state to the quick actions window.
|
||||
// It closes the window and returns true if the connection status has changed,
|
||||
// in which case the caller should stop processing further states.
|
||||
func (s *serviceClient) applyQuickActionsUiState(
|
||||
uiState quickActionsUiState,
|
||||
components quickActionsUiComponents,
|
||||
) bool {
|
||||
if uiState.isConnectionChanged {
|
||||
fyne.DoAndWait(func() {
|
||||
s.wQuickActions.Close()
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
var logo *canvas.Image
|
||||
var buttonText string
|
||||
var buttonIcon fyne.Resource
|
||||
|
||||
if uiState.connectionStatus == string(internal.StatusConnected) {
|
||||
buttonText = components.connectedLabelText
|
||||
buttonIcon = components.connectedCircleRes
|
||||
logo = components.connectedImage
|
||||
} else if uiState.connectionStatus == string(internal.StatusIdle) {
|
||||
buttonText = components.disconnectedLabelText
|
||||
buttonIcon = components.disconnectedCircleRes
|
||||
logo = components.disconnectedImage
|
||||
}
|
||||
|
||||
fyne.DoAndWait(func() {
|
||||
if buttonText != "" {
|
||||
components.toggleConnectionButton.SetText(buttonText)
|
||||
}
|
||||
|
||||
if buttonIcon != nil {
|
||||
components.toggleConnectionButton.SetIcon(buttonIcon)
|
||||
}
|
||||
|
||||
if uiState.isToggleButtonEnabled {
|
||||
components.toggleConnectionButton.Enable()
|
||||
} else {
|
||||
components.toggleConnectionButton.Disable()
|
||||
}
|
||||
|
||||
components.toggleConnectionButton.OnTapped = func() {
|
||||
if uiState.toggleAction != nil {
|
||||
go uiState.toggleAction()
|
||||
}
|
||||
}
|
||||
|
||||
components.toggleConnectionButton.Refresh()
|
||||
|
||||
// the second position in the content's object array is the NetBird logo.
|
||||
if logo != nil {
|
||||
components.content.Objects[1] = logo
|
||||
components.content.Refresh()
|
||||
}
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// showQuickActionsUI displays a simple window with the NetBird logo and a connection toggle button.
|
||||
func (s *serviceClient) showQuickActionsUI() {
|
||||
s.wQuickActions = s.app.NewWindow("NetBird")
|
||||
vmCtx, vmCancel := context.WithCancel(s.ctx)
|
||||
s.wQuickActions.SetOnClosed(vmCancel)
|
||||
|
||||
client, err := s.getSrvClient(defaultFailTimeout)
|
||||
|
||||
connCmd := connectCommand{
|
||||
connectClient: func() error {
|
||||
return s.menuUpClick(s.ctx)
|
||||
},
|
||||
}
|
||||
|
||||
disConnCmd := disconnectCommand{
|
||||
disconnectClient: func() error {
|
||||
return s.menuDownClick()
|
||||
},
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("get service client: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
uiChan := make(chan quickActionsUiState, 1)
|
||||
newQuickActionsViewModel(vmCtx, daemonClientConnectionStatusProvider{client: client}, connCmd, disConnCmd, uiChan)
|
||||
|
||||
connectedImage := s.getNetBirdImage("netbird.png", iconAbout)
|
||||
disconnectedImage := s.getNetBirdImage("netbird-disconnected.png", iconAboutDisconnected)
|
||||
|
||||
connectedCircle := canvas.NewImageFromResource(resourceConnectedPng)
|
||||
disconnectedCircle := canvas.NewImageFromResource(resourceDisconnectedPng)
|
||||
|
||||
connectedLabelText := "Disconnect"
|
||||
disconnectedLabelText := "Connect"
|
||||
|
||||
toggleConnectionButton := widget.NewButtonWithIcon(disconnectedLabelText, disconnectedCircle.Resource, func() {
|
||||
// This button's tap function will be set when an ui state arrives via the uiChan channel.
|
||||
})
|
||||
|
||||
// Button starts disabled until the first ui state arrives.
|
||||
toggleConnectionButton.Disable()
|
||||
|
||||
hintLabelText := fmt.Sprintf("You can always access NetBird from your %s.", getSystemTrayName())
|
||||
hintLabel := widget.NewLabel(hintLabelText)
|
||||
|
||||
content := container.NewVBox(
|
||||
layout.NewSpacer(),
|
||||
disconnectedImage,
|
||||
layout.NewSpacer(),
|
||||
container.NewCenter(toggleConnectionButton),
|
||||
layout.NewSpacer(),
|
||||
container.NewCenter(hintLabel),
|
||||
)
|
||||
|
||||
// this watches for ui state updates.
|
||||
go func() {
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-vmCtx.Done():
|
||||
return
|
||||
case uiState, ok := <-uiChan:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
closed := s.applyQuickActionsUiState(
|
||||
uiState,
|
||||
quickActionsUiComponents{
|
||||
content,
|
||||
toggleConnectionButton,
|
||||
connectedLabelText, disconnectedLabelText,
|
||||
connectedImage, disconnectedImage,
|
||||
connectedCircle.Resource, disconnectedCircle.Resource,
|
||||
},
|
||||
)
|
||||
if closed {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
s.wQuickActions.SetContent(content)
|
||||
s.wQuickActions.Resize(fyne.NewSize(400, 200))
|
||||
s.wQuickActions.SetFixedSize(true)
|
||||
s.wQuickActions.Show()
|
||||
}
|
||||
23
client/ui/quickactions_assets.go
Normal file
23
client/ui/quickactions_assets.go
Normal file
@@ -0,0 +1,23 @@
|
||||
// auto-generated
|
||||
// Code generated by '$ fyne bundle'. DO NOT EDIT.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fyne.io/fyne/v2"
|
||||
)
|
||||
|
||||
//go:embed assets/connected.png
|
||||
var resourceConnectedPngData []byte
|
||||
var resourceConnectedPng = &fyne.StaticResource{
|
||||
StaticName: "assets/connected.png",
|
||||
StaticContent: resourceConnectedPngData,
|
||||
}
|
||||
|
||||
//go:embed assets/disconnected.png
|
||||
var resourceDisconnectedPngData []byte
|
||||
var resourceDisconnectedPng = &fyne.StaticResource{
|
||||
StaticName: "assets/disconnected.png",
|
||||
StaticContent: resourceDisconnectedPngData,
|
||||
}
|
||||
76
client/ui/signal_unix.go
Normal file
76
client/ui/signal_unix.go
Normal 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)
|
||||
}
|
||||
171
client/ui/signal_windows.go
Normal file
171
client/ui/signal_windows.go
Normal file
@@ -0,0 +1,171 @@
|
||||
//go:build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const (
|
||||
quickActionsTriggerEventName = `Global\NetBirdQuickActionsTriggerEvent`
|
||||
waitTimeout = 5 * time.Second
|
||||
// SYNCHRONIZE is needed for WaitForSingleObject, EVENT_MODIFY_STATE for ResetEvent.
|
||||
desiredAccesses = windows.SYNCHRONIZE | windows.EVENT_MODIFY_STATE
|
||||
)
|
||||
|
||||
func getEventNameUint16Pointer() (*uint16, error) {
|
||||
eventNamePtr, err := windows.UTF16PtrFromString(quickActionsTriggerEventName)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to convert event name '%s' to UTF16: %v", quickActionsTriggerEventName, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return eventNamePtr, nil
|
||||
}
|
||||
|
||||
// setupSignalHandler sets up signal handling for Windows.
|
||||
// Windows doesn't support SIGUSR1, so this uses a similar approach using windows.Events.
|
||||
func (s *serviceClient) setupSignalHandler(ctx context.Context) {
|
||||
eventNamePtr, err := getEventNameUint16Pointer()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
eventHandle, err := windows.CreateEvent(nil, 1, 0, eventNamePtr)
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, windows.ERROR_ALREADY_EXISTS) {
|
||||
log.Warnf("Quick actions trigger event '%s' already exists. Attempting to open.", quickActionsTriggerEventName)
|
||||
eventHandle, err = windows.OpenEvent(desiredAccesses, false, eventNamePtr)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to open existing quick actions trigger event '%s': %v", quickActionsTriggerEventName, err)
|
||||
return
|
||||
}
|
||||
log.Infof("Successfully opened existing quick actions trigger event '%s'.", quickActionsTriggerEventName)
|
||||
} else {
|
||||
log.Errorf("Failed to create quick actions trigger event '%s': %v", quickActionsTriggerEventName, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if eventHandle == windows.InvalidHandle {
|
||||
log.Errorf("Obtained an invalid handle for quick actions trigger event '%s'", quickActionsTriggerEventName)
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("Quick actions handler waiting for signal on event: %s", quickActionsTriggerEventName)
|
||||
|
||||
go s.waitForEvent(ctx, eventHandle)
|
||||
}
|
||||
|
||||
func (s *serviceClient) waitForEvent(ctx context.Context, eventHandle windows.Handle) {
|
||||
defer func() {
|
||||
if err := windows.CloseHandle(eventHandle); err != nil {
|
||||
log.Errorf("Failed to close quick actions event handle '%s': %v", quickActionsTriggerEventName, err)
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
status, err := windows.WaitForSingleObject(eventHandle, uint32(waitTimeout.Milliseconds()))
|
||||
|
||||
switch status {
|
||||
case windows.WAIT_OBJECT_0:
|
||||
log.Info("Received signal on quick actions event. Opening quick actions window.")
|
||||
|
||||
// reset the event so it can be triggered again later (manual reset == 1)
|
||||
if err := windows.ResetEvent(eventHandle); err != nil {
|
||||
log.Errorf("Failed to reset quick actions event '%s': %v", quickActionsTriggerEventName, err)
|
||||
}
|
||||
|
||||
s.openQuickActions()
|
||||
case uint32(windows.WAIT_TIMEOUT):
|
||||
|
||||
default:
|
||||
if isDone := logUnexpectedStatus(ctx, status, err); isDone {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func logUnexpectedStatus(ctx context.Context, status uint32, err error) bool {
|
||||
log.Errorf("Unexpected status %d from WaitForSingleObject for quick actions event '%s': %v",
|
||||
status, quickActionsTriggerEventName, err)
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
return false
|
||||
case <-ctx.Done():
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 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("error starting 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 {
|
||||
_, err := os.FindProcess(int(pid))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
eventNamePtr, err := getEventNameUint16Pointer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
eventHandle, err := windows.OpenEvent(desiredAccesses, false, eventNamePtr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = windows.SetEvent(eventHandle)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error setting event: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user