[client/ui] Replace fyne UI with Wails (rename ui-wails to ui)
Removes the legacy fyne-based client/ui implementation and renames the Wails replacement (client/ui-wails) to take its place at client/ui. Go imports, frontend bindings, CI workflows, goreleaser configs and the windows .syso icon path are updated to follow the rename.
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
@@ -1,54 +0,0 @@
|
|||||||
//go:build !android && !ios && !freebsd && !js
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import _ "embed"
|
|
||||||
|
|
||||||
// Tray icons embedded from the legacy Fyne UI's asset set. Each pair is a
|
|
||||||
// light-mode PNG and its dark-mode variant; macOS template variants
|
|
||||||
// (*-macos.png) live alongside for menubar use. Windows uses the same
|
|
||||||
// PNGs — multi-resolution .ico files looked promising on disk but
|
|
||||||
// Wails3's Shell_NotifyIcon NIM_MODIFY never redrew them on the running
|
|
||||||
// tray; PNG single-frame works.
|
|
||||||
|
|
||||||
//go:embed assets/netbird-systemtray-connected.png
|
|
||||||
var iconConnected []byte
|
|
||||||
|
|
||||||
//go:embed assets/netbird-systemtray-connected-dark.png
|
|
||||||
var iconConnectedDark []byte
|
|
||||||
|
|
||||||
//go:embed assets/netbird-systemtray-disconnected.png
|
|
||||||
var iconDisconnected []byte
|
|
||||||
|
|
||||||
//go:embed assets/netbird-systemtray-connecting.png
|
|
||||||
var iconConnecting []byte
|
|
||||||
|
|
||||||
//go:embed assets/netbird-systemtray-error.png
|
|
||||||
var iconError []byte
|
|
||||||
|
|
||||||
//go:embed assets/netbird-systemtray-update-connected.png
|
|
||||||
var iconUpdateConnected []byte
|
|
||||||
|
|
||||||
//go:embed assets/netbird-systemtray-update-disconnected.png
|
|
||||||
var iconUpdateDisconnected []byte
|
|
||||||
|
|
||||||
//go:embed assets/netbird-systemtray-connected-macos.png
|
|
||||||
var iconConnectedMacOS []byte
|
|
||||||
|
|
||||||
//go:embed assets/netbird-systemtray-disconnected-macos.png
|
|
||||||
var iconDisconnectedMacOS []byte
|
|
||||||
|
|
||||||
//go:embed assets/netbird-systemtray-connecting-macos.png
|
|
||||||
var iconConnectingMacOS []byte
|
|
||||||
|
|
||||||
//go:embed assets/netbird-systemtray-error-macos.png
|
|
||||||
var iconErrorMacOS []byte
|
|
||||||
|
|
||||||
//go:embed assets/netbird-systemtray-update-connected-macos.png
|
|
||||||
var iconUpdateConnectedMacOS []byte
|
|
||||||
|
|
||||||
//go:embed assets/netbird-systemtray-update-disconnected-macos.png
|
|
||||||
var iconUpdateDisconnectedMacOS []byte
|
|
||||||
|
|
||||||
//go:embed assets/netbird.png
|
|
||||||
var iconWindow []byte
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
//go:build !windows && !android && !ios && !freebsd && !js
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// listenForShowSignal opens the main window when the process receives SIGUSR1.
|
|
||||||
// External tools (the daemon, the installer, or another `netbird-ui` invocation)
|
|
||||||
// can poke this channel by signalling the running pid.
|
|
||||||
func listenForShowSignal(ctx context.Context, tray *Tray) {
|
|
||||||
sigCh := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(sigCh, syscall.SIGUSR1)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
signal.Stop(sigCh)
|
|
||||||
return
|
|
||||||
case <-sigCh:
|
|
||||||
log.Debug("SIGUSR1 received, showing window")
|
|
||||||
tray.ShowWindow()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
//go:build windows
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"golang.org/x/sys/windows"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
quickActionsTriggerEventName = `Global\NetBirdQuickActionsTriggerEvent`
|
|
||||||
waitTimeout = 5 * time.Second
|
|
||||||
desiredAccesses = windows.SYNCHRONIZE | windows.EVENT_MODIFY_STATE
|
|
||||||
|
|
||||||
// WaitForSingleObject returns this when the timeout elapses without the
|
|
||||||
// object being signalled. golang.org/x/sys/windows does not expose it.
|
|
||||||
waitTimeoutCode uint32 = 0x00000102
|
|
||||||
)
|
|
||||||
|
|
||||||
// listenForShowSignal opens the main window when an external process pulses
|
|
||||||
// the named event Global\NetBirdQuickActionsTriggerEvent. Mirrors the trigger
|
|
||||||
// the legacy Fyne UI used so the installer and CLI integrations keep working.
|
|
||||||
func listenForShowSignal(ctx context.Context, tray *Tray) {
|
|
||||||
namePtr, err := windows.UTF16PtrFromString(quickActionsTriggerEventName)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("trigger event name: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
handle, err := windows.CreateEvent(nil, 1, 0, namePtr)
|
|
||||||
if err != nil {
|
|
||||||
if !errors.Is(err, windows.ERROR_ALREADY_EXISTS) {
|
|
||||||
log.Errorf("create trigger event %q: %v", quickActionsTriggerEventName, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
handle, err = windows.OpenEvent(desiredAccesses, false, namePtr)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("open trigger event %q: %v", quickActionsTriggerEventName, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if handle == windows.InvalidHandle {
|
|
||||||
log.Errorf("invalid handle for trigger event %q", quickActionsTriggerEventName)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
go waitForTrigger(ctx, handle, tray)
|
|
||||||
}
|
|
||||||
|
|
||||||
func waitForTrigger(ctx context.Context, handle windows.Handle, tray *Tray) {
|
|
||||||
defer func() {
|
|
||||||
if err := windows.CloseHandle(handle); err != nil {
|
|
||||||
log.Errorf("close trigger event handle: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
timeoutMs := uint32(waitTimeout / time.Millisecond)
|
|
||||||
for {
|
|
||||||
if ctx.Err() != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ev, err := windows.WaitForSingleObject(handle, timeoutMs)
|
|
||||||
switch {
|
|
||||||
case err != nil:
|
|
||||||
log.Errorf("wait trigger event: %v", err)
|
|
||||||
return
|
|
||||||
case ev == waitTimeoutCode:
|
|
||||||
continue
|
|
||||||
case ev == windows.WAIT_OBJECT_0:
|
|
||||||
if err := windows.ResetEvent(handle); err != nil {
|
|
||||||
log.Errorf("reset trigger event: %v", err)
|
|
||||||
}
|
|
||||||
tray.ShowWindow()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 932 B After Width: | Height: | Size: 932 B |
|
Before Width: | Height: | Size: 997 B After Width: | Height: | Size: 997 B |
|
Before Width: | Height: | Size: 723 B After Width: | Height: | Size: 723 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 678 B After Width: | Height: | Size: 678 B |
|
Before Width: | Height: | Size: 723 B After Width: | Height: | Size: 723 B |
|
Before Width: | Height: | Size: 745 B After Width: | Height: | Size: 745 B |
|
Before Width: | Height: | Size: 793 B After Width: | Height: | Size: 793 B |
|
Before Width: | Height: | Size: 761 B After Width: | Height: | Size: 761 B |
|
Before Width: | Height: | Size: 806 B After Width: | Height: | Size: 806 B |
|
Before Width: | Height: | Size: 745 B After Width: | Height: | Size: 745 B |
|
Before Width: | Height: | Size: 790 B After Width: | Height: | Size: 790 B |
|
Before Width: | Height: | Size: 759 B After Width: | Height: | Size: 759 B |
|
Before Width: | Height: | Size: 805 B After Width: | Height: | Size: 805 B |
|
Before Width: | Height: | Size: 732 B After Width: | Height: | Size: 732 B |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 26 KiB |
@@ -1,5 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
sudo apt update
|
|
||||||
sudo apt remove gir1.2-appindicator3-0.1
|
|
||||||
sudo apt install -y libayatana-appindicator3-dev
|
|
||||||
go build
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
[Desktop Entry]
|
|
||||||
Name=Netbird
|
|
||||||
Exec=/usr/bin/netbird-ui
|
|
||||||
Icon=netbird
|
|
||||||
Type=Application
|
|
||||||
Terminal=false
|
|
||||||
Categories=Utility;
|
|
||||||
Keywords=netbird;
|
|
||||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
@@ -1,16 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
const (
|
|
||||||
allowSSHMenuDescr = "Allow SSH connections"
|
|
||||||
autoConnectMenuDescr = "Connect automatically when the service starts"
|
|
||||||
quantumResistanceMenuDescr = "Enable post-quantum security via Rosenpass"
|
|
||||||
lazyConnMenuDescr = "[Experimental] Enable lazy connections"
|
|
||||||
blockInboundMenuDescr = "Block inbound connections to the local machine and routed networks"
|
|
||||||
notificationsMenuDescr = "Enable notifications"
|
|
||||||
advancedSettingsMenuDescr = "Advanced settings of the application"
|
|
||||||
debugBundleMenuDescr = "Create and open debug information bundle"
|
|
||||||
disabledMenuDescr = ""
|
|
||||||
networksMenuDescr = "Open the networks management window"
|
|
||||||
latestVersionMenuDescr = "Download latest version"
|
|
||||||
quitMenuDescr = "Quit the client app"
|
|
||||||
)
|
|
||||||
@@ -1,727 +0,0 @@
|
|||||||
//go:build !(linux && 386)
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
|
||||||
"fyne.io/fyne/v2/container"
|
|
||||||
"fyne.io/fyne/v2/dialog"
|
|
||||||
"fyne.io/fyne/v2/widget"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"github.com/skratchdot/open-golang/open"
|
|
||||||
"google.golang.org/protobuf/types/known/durationpb"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal"
|
|
||||||
"github.com/netbirdio/netbird/client/proto"
|
|
||||||
uptypes "github.com/netbirdio/netbird/upload-server/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Initial state for the debug collection
|
|
||||||
type debugInitialState struct {
|
|
||||||
wasDown bool
|
|
||||||
needsRestoreUp bool
|
|
||||||
logLevel proto.LogLevel
|
|
||||||
isLevelTrace bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debug collection parameters
|
|
||||||
type debugCollectionParams struct {
|
|
||||||
duration time.Duration
|
|
||||||
anonymize bool
|
|
||||||
systemInfo bool
|
|
||||||
upload bool
|
|
||||||
uploadURL string
|
|
||||||
enablePersistence bool
|
|
||||||
capture bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// UI components for progress tracking
|
|
||||||
type progressUI struct {
|
|
||||||
statusLabel *widget.Label
|
|
||||||
progressBar *widget.ProgressBar
|
|
||||||
uiControls []fyne.Disableable
|
|
||||||
window fyne.Window
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *serviceClient) showDebugUI() {
|
|
||||||
w := s.app.NewWindow("NetBird Debug")
|
|
||||||
w.SetOnClosed(s.cancel)
|
|
||||||
w.Resize(fyne.NewSize(600, 500))
|
|
||||||
w.SetFixedSize(true)
|
|
||||||
|
|
||||||
anonymizeCheck := widget.NewCheck("Anonymize sensitive information (public IPs, domains, ...)", nil)
|
|
||||||
systemInfoCheck := widget.NewCheck("Include system information (routes, interfaces, ...)", nil)
|
|
||||||
systemInfoCheck.SetChecked(true)
|
|
||||||
captureCheck := widget.NewCheck("Include packet capture", nil)
|
|
||||||
uploadCheck := widget.NewCheck("Upload bundle automatically after creation", nil)
|
|
||||||
uploadCheck.SetChecked(true)
|
|
||||||
|
|
||||||
uploadURLContainer, uploadURL := s.buildUploadSection(uploadCheck)
|
|
||||||
|
|
||||||
debugModeContainer, runForDurationCheck, durationInput, noteLabel := s.buildDurationSection()
|
|
||||||
|
|
||||||
statusLabel := widget.NewLabel("")
|
|
||||||
statusLabel.Hide()
|
|
||||||
progressBar := widget.NewProgressBar()
|
|
||||||
progressBar.Hide()
|
|
||||||
createButton := widget.NewButton("Create Debug Bundle", nil)
|
|
||||||
|
|
||||||
uiControls := []fyne.Disableable{
|
|
||||||
anonymizeCheck, systemInfoCheck, captureCheck,
|
|
||||||
uploadCheck, uploadURL, runForDurationCheck, durationInput, createButton,
|
|
||||||
}
|
|
||||||
|
|
||||||
createButton.OnTapped = s.getCreateHandler(
|
|
||||||
statusLabel, progressBar, uploadCheck, uploadURL,
|
|
||||||
anonymizeCheck, systemInfoCheck, captureCheck,
|
|
||||||
runForDurationCheck, durationInput, uiControls, w,
|
|
||||||
)
|
|
||||||
|
|
||||||
content := container.NewVBox(
|
|
||||||
widget.NewLabel("Create a debug bundle to help troubleshoot issues with NetBird"),
|
|
||||||
widget.NewLabel(""),
|
|
||||||
anonymizeCheck, systemInfoCheck, captureCheck,
|
|
||||||
uploadCheck, uploadURLContainer,
|
|
||||||
widget.NewLabel(""),
|
|
||||||
debugModeContainer, noteLabel,
|
|
||||||
widget.NewLabel(""),
|
|
||||||
statusLabel, progressBar, createButton,
|
|
||||||
)
|
|
||||||
|
|
||||||
w.SetContent(container.NewPadded(content))
|
|
||||||
w.Show()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *serviceClient) buildUploadSection(uploadCheck *widget.Check) (*fyne.Container, *widget.Entry) {
|
|
||||||
uploadURL := widget.NewEntry()
|
|
||||||
uploadURL.SetText(uptypes.DefaultBundleURL)
|
|
||||||
uploadURL.SetPlaceHolder("Enter upload URL")
|
|
||||||
|
|
||||||
uploadURLContainer := container.NewVBox(widget.NewLabel("Debug upload URL:"), uploadURL)
|
|
||||||
|
|
||||||
uploadCheck.OnChanged = func(checked bool) {
|
|
||||||
if checked {
|
|
||||||
uploadURLContainer.Show()
|
|
||||||
} else {
|
|
||||||
uploadURLContainer.Hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return uploadURLContainer, uploadURL
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *serviceClient) buildDurationSection() (*fyne.Container, *widget.Check, *widget.Entry, *widget.Label) {
|
|
||||||
runForDurationCheck := widget.NewCheck("Run with trace logs before creating bundle", nil)
|
|
||||||
runForDurationCheck.SetChecked(true)
|
|
||||||
|
|
||||||
forLabel := widget.NewLabel("for")
|
|
||||||
durationInput := widget.NewEntry()
|
|
||||||
durationInput.SetText("1")
|
|
||||||
minutesLabel := widget.NewLabel("minute")
|
|
||||||
durationInput.Validator = func(s string) error {
|
|
||||||
return validateMinute(s, minutesLabel)
|
|
||||||
}
|
|
||||||
|
|
||||||
noteLabel := widget.NewLabel("Note: NetBird will be brought up and down during collection")
|
|
||||||
|
|
||||||
runForDurationCheck.OnChanged = func(checked bool) {
|
|
||||||
if checked {
|
|
||||||
forLabel.Show()
|
|
||||||
durationInput.Show()
|
|
||||||
minutesLabel.Show()
|
|
||||||
noteLabel.Show()
|
|
||||||
} else {
|
|
||||||
forLabel.Hide()
|
|
||||||
durationInput.Hide()
|
|
||||||
minutesLabel.Hide()
|
|
||||||
noteLabel.Hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
modeContainer := container.NewHBox(runForDurationCheck, forLabel, durationInput, minutesLabel)
|
|
||||||
return modeContainer, runForDurationCheck, durationInput, noteLabel
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateMinute(s string, minutesLabel *widget.Label) error {
|
|
||||||
if val, err := strconv.Atoi(s); err != nil || val < 1 {
|
|
||||||
return fmt.Errorf("must be a number ≥ 1")
|
|
||||||
}
|
|
||||||
if s == "1" {
|
|
||||||
minutesLabel.SetText("minute")
|
|
||||||
} else {
|
|
||||||
minutesLabel.SetText("minutes")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// disableUIControls disables the provided UI controls
|
|
||||||
func disableUIControls(controls []fyne.Disableable) {
|
|
||||||
for _, control := range controls {
|
|
||||||
control.Disable()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// enableUIControls enables the provided UI controls
|
|
||||||
func enableUIControls(controls []fyne.Disableable) {
|
|
||||||
for _, control := range controls {
|
|
||||||
control.Enable()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *serviceClient) getCreateHandler(
|
|
||||||
statusLabel *widget.Label,
|
|
||||||
progressBar *widget.ProgressBar,
|
|
||||||
uploadCheck *widget.Check,
|
|
||||||
uploadURL *widget.Entry,
|
|
||||||
anonymizeCheck *widget.Check,
|
|
||||||
systemInfoCheck *widget.Check,
|
|
||||||
captureCheck *widget.Check,
|
|
||||||
runForDurationCheck *widget.Check,
|
|
||||||
duration *widget.Entry,
|
|
||||||
uiControls []fyne.Disableable,
|
|
||||||
w fyne.Window,
|
|
||||||
) func() {
|
|
||||||
return func() {
|
|
||||||
disableUIControls(uiControls)
|
|
||||||
statusLabel.Show()
|
|
||||||
|
|
||||||
var url string
|
|
||||||
if uploadCheck.Checked {
|
|
||||||
url = uploadURL.Text
|
|
||||||
if url == "" {
|
|
||||||
statusLabel.SetText("Error: Upload URL is required when upload is enabled")
|
|
||||||
enableUIControls(uiControls)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
params := &debugCollectionParams{
|
|
||||||
anonymize: anonymizeCheck.Checked,
|
|
||||||
systemInfo: systemInfoCheck.Checked,
|
|
||||||
capture: captureCheck.Checked,
|
|
||||||
upload: uploadCheck.Checked,
|
|
||||||
uploadURL: url,
|
|
||||||
enablePersistence: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
runForDuration := runForDurationCheck.Checked
|
|
||||||
if runForDuration {
|
|
||||||
minutes, err := time.ParseDuration(duration.Text + "m")
|
|
||||||
if err != nil {
|
|
||||||
statusLabel.SetText(fmt.Sprintf("Error: Invalid duration: %v", err))
|
|
||||||
enableUIControls(uiControls)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
params.duration = minutes
|
|
||||||
|
|
||||||
statusLabel.SetText(fmt.Sprintf("Running in debug mode for %d minutes...", int(minutes.Minutes())))
|
|
||||||
progressBar.Show()
|
|
||||||
progressBar.SetValue(0)
|
|
||||||
|
|
||||||
go s.handleRunForDuration(
|
|
||||||
statusLabel,
|
|
||||||
progressBar,
|
|
||||||
uiControls,
|
|
||||||
w,
|
|
||||||
params,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
statusLabel.SetText("Creating debug bundle...")
|
|
||||||
go s.handleDebugCreation(
|
|
||||||
params,
|
|
||||||
statusLabel,
|
|
||||||
uiControls,
|
|
||||||
w,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *serviceClient) handleRunForDuration(
|
|
||||||
statusLabel *widget.Label,
|
|
||||||
progressBar *widget.ProgressBar,
|
|
||||||
uiControls []fyne.Disableable,
|
|
||||||
w fyne.Window,
|
|
||||||
params *debugCollectionParams,
|
|
||||||
) {
|
|
||||||
progressUI := &progressUI{
|
|
||||||
statusLabel: statusLabel,
|
|
||||||
progressBar: progressBar,
|
|
||||||
uiControls: uiControls,
|
|
||||||
window: w,
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := s.getSrvClient(failFastTimeout)
|
|
||||||
if err != nil {
|
|
||||||
handleError(progressUI, fmt.Sprintf("Failed to get client for debug: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
initialState, err := s.getInitialState(conn)
|
|
||||||
if err != nil {
|
|
||||||
handleError(progressUI, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer s.restoreServiceState(conn, initialState)
|
|
||||||
|
|
||||||
if err := s.collectDebugData(conn, initialState, params, progressUI); err != nil {
|
|
||||||
handleError(progressUI, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.createDebugBundleFromCollection(conn, params, progressUI); err != nil {
|
|
||||||
handleError(progressUI, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
progressUI.statusLabel.SetText("Bundle created successfully")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get initial state of the service
|
|
||||||
func (s *serviceClient) getInitialState(conn proto.DaemonServiceClient) (*debugInitialState, error) {
|
|
||||||
statusResp, err := conn.Status(s.ctx, &proto.StatusRequest{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf(" get status: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logLevelResp, err := conn.GetLogLevel(s.ctx, &proto.GetLogLevelRequest{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("get log level: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
wasDown := statusResp.Status != string(internal.StatusConnected) &&
|
|
||||||
statusResp.Status != string(internal.StatusConnecting)
|
|
||||||
|
|
||||||
initialLogLevel := logLevelResp.GetLevel()
|
|
||||||
initialLevelTrace := initialLogLevel >= proto.LogLevel_TRACE
|
|
||||||
|
|
||||||
return &debugInitialState{
|
|
||||||
wasDown: wasDown,
|
|
||||||
logLevel: initialLogLevel,
|
|
||||||
isLevelTrace: initialLevelTrace,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle progress tracking during collection
|
|
||||||
func startProgressTracker(ctx context.Context, wg *sync.WaitGroup, duration time.Duration, progress *progressUI) {
|
|
||||||
progress.progressBar.Show()
|
|
||||||
progress.progressBar.SetValue(0)
|
|
||||||
|
|
||||||
startTime := time.Now()
|
|
||||||
endTime := startTime.Add(duration)
|
|
||||||
wg.Add(1)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
ticker := time.NewTicker(500 * time.Millisecond)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case <-ticker.C:
|
|
||||||
remaining := time.Until(endTime)
|
|
||||||
if remaining <= 0 {
|
|
||||||
remaining = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
elapsed := time.Since(startTime)
|
|
||||||
progressVal := float64(elapsed) / float64(duration)
|
|
||||||
if progressVal > 1.0 {
|
|
||||||
progressVal = 1.0
|
|
||||||
}
|
|
||||||
|
|
||||||
progress.progressBar.SetValue(progressVal)
|
|
||||||
progress.statusLabel.SetText(fmt.Sprintf("Running with trace logs... %s remaining", formatDuration(remaining)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *serviceClient) configureServiceForDebug(
|
|
||||||
conn proto.DaemonServiceClient,
|
|
||||||
state *debugInitialState,
|
|
||||||
params *debugCollectionParams,
|
|
||||||
) {
|
|
||||||
if state.wasDown {
|
|
||||||
if _, err := conn.Up(s.ctx, &proto.UpRequest{}); err != nil {
|
|
||||||
log.Warnf("failed to bring service up: %v", err)
|
|
||||||
} else {
|
|
||||||
log.Info("Service brought up for debug")
|
|
||||||
time.Sleep(time.Second * 10)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !state.isLevelTrace {
|
|
||||||
if _, err := conn.SetLogLevel(s.ctx, &proto.SetLogLevelRequest{Level: proto.LogLevel_TRACE}); err != nil {
|
|
||||||
log.Warnf("failed to set log level to TRACE: %v", err)
|
|
||||||
} else {
|
|
||||||
log.Info("Log level set to TRACE for debug")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := conn.Down(s.ctx, &proto.DownRequest{}); err != nil {
|
|
||||||
log.Warnf("failed to bring service down: %v", err)
|
|
||||||
} else {
|
|
||||||
state.needsRestoreUp = !state.wasDown
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
if params.enablePersistence {
|
|
||||||
if _, err := conn.SetSyncResponsePersistence(s.ctx, &proto.SetSyncResponsePersistenceRequest{
|
|
||||||
Enabled: true,
|
|
||||||
}); err != nil {
|
|
||||||
log.Warnf("failed to enable sync response persistence: %v", err)
|
|
||||||
} else {
|
|
||||||
log.Info("Sync response persistence enabled for debug")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := conn.Up(s.ctx, &proto.UpRequest{}); err != nil {
|
|
||||||
log.Warnf("failed to bring service back up: %v", err)
|
|
||||||
} else {
|
|
||||||
state.needsRestoreUp = false
|
|
||||||
time.Sleep(time.Second * 3)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := conn.StartCPUProfile(s.ctx, &proto.StartCPUProfileRequest{}); err != nil {
|
|
||||||
log.Warnf("failed to start CPU profiling: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.startBundleCaptureIfEnabled(conn, params)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *serviceClient) startBundleCaptureIfEnabled(conn proto.DaemonServiceClient, params *debugCollectionParams) {
|
|
||||||
if !params.capture {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxCapture = 10 * time.Minute
|
|
||||||
timeout := params.duration + 30*time.Second
|
|
||||||
if timeout > maxCapture {
|
|
||||||
timeout = maxCapture
|
|
||||||
log.Warnf("packet capture clamped to %s (server maximum)", maxCapture)
|
|
||||||
}
|
|
||||||
if _, err := conn.StartBundleCapture(s.ctx, &proto.StartBundleCaptureRequest{
|
|
||||||
Timeout: durationpb.New(timeout),
|
|
||||||
}); err != nil {
|
|
||||||
log.Warnf("failed to start bundle capture: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *serviceClient) collectDebugData(
|
|
||||||
conn proto.DaemonServiceClient,
|
|
||||||
state *debugInitialState,
|
|
||||||
params *debugCollectionParams,
|
|
||||||
progress *progressUI,
|
|
||||||
) error {
|
|
||||||
ctx, cancel := context.WithTimeout(s.ctx, params.duration)
|
|
||||||
defer cancel()
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
startProgressTracker(ctx, &wg, params.duration, progress)
|
|
||||||
|
|
||||||
s.configureServiceForDebug(conn, state, params)
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
progress.progressBar.Hide()
|
|
||||||
progress.statusLabel.SetText("Collecting debug data...")
|
|
||||||
|
|
||||||
if _, err := conn.StopCPUProfile(s.ctx, &proto.StopCPUProfileRequest{}); err != nil {
|
|
||||||
log.Warnf("failed to stop CPU profiling: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if params.capture {
|
|
||||||
stopCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
if _, err := conn.StopBundleCapture(stopCtx, &proto.StopBundleCaptureRequest{}); err != nil {
|
|
||||||
log.Warnf("failed to stop bundle capture: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the debug bundle with collected data
|
|
||||||
func (s *serviceClient) createDebugBundleFromCollection(
|
|
||||||
conn proto.DaemonServiceClient,
|
|
||||||
params *debugCollectionParams,
|
|
||||||
progress *progressUI,
|
|
||||||
) error {
|
|
||||||
progress.statusLabel.SetText("Creating debug bundle with collected logs...")
|
|
||||||
|
|
||||||
request := &proto.DebugBundleRequest{
|
|
||||||
Anonymize: params.anonymize,
|
|
||||||
SystemInfo: params.systemInfo,
|
|
||||||
}
|
|
||||||
|
|
||||||
if params.upload {
|
|
||||||
request.UploadURL = params.uploadURL
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := conn.DebugBundle(s.ctx, request)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("create debug bundle: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show appropriate dialog based on upload status
|
|
||||||
localPath := resp.GetPath()
|
|
||||||
uploadFailureReason := resp.GetUploadFailureReason()
|
|
||||||
uploadedKey := resp.GetUploadedKey()
|
|
||||||
|
|
||||||
if params.upload {
|
|
||||||
if uploadFailureReason != "" {
|
|
||||||
showUploadFailedDialog(progress.window, localPath, uploadFailureReason)
|
|
||||||
} else {
|
|
||||||
showUploadSuccessDialog(s.app, progress.window, localPath, uploadedKey)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showBundleCreatedDialog(progress.window, localPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
enableUIControls(progress.uiControls)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore service to original state
|
|
||||||
func (s *serviceClient) restoreServiceState(conn proto.DaemonServiceClient, state *debugInitialState) {
|
|
||||||
if state.needsRestoreUp {
|
|
||||||
if _, err := conn.Up(s.ctx, &proto.UpRequest{}); err != nil {
|
|
||||||
log.Warnf("failed to restore up state: %v", err)
|
|
||||||
} else {
|
|
||||||
log.Info("Service state restored to up")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if state.wasDown {
|
|
||||||
if _, err := conn.Down(s.ctx, &proto.DownRequest{}); err != nil {
|
|
||||||
log.Warnf("failed to restore down state: %v", err)
|
|
||||||
} else {
|
|
||||||
log.Info("Service state restored to down")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !state.isLevelTrace {
|
|
||||||
if _, err := conn.SetLogLevel(s.ctx, &proto.SetLogLevelRequest{Level: state.logLevel}); err != nil {
|
|
||||||
log.Warnf("failed to restore log level: %v", err)
|
|
||||||
} else {
|
|
||||||
log.Info("Log level restored to original setting")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle errors during debug collection
|
|
||||||
func handleError(progress *progressUI, errMsg string) {
|
|
||||||
log.Errorf("%s", errMsg)
|
|
||||||
progress.statusLabel.SetText(errMsg)
|
|
||||||
progress.progressBar.Hide()
|
|
||||||
enableUIControls(progress.uiControls)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *serviceClient) handleDebugCreation(
|
|
||||||
params *debugCollectionParams,
|
|
||||||
statusLabel *widget.Label,
|
|
||||||
uiControls []fyne.Disableable,
|
|
||||||
w fyne.Window,
|
|
||||||
) {
|
|
||||||
conn, err := s.getSrvClient(failFastTimeout)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Failed to get client for debug: %v", err)
|
|
||||||
statusLabel.SetText(fmt.Sprintf("Error: %v", err))
|
|
||||||
enableUIControls(uiControls)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if params.capture {
|
|
||||||
if _, err := conn.StartBundleCapture(s.ctx, &proto.StartBundleCaptureRequest{
|
|
||||||
Timeout: durationpb.New(30 * time.Second),
|
|
||||||
}); err != nil {
|
|
||||||
log.Warnf("failed to start bundle capture: %v", err)
|
|
||||||
} else {
|
|
||||||
defer func() {
|
|
||||||
stopCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
if _, err := conn.StopBundleCapture(stopCtx, &proto.StopBundleCaptureRequest{}); err != nil {
|
|
||||||
log.Warnf("failed to stop bundle capture: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
time.Sleep(2 * time.Second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := s.createDebugBundle(params.anonymize, params.systemInfo, params.uploadURL)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Failed to create debug bundle: %v", err)
|
|
||||||
statusLabel.SetText(fmt.Sprintf("Error creating bundle: %v", err))
|
|
||||||
enableUIControls(uiControls)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
localPath := resp.GetPath()
|
|
||||||
uploadFailureReason := resp.GetUploadFailureReason()
|
|
||||||
uploadedKey := resp.GetUploadedKey()
|
|
||||||
|
|
||||||
if params.upload {
|
|
||||||
if uploadFailureReason != "" {
|
|
||||||
showUploadFailedDialog(w, localPath, uploadFailureReason)
|
|
||||||
} else {
|
|
||||||
showUploadSuccessDialog(s.app, w, localPath, uploadedKey)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showBundleCreatedDialog(w, localPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
enableUIControls(uiControls)
|
|
||||||
statusLabel.SetText("Bundle created successfully")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *serviceClient) createDebugBundle(anonymize bool, systemInfo bool, uploadURL string) (*proto.DebugBundleResponse, error) {
|
|
||||||
conn, err := s.getSrvClient(failFastTimeout)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("get client: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
request := &proto.DebugBundleRequest{
|
|
||||||
Anonymize: anonymize,
|
|
||||||
SystemInfo: systemInfo,
|
|
||||||
}
|
|
||||||
|
|
||||||
if uploadURL != "" {
|
|
||||||
request.UploadURL = uploadURL
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := conn.DebugBundle(s.ctx, request)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create debug bundle via daemon: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// formatDuration formats a duration in HH:MM:SS format
|
|
||||||
func formatDuration(d time.Duration) string {
|
|
||||||
d = d.Round(time.Second)
|
|
||||||
h := d / time.Hour
|
|
||||||
d %= time.Hour
|
|
||||||
m := d / time.Minute
|
|
||||||
d %= time.Minute
|
|
||||||
s := d / time.Second
|
|
||||||
return fmt.Sprintf("%02d:%02d:%02d", h, m, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// createButtonWithAction creates a button with the given label and action
|
|
||||||
func createButtonWithAction(label string, action func()) *widget.Button {
|
|
||||||
button := widget.NewButton(label, action)
|
|
||||||
return button
|
|
||||||
}
|
|
||||||
|
|
||||||
// showUploadFailedDialog displays a dialog when upload fails
|
|
||||||
func showUploadFailedDialog(w fyne.Window, localPath, failureReason string) {
|
|
||||||
content := container.NewVBox(
|
|
||||||
widget.NewLabel(fmt.Sprintf("Bundle upload failed:\n%s\n\n"+
|
|
||||||
"A local copy was saved at:\n%s", failureReason, localPath)),
|
|
||||||
)
|
|
||||||
|
|
||||||
customDialog := dialog.NewCustom("Upload Failed", "Cancel", content, w)
|
|
||||||
|
|
||||||
buttonBox := container.NewHBox(
|
|
||||||
createButtonWithAction("Open file", func() {
|
|
||||||
log.Infof("Attempting to open local file: %s", localPath)
|
|
||||||
if openErr := open.Start(localPath); openErr != nil {
|
|
||||||
log.Errorf("Failed to open local file '%s': %v", localPath, openErr)
|
|
||||||
dialog.ShowError(fmt.Errorf("open the local file:\n%s\n\nError: %v", localPath, openErr), w)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
createButtonWithAction("Open folder", func() {
|
|
||||||
folderPath := filepath.Dir(localPath)
|
|
||||||
log.Infof("Attempting to open local folder: %s", folderPath)
|
|
||||||
if openErr := open.Start(folderPath); openErr != nil {
|
|
||||||
log.Errorf("Failed to open local folder '%s': %v", folderPath, openErr)
|
|
||||||
dialog.ShowError(fmt.Errorf("open the local folder:\n%s\n\nError: %v", folderPath, openErr), w)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
content.Add(buttonBox)
|
|
||||||
customDialog.Show()
|
|
||||||
}
|
|
||||||
|
|
||||||
// showUploadSuccessDialog displays a dialog when upload succeeds
|
|
||||||
func showUploadSuccessDialog(a fyne.App, w fyne.Window, localPath, uploadedKey string) {
|
|
||||||
log.Infof("Upload key: %s", uploadedKey)
|
|
||||||
keyEntry := widget.NewEntry()
|
|
||||||
keyEntry.SetText(uploadedKey)
|
|
||||||
keyEntry.Disable()
|
|
||||||
|
|
||||||
content := container.NewVBox(
|
|
||||||
widget.NewLabel("Bundle uploaded successfully!"),
|
|
||||||
widget.NewLabel(""),
|
|
||||||
widget.NewLabel("Upload key:"),
|
|
||||||
keyEntry,
|
|
||||||
widget.NewLabel(""),
|
|
||||||
widget.NewLabel(fmt.Sprintf("Local copy saved at:\n%s", localPath)),
|
|
||||||
)
|
|
||||||
|
|
||||||
customDialog := dialog.NewCustom("Upload Successful", "OK", content, w)
|
|
||||||
|
|
||||||
copyBtn := createButtonWithAction("Copy key", func() {
|
|
||||||
a.Clipboard().SetContent(uploadedKey)
|
|
||||||
log.Info("Upload key copied to clipboard")
|
|
||||||
})
|
|
||||||
|
|
||||||
buttonBox := createButtonBox(localPath, w, copyBtn)
|
|
||||||
content.Add(buttonBox)
|
|
||||||
customDialog.Show()
|
|
||||||
}
|
|
||||||
|
|
||||||
// showBundleCreatedDialog displays a dialog when bundle is created without upload
|
|
||||||
func showBundleCreatedDialog(w fyne.Window, localPath string) {
|
|
||||||
content := container.NewVBox(
|
|
||||||
widget.NewLabel(fmt.Sprintf("Bundle created locally at:\n%s\n\n"+
|
|
||||||
"Administrator privileges may be required to access the file.", localPath)),
|
|
||||||
)
|
|
||||||
|
|
||||||
customDialog := dialog.NewCustom("Debug Bundle Created", "Cancel", content, w)
|
|
||||||
|
|
||||||
buttonBox := createButtonBox(localPath, w, nil)
|
|
||||||
content.Add(buttonBox)
|
|
||||||
customDialog.Show()
|
|
||||||
}
|
|
||||||
|
|
||||||
func createButtonBox(localPath string, w fyne.Window, elems ...fyne.Widget) *fyne.Container {
|
|
||||||
box := container.NewHBox()
|
|
||||||
for _, elem := range elems {
|
|
||||||
box.Add(elem)
|
|
||||||
}
|
|
||||||
|
|
||||||
fileBtn := createButtonWithAction("Open file", func() {
|
|
||||||
log.Infof("Attempting to open local file: %s", localPath)
|
|
||||||
if openErr := open.Start(localPath); openErr != nil {
|
|
||||||
log.Errorf("Failed to open local file '%s': %v", localPath, openErr)
|
|
||||||
dialog.ShowError(fmt.Errorf("open the local file:\n%s\n\nError: %v", localPath, openErr), w)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
folderBtn := createButtonWithAction("Open folder", func() {
|
|
||||||
folderPath := filepath.Dir(localPath)
|
|
||||||
log.Infof("Attempting to open local folder: %s", folderPath)
|
|
||||||
if openErr := open.Start(folderPath); openErr != nil {
|
|
||||||
log.Errorf("Failed to open local folder '%s': %v", folderPath, openErr)
|
|
||||||
dialog.ShowError(fmt.Errorf("open the local folder:\n%s\n\nError: %v", folderPath, openErr), w)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
box.Add(fileBtn)
|
|
||||||
box.Add(folderBtn)
|
|
||||||
|
|
||||||
return box
|
|
||||||
}
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
package event
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/cenkalti/backoff/v4"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"google.golang.org/grpc"
|
|
||||||
"google.golang.org/grpc/credentials/insecure"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/proto"
|
|
||||||
"github.com/netbirdio/netbird/client/ui/desktop"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Notifier sends desktop notifications. Defined here so the event package
|
|
||||||
// does not depend on fyne or the platform-specific notifier implementation.
|
|
||||||
type Notifier interface {
|
|
||||||
Send(title, body string)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Handler func(*proto.SystemEvent)
|
|
||||||
|
|
||||||
type Manager struct {
|
|
||||||
notifier Notifier
|
|
||||||
addr string
|
|
||||||
|
|
||||||
mu sync.Mutex
|
|
||||||
ctx context.Context
|
|
||||||
cancel context.CancelFunc
|
|
||||||
enabled bool
|
|
||||||
handlers []Handler
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewManager(notifier Notifier, addr string) *Manager {
|
|
||||||
return &Manager{
|
|
||||||
notifier: notifier,
|
|
||||||
addr: addr,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Manager) Start(ctx context.Context) {
|
|
||||||
e.mu.Lock()
|
|
||||||
e.ctx, e.cancel = context.WithCancel(ctx)
|
|
||||||
e.mu.Unlock()
|
|
||||||
|
|
||||||
expBackOff := backoff.WithContext(&backoff.ExponentialBackOff{
|
|
||||||
InitialInterval: time.Second,
|
|
||||||
RandomizationFactor: backoff.DefaultRandomizationFactor,
|
|
||||||
Multiplier: backoff.DefaultMultiplier,
|
|
||||||
MaxInterval: 10 * time.Second,
|
|
||||||
MaxElapsedTime: 0,
|
|
||||||
Stop: backoff.Stop,
|
|
||||||
Clock: backoff.SystemClock,
|
|
||||||
}, ctx)
|
|
||||||
|
|
||||||
if err := backoff.Retry(e.streamEvents, expBackOff); err != nil {
|
|
||||||
log.Errorf("event stream ended: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Manager) streamEvents() error {
|
|
||||||
e.mu.Lock()
|
|
||||||
ctx := e.ctx
|
|
||||||
e.mu.Unlock()
|
|
||||||
|
|
||||||
client, err := getClient(e.addr)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("create client: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
stream, err := client.SubscribeEvents(ctx, &proto.SubscribeRequest{})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to subscribe to events: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("subscribed to daemon events")
|
|
||||||
defer func() {
|
|
||||||
log.Info("unsubscribed from daemon events")
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
|
||||||
event, err := stream.Recv()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error receiving event: %w", err)
|
|
||||||
}
|
|
||||||
e.handleEvent(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Manager) Stop() {
|
|
||||||
e.mu.Lock()
|
|
||||||
defer e.mu.Unlock()
|
|
||||||
if e.cancel != nil {
|
|
||||||
e.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Manager) SetNotificationsEnabled(enabled bool) {
|
|
||||||
e.mu.Lock()
|
|
||||||
defer e.mu.Unlock()
|
|
||||||
e.enabled = enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Manager) handleEvent(event *proto.SystemEvent) {
|
|
||||||
e.mu.Lock()
|
|
||||||
enabled := e.enabled
|
|
||||||
handlers := slices.Clone(e.handlers)
|
|
||||||
e.mu.Unlock()
|
|
||||||
|
|
||||||
if event.UserMessage != "" && (enabled || event.Severity == proto.SystemEvent_CRITICAL) {
|
|
||||||
title := e.getEventTitle(event)
|
|
||||||
body := event.UserMessage
|
|
||||||
id := event.Metadata["id"]
|
|
||||||
if id != "" {
|
|
||||||
body += fmt.Sprintf(" ID: %s", id)
|
|
||||||
}
|
|
||||||
e.notifier.Send(title, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, handler := range handlers {
|
|
||||||
go handler(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Manager) AddHandler(handler Handler) {
|
|
||||||
e.mu.Lock()
|
|
||||||
defer e.mu.Unlock()
|
|
||||||
e.handlers = append(e.handlers, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Manager) getEventTitle(event *proto.SystemEvent) string {
|
|
||||||
var prefix string
|
|
||||||
switch event.Severity {
|
|
||||||
case proto.SystemEvent_CRITICAL:
|
|
||||||
prefix = "Critical"
|
|
||||||
case proto.SystemEvent_ERROR:
|
|
||||||
prefix = "Error"
|
|
||||||
case proto.SystemEvent_WARNING:
|
|
||||||
prefix = "Warning"
|
|
||||||
default:
|
|
||||||
prefix = "Info"
|
|
||||||
}
|
|
||||||
|
|
||||||
var category string
|
|
||||||
switch event.Category {
|
|
||||||
case proto.SystemEvent_DNS:
|
|
||||||
category = "DNS"
|
|
||||||
case proto.SystemEvent_NETWORK:
|
|
||||||
category = "Network"
|
|
||||||
case proto.SystemEvent_AUTHENTICATION:
|
|
||||||
category = "Authentication"
|
|
||||||
case proto.SystemEvent_CONNECTIVITY:
|
|
||||||
category = "Connectivity"
|
|
||||||
default:
|
|
||||||
category = "System"
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s: %s", prefix, category)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getClient(addr string) (proto.DaemonServiceClient, error) {
|
|
||||||
conn, err := grpc.NewClient(
|
|
||||||
strings.TrimPrefix(addr, "tcp://"),
|
|
||||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
|
||||||
grpc.WithUserAgent(desktop.GetUIUserAgent()),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return proto.NewDaemonServiceClient(conn), nil
|
|
||||||
}
|
|
||||||
@@ -1,326 +0,0 @@
|
|||||||
//go:build !(linux && 386)
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
|
|
||||||
"fyne.io/systray"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"google.golang.org/grpc/codes"
|
|
||||||
"google.golang.org/grpc/status"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/proto"
|
|
||||||
"github.com/netbirdio/netbird/version"
|
|
||||||
)
|
|
||||||
|
|
||||||
type eventHandler struct {
|
|
||||||
client *serviceClient
|
|
||||||
}
|
|
||||||
|
|
||||||
func newEventHandler(client *serviceClient) *eventHandler {
|
|
||||||
return &eventHandler{
|
|
||||||
client: client,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *eventHandler) listen(ctx context.Context) {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case <-h.client.mUp.ClickedCh:
|
|
||||||
h.handleConnectClick()
|
|
||||||
case <-h.client.mDown.ClickedCh:
|
|
||||||
h.handleDisconnectClick()
|
|
||||||
case <-h.client.mAllowSSH.ClickedCh:
|
|
||||||
h.handleAllowSSHClick()
|
|
||||||
case <-h.client.mAutoConnect.ClickedCh:
|
|
||||||
h.handleAutoConnectClick()
|
|
||||||
case <-h.client.mEnableRosenpass.ClickedCh:
|
|
||||||
h.handleRosenpassClick()
|
|
||||||
case <-h.client.mLazyConnEnabled.ClickedCh:
|
|
||||||
h.handleLazyConnectionClick()
|
|
||||||
case <-h.client.mBlockInbound.ClickedCh:
|
|
||||||
h.handleBlockInboundClick()
|
|
||||||
case <-h.client.mAdvancedSettings.ClickedCh:
|
|
||||||
h.handleAdvancedSettingsClick()
|
|
||||||
case <-h.client.mCreateDebugBundle.ClickedCh:
|
|
||||||
h.handleCreateDebugBundleClick()
|
|
||||||
case <-h.client.mQuit.ClickedCh:
|
|
||||||
h.handleQuitClick()
|
|
||||||
return
|
|
||||||
case <-h.client.mGitHub.ClickedCh:
|
|
||||||
h.handleGitHubClick()
|
|
||||||
case <-h.client.mUpdate.ClickedCh:
|
|
||||||
h.handleUpdateClick()
|
|
||||||
case <-h.client.mNetworks.ClickedCh:
|
|
||||||
h.handleNetworksClick()
|
|
||||||
case <-h.client.mNotifications.ClickedCh:
|
|
||||||
h.handleNotificationsClick()
|
|
||||||
case <-systray.TrayOpenedCh:
|
|
||||||
h.client.updateExitNodes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *eventHandler) handleConnectClick() {
|
|
||||||
h.client.mUp.Disable()
|
|
||||||
|
|
||||||
if h.client.connectCancel != nil {
|
|
||||||
h.client.connectCancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
connectCtx, connectCancel := context.WithCancel(h.client.ctx)
|
|
||||||
h.client.connectCancel = connectCancel
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer connectCancel()
|
|
||||||
|
|
||||||
if err := h.client.menuUpClick(connectCtx); err != nil {
|
|
||||||
st, ok := status.FromError(err)
|
|
||||||
if errors.Is(err, context.Canceled) || (ok && st.Code() == codes.Canceled) {
|
|
||||||
log.Debugf("connect operation cancelled by user")
|
|
||||||
} else {
|
|
||||||
h.client.notifier.Send("Error", "Failed to connect")
|
|
||||||
log.Errorf("connect failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.client.updateStatus(); err != nil {
|
|
||||||
log.Debugf("failed to update status after connect: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *eventHandler) handleDisconnectClick() {
|
|
||||||
h.client.mDown.Disable()
|
|
||||||
h.client.cancelExitNodeRetry()
|
|
||||||
|
|
||||||
if h.client.connectCancel != nil {
|
|
||||||
log.Debugf("cancelling ongoing connect operation")
|
|
||||||
h.client.connectCancel()
|
|
||||||
h.client.connectCancel = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
if err := h.client.menuDownClick(); err != nil {
|
|
||||||
st, ok := status.FromError(err)
|
|
||||||
if !errors.Is(err, context.Canceled) && !(ok && st.Code() == codes.Canceled) {
|
|
||||||
h.client.notifier.Send("Error", "Failed to disconnect")
|
|
||||||
log.Errorf("disconnect failed: %v", err)
|
|
||||||
} else {
|
|
||||||
log.Debugf("disconnect cancelled or already disconnecting")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.client.updateStatus(); err != nil {
|
|
||||||
log.Debugf("failed to update status after disconnect: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *eventHandler) handleAllowSSHClick() {
|
|
||||||
h.toggleCheckbox(h.client.mAllowSSH)
|
|
||||||
if err := h.updateConfigWithErr(); err != nil {
|
|
||||||
h.toggleCheckbox(h.client.mAllowSSH) // revert checkbox state on error
|
|
||||||
log.Errorf("failed to update config: %v", err)
|
|
||||||
h.client.notifier.Send("Error", "Failed to update SSH settings")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *eventHandler) handleAutoConnectClick() {
|
|
||||||
h.toggleCheckbox(h.client.mAutoConnect)
|
|
||||||
if err := h.updateConfigWithErr(); err != nil {
|
|
||||||
h.toggleCheckbox(h.client.mAutoConnect) // revert checkbox state on error
|
|
||||||
log.Errorf("failed to update config: %v", err)
|
|
||||||
h.client.notifier.Send("Error", "Failed to update auto-connect settings")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *eventHandler) handleRosenpassClick() {
|
|
||||||
h.toggleCheckbox(h.client.mEnableRosenpass)
|
|
||||||
if err := h.updateConfigWithErr(); err != nil {
|
|
||||||
h.toggleCheckbox(h.client.mEnableRosenpass) // revert checkbox state on error
|
|
||||||
log.Errorf("failed to update config: %v", err)
|
|
||||||
h.client.notifier.Send("Error", "Failed to update Rosenpass settings")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *eventHandler) handleLazyConnectionClick() {
|
|
||||||
h.toggleCheckbox(h.client.mLazyConnEnabled)
|
|
||||||
if err := h.updateConfigWithErr(); err != nil {
|
|
||||||
h.toggleCheckbox(h.client.mLazyConnEnabled) // revert checkbox state on error
|
|
||||||
log.Errorf("failed to update config: %v", err)
|
|
||||||
h.client.notifier.Send("Error", "Failed to update lazy connection settings")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *eventHandler) handleBlockInboundClick() {
|
|
||||||
h.toggleCheckbox(h.client.mBlockInbound)
|
|
||||||
if err := h.updateConfigWithErr(); err != nil {
|
|
||||||
h.toggleCheckbox(h.client.mBlockInbound) // revert checkbox state on error
|
|
||||||
log.Errorf("failed to update config: %v", err)
|
|
||||||
h.client.notifier.Send("Error", "Failed to update block inbound settings")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *eventHandler) handleNotificationsClick() {
|
|
||||||
h.toggleCheckbox(h.client.mNotifications)
|
|
||||||
if err := h.updateConfigWithErr(); err != nil {
|
|
||||||
h.toggleCheckbox(h.client.mNotifications) // revert checkbox state on error
|
|
||||||
log.Errorf("failed to update config: %v", err)
|
|
||||||
h.client.notifier.Send("Error", "Failed to update notifications settings")
|
|
||||||
} else if h.client.eventManager != nil {
|
|
||||||
h.client.eventManager.SetNotificationsEnabled(h.client.mNotifications.Checked())
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *eventHandler) handleAdvancedSettingsClick() {
|
|
||||||
h.client.mAdvancedSettings.Disable()
|
|
||||||
go func() {
|
|
||||||
defer h.client.mAdvancedSettings.Enable()
|
|
||||||
defer h.client.getSrvConfig()
|
|
||||||
h.runSelfCommand(h.client.ctx, "settings")
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *eventHandler) handleCreateDebugBundleClick() {
|
|
||||||
h.client.mCreateDebugBundle.Disable()
|
|
||||||
go func() {
|
|
||||||
defer h.client.mCreateDebugBundle.Enable()
|
|
||||||
h.runSelfCommand(h.client.ctx, "debug")
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *eventHandler) handleQuitClick() {
|
|
||||||
systray.Quit()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *eventHandler) handleGitHubClick() {
|
|
||||||
if err := openURL("https://github.com/netbirdio/netbird"); err != nil {
|
|
||||||
log.Errorf("failed to open GitHub URL: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *eventHandler) handleUpdateClick() {
|
|
||||||
h.client.updateIndicationLock.Lock()
|
|
||||||
enforced := h.client.isEnforcedUpdate
|
|
||||||
h.client.updateIndicationLock.Unlock()
|
|
||||||
|
|
||||||
if !enforced {
|
|
||||||
if err := openURL(version.DownloadUrl()); err != nil {
|
|
||||||
log.Errorf("failed to open download URL: %v", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// prevent blocking against a busy server
|
|
||||||
h.client.mUpdate.Disable()
|
|
||||||
go func() {
|
|
||||||
defer h.client.mUpdate.Enable()
|
|
||||||
conn, err := h.client.getSrvClient(defaultFailTimeout)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to get service client for update: %v", err)
|
|
||||||
_ = openURL(version.DownloadUrl())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := conn.TriggerUpdate(h.client.ctx, &proto.TriggerUpdateRequest{})
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("TriggerUpdate failed: %v", err)
|
|
||||||
_ = openURL(version.DownloadUrl())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !resp.Success {
|
|
||||||
log.Errorf("TriggerUpdate failed: %s", resp.ErrorMsg)
|
|
||||||
_ = openURL(version.DownloadUrl())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("update triggered via daemon")
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *eventHandler) handleNetworksClick() {
|
|
||||||
h.client.mNetworks.Disable()
|
|
||||||
go func() {
|
|
||||||
defer h.client.mNetworks.Enable()
|
|
||||||
h.runSelfCommand(h.client.ctx, "networks")
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *eventHandler) toggleCheckbox(item *systray.MenuItem) {
|
|
||||||
if item.Checked() {
|
|
||||||
item.Uncheck()
|
|
||||||
} else {
|
|
||||||
item.Check()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *eventHandler) updateConfigWithErr() error {
|
|
||||||
if err := h.client.updateConfig(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *eventHandler) runSelfCommand(ctx context.Context, command string, args ...string) {
|
|
||||||
proc, err := os.Executable()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("error getting executable path: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the full command arguments
|
|
||||||
cmdArgs := []string{
|
|
||||||
fmt.Sprintf("--%s=true", command),
|
|
||||||
fmt.Sprintf("--daemon-addr=%s", h.client.addr),
|
|
||||||
}
|
|
||||||
cmdArgs = append(cmdArgs, args...)
|
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, proc, cmdArgs...)
|
|
||||||
|
|
||||||
if out := h.client.attachOutput(cmd); out != nil {
|
|
||||||
defer func() {
|
|
||||||
if err := out.Close(); err != nil {
|
|
||||||
log.Errorf("error closing log file %s: %v", h.client.logFile, err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("running command: %s", cmd.String())
|
|
||||||
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
var exitErr *exec.ExitError
|
|
||||||
if errors.As(err, &exitErr) {
|
|
||||||
log.Printf("command '%s' failed with exit code %d", cmd.String(), exitErr.ExitCode())
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("command '%s' completed successfully", cmd.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *eventHandler) logout(ctx context.Context) error {
|
|
||||||
client, err := h.client.getSrvClient(defaultFailTimeout)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get service client: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = client.Logout(ctx, &proto.LogoutRequest{})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("logout failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
h.client.getSrvConfig()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
//go:build freebsd || openbsd || netbsd || dragonfly
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"runtime"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *serviceClient) setDefaultFonts() {
|
|
||||||
paths := []string{
|
|
||||||
"/usr/local/share/fonts/TTF/DejaVuSans.ttf",
|
|
||||||
"/usr/local/share/fonts/dejavu/DejaVuSans.ttf",
|
|
||||||
"/usr/local/share/noto/NotoSans-Regular.ttf",
|
|
||||||
"/usr/local/share/fonts/noto/NotoSans-Regular.ttf",
|
|
||||||
"/usr/local/share/fonts/liberation-fonts-ttf/LiberationSans-Regular.ttf",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, fontPath := range paths {
|
|
||||||
if _, err := os.Stat(fontPath); err == nil {
|
|
||||||
os.Setenv("FYNE_FONT", fontPath)
|
|
||||||
log.Debugf("Using font: %s", fontPath)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Errorf("Failed to find any suitable font files for %s", runtime.GOOS)
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
const defaultFontPath = "/Library/Fonts/Arial Unicode.ttf"
|
|
||||||
|
|
||||||
func (s *serviceClient) setDefaultFonts() {
|
|
||||||
if _, err := os.Stat(defaultFontPath); err != nil {
|
|
||||||
log.Errorf("Failed to find default font file: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
os.Setenv("FYNE_FONT", defaultFontPath)
|
|
||||||
}
|
|
||||||