[client] Add debug for duration option to netbird ui (#3772)

This commit is contained in:
Viktor Liu 2025-05-01 23:25:27 +02:00 committed by GitHub
parent 7b64953eed
commit 01c3719c5d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 586 additions and 102 deletions

View File

@ -235,13 +235,6 @@ func runForDuration(cmd *cobra.Command, args []string) error {
return fmt.Errorf("failed to bundle debug: %v", status.Convert(err).Message()) return fmt.Errorf("failed to bundle debug: %v", status.Convert(err).Message())
} }
// Disable network map persistence after creating the debug bundle
if _, err := client.SetNetworkMapPersistence(cmd.Context(), &proto.SetNetworkMapPersistenceRequest{
Enabled: false,
}); err != nil {
return fmt.Errorf("failed to disable network map persistence: %v", status.Convert(err).Message())
}
if stateWasDown { if stateWasDown {
if _, err := client.Down(cmd.Context(), &proto.DownRequest{}); err != nil { if _, err := client.Down(cmd.Context(), &proto.DownRequest{}); err != nil {
return fmt.Errorf("failed to down: %v", status.Convert(err).Message()) return fmt.Errorf("failed to down: %v", status.Convert(err).Message())

View File

@ -51,14 +51,16 @@ func (s *Server) DebugBundle(_ context.Context, req *proto.DebugBundleRequest) (
} }
if req.GetUploadURL() == "" { if req.GetUploadURL() == "" {
return &proto.DebugBundleResponse{Path: path}, nil return &proto.DebugBundleResponse{Path: path}, nil
} }
key, err := uploadDebugBundle(context.Background(), req.GetUploadURL(), s.config.ManagementURL.String(), path) key, err := uploadDebugBundle(context.Background(), req.GetUploadURL(), s.config.ManagementURL.String(), path)
if err != nil { if err != nil {
log.Errorf("failed to upload debug bundle to %s: %v", req.GetUploadURL(), err)
return &proto.DebugBundleResponse{Path: path, UploadFailureReason: err.Error()}, nil return &proto.DebugBundleResponse{Path: path, UploadFailureReason: err.Error()}, nil
} }
log.Infof("debug bundle uploaded to %s with key %s", req.GetUploadURL(), key)
return &proto.DebugBundleResponse{Path: path, UploadedKey: key}, nil return &proto.DebugBundleResponse{Path: path, UploadedKey: key}, nil
} }

View File

@ -54,11 +54,14 @@ func main() {
daemonAddr, showSettings, showNetworks, showDebug, errorMsg, saveLogsInFile := parseFlags() daemonAddr, showSettings, showNetworks, showDebug, errorMsg, saveLogsInFile := parseFlags()
// Initialize file logging if needed. // Initialize file logging if needed.
var logFile string
if saveLogsInFile { if saveLogsInFile {
if err := initLogFile(); err != nil { file, err := initLogFile()
if err != nil {
log.Errorf("error while initializing log: %v", err) log.Errorf("error while initializing log: %v", err)
return return
} }
logFile = file
} }
// Create the Fyne application. // Create the Fyne application.
@ -72,7 +75,7 @@ func main() {
} }
// Create the service client (this also builds the settings or networks UI if requested). // Create the service client (this also builds the settings or networks UI if requested).
client := newServiceClient(daemonAddr, a, showSettings, showNetworks, showDebug) client := newServiceClient(daemonAddr, logFile, a, showSettings, showNetworks, showDebug)
// Watch for theme/settings changes to update the icon. // Watch for theme/settings changes to update the icon.
go watchSettingsChanges(a, client) go watchSettingsChanges(a, client)
@ -115,9 +118,9 @@ func parseFlags() (daemonAddr string, showSettings, showNetworks, showDebug bool
} }
// initLogFile initializes logging into a file. // initLogFile initializes logging into a file.
func initLogFile() error { func initLogFile() (string, error) {
logFile := path.Join(os.TempDir(), fmt.Sprintf("netbird-ui-%d.log", os.Getpid())) logFile := path.Join(os.TempDir(), fmt.Sprintf("netbird-ui-%d.log", os.Getpid()))
return util.InitLog("trace", logFile) return logFile, util.InitLog("trace", logFile)
} }
// watchSettingsChanges listens for Fyne theme/settings changes and updates the client icon. // watchSettingsChanges listens for Fyne theme/settings changes and updates the client icon.
@ -161,6 +164,7 @@ var iconErrorMacOS []byte
type serviceClient struct { type serviceClient struct {
ctx context.Context ctx context.Context
cancel context.CancelFunc
addr string addr string
conn proto.DaemonServiceClient conn proto.DaemonServiceClient
@ -224,12 +228,13 @@ type serviceClient struct {
updateIndicationLock sync.Mutex updateIndicationLock sync.Mutex
isUpdateIconActive bool isUpdateIconActive bool
showNetworks bool showNetworks bool
wRoutes fyne.Window wNetworks fyne.Window
eventManager *event.Manager eventManager *event.Manager
exitNodeMu sync.Mutex exitNodeMu sync.Mutex
mExitNodeItems []menuHandler mExitNodeItems []menuHandler
logFile string
} }
type menuHandler struct { type menuHandler struct {
@ -240,11 +245,14 @@ type menuHandler struct {
// newServiceClient instance constructor // newServiceClient instance constructor
// //
// This constructor also builds the UI elements for the settings window. // This constructor also builds the UI elements for the settings window.
func newServiceClient(addr string, a fyne.App, showSettings bool, showNetworks bool, showDebug bool) *serviceClient { func newServiceClient(addr string, logFile string, a fyne.App, showSettings bool, showNetworks bool, showDebug bool) *serviceClient {
ctx, cancel := context.WithCancel(context.Background())
s := &serviceClient{ s := &serviceClient{
ctx: context.Background(), ctx: ctx,
cancel: cancel,
addr: addr, addr: addr,
app: a, app: a,
logFile: logFile,
sendNotification: false, sendNotification: false,
showAdvancedSettings: showSettings, showAdvancedSettings: showSettings,
@ -256,9 +264,7 @@ func newServiceClient(addr string, a fyne.App, showSettings bool, showNetworks b
switch { switch {
case showSettings: case showSettings:
s.showSettingsUI() s.showSettingsUI()
return s
case showNetworks: case showNetworks:
s.showNetworksUI() s.showNetworksUI()
case showDebug: case showDebug:
@ -309,6 +315,8 @@ func (s *serviceClient) updateIcon() {
func (s *serviceClient) showSettingsUI() { func (s *serviceClient) showSettingsUI() {
// add settings window UI elements. // add settings window UI elements.
s.wSettings = s.app.NewWindow("NetBird Settings") s.wSettings = s.app.NewWindow("NetBird Settings")
s.wSettings.SetOnClosed(s.cancel)
s.iMngURL = widget.NewEntry() s.iMngURL = widget.NewEntry()
s.iAdminURL = widget.NewEntry() s.iAdminURL = widget.NewEntry()
s.iConfigFile = widget.NewEntry() s.iConfigFile = widget.NewEntry()
@ -784,7 +792,7 @@ func (s *serviceClient) onTrayReady() {
func (s *serviceClient) runSelfCommand(command, arg string) { func (s *serviceClient) runSelfCommand(command, arg string) {
proc, err := os.Executable() proc, err := os.Executable()
if err != nil { if err != nil {
log.Errorf("show %s failed with error: %v", command, err) log.Errorf("Error getting executable path: %v", err)
return return
} }
@ -793,14 +801,48 @@ func (s *serviceClient) runSelfCommand(command, arg string) {
fmt.Sprintf("--daemon-addr=%s", s.addr), fmt.Sprintf("--daemon-addr=%s", s.addr),
) )
out, err := cmd.CombinedOutput() if out := s.attachOutput(cmd); out != nil {
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 { defer func() {
log.Errorf("start %s UI: %v, %s", command, err, string(out)) if err := out.Close(); err != nil {
log.Errorf("Error closing log file %s: %v", s.logFile, err)
}
}()
}
log.Printf("Running command: %s --%s=%s --daemon-addr=%s", proc, command, arg, s.addr)
err = cmd.Run()
if err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
log.Printf("Command '%s %s' failed with exit code %d", command, arg, exitErr.ExitCode())
} else {
log.Printf("Failed to start/run command '%s %s': %v", command, arg, err)
}
return return
} }
if len(out) != 0 {
log.Infof("command %s executed: %s", command, string(out)) log.Printf("Command '%s %s' completed successfully.", command, arg)
}
func (s *serviceClient) attachOutput(cmd *exec.Cmd) *os.File {
if s.logFile == "" {
// attach child's streams to parent's streams
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return nil
} }
out, err := os.OpenFile(s.logFile, os.O_WRONLY|os.O_APPEND, 0)
if err != nil {
log.Errorf("Failed to open log file %s: %v", s.logFile, err)
return nil
}
cmd.Stdout = out
cmd.Stderr = out
return out
} }
func normalizedVersion(version string) string { func normalizedVersion(version string) string {
@ -813,9 +855,7 @@ func normalizedVersion(version string) string {
// onTrayExit is called when the tray icon is closed. // onTrayExit is called when the tray icon is closed.
func (s *serviceClient) onTrayExit() { func (s *serviceClient) onTrayExit() {
for _, item := range s.mExitNodeItems { s.cancel()
item.cancel()
}
} }
// getSrvClient connection to the service. // getSrvClient connection to the service.
@ -824,7 +864,7 @@ func (s *serviceClient) getSrvClient(timeout time.Duration) (proto.DaemonService
return s.conn, nil return s.conn, nil
} }
ctx, cancel := context.WithTimeout(context.Background(), timeout) ctx, cancel := context.WithTimeout(s.ctx, timeout)
defer cancel() defer cancel()
conn, err := grpc.DialContext( conn, err := grpc.DialContext(

View File

@ -3,8 +3,12 @@
package main package main
import ( import (
"context"
"fmt" "fmt"
"path/filepath" "path/filepath"
"strconv"
"sync"
"time"
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
@ -13,18 +17,46 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/skratchdot/open-golang/open" "github.com/skratchdot/open-golang/open"
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/client/proto"
nbstatus "github.com/netbirdio/netbird/client/status" nbstatus "github.com/netbirdio/netbird/client/status"
uptypes "github.com/netbirdio/netbird/upload-server/types" uptypes "github.com/netbirdio/netbird/upload-server/types"
) )
// Initial state for the debug collection
type debugInitialState struct {
wasDown 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
}
// UI components for progress tracking
type progressUI struct {
statusLabel *widget.Label
progressBar *widget.ProgressBar
uiControls []fyne.Disableable
window fyne.Window
}
func (s *serviceClient) showDebugUI() { func (s *serviceClient) showDebugUI() {
w := s.app.NewWindow("NetBird Debug") w := s.app.NewWindow("NetBird Debug")
w.Resize(fyne.NewSize(600, 400)) w.SetOnClosed(s.cancel)
w.Resize(fyne.NewSize(600, 500))
w.SetFixedSize(true) w.SetFixedSize(true)
anonymizeCheck := widget.NewCheck("Anonymize sensitive information (Public IPs, domains, ...)", nil) anonymizeCheck := widget.NewCheck("Anonymize sensitive information (public IPs, domains, ...)", nil)
systemInfoCheck := widget.NewCheck("Include system information", nil) systemInfoCheck := widget.NewCheck("Include system information (routes, interfaces, ...)", nil)
systemInfoCheck.SetChecked(true) systemInfoCheck.SetChecked(true)
uploadCheck := widget.NewCheck("Upload bundle automatically after creation", nil) uploadCheck := widget.NewCheck("Upload bundle automatically after creation", nil)
uploadCheck.SetChecked(true) uploadCheck.SetChecked(true)
@ -34,11 +66,6 @@ func (s *serviceClient) showDebugUI() {
uploadURL.SetText(uptypes.DefaultBundleURL) uploadURL.SetText(uptypes.DefaultBundleURL)
uploadURL.SetPlaceHolder("Enter upload URL") uploadURL.SetPlaceHolder("Enter upload URL")
statusLabel := widget.NewLabel("")
statusLabel.Hide()
createButton := widget.NewButton("Create Debug Bundle", nil)
uploadURLContainer := container.NewVBox( uploadURLContainer := container.NewVBox(
uploadURLLabel, uploadURLLabel,
uploadURL, uploadURL,
@ -52,7 +79,71 @@ func (s *serviceClient) showDebugUI() {
} }
} }
createButton.OnTapped = s.getCreateHandler(createButton, statusLabel, uploadCheck, uploadURL, anonymizeCheck, systemInfoCheck, w) debugModeContainer := container.NewHBox()
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()
}
}
debugModeContainer.Add(runForDurationCheck)
debugModeContainer.Add(forLabel)
debugModeContainer.Add(durationInput)
debugModeContainer.Add(minutesLabel)
statusLabel := widget.NewLabel("")
statusLabel.Hide()
progressBar := widget.NewProgressBar()
progressBar.Hide()
createButton := widget.NewButton("Create Debug Bundle", nil)
// UI controls that should be disabled during debug collection
uiControls := []fyne.Disableable{
anonymizeCheck,
systemInfoCheck,
uploadCheck,
uploadURL,
runForDurationCheck,
durationInput,
createButton,
}
createButton.OnTapped = s.getCreateHandler(
statusLabel,
progressBar,
uploadCheck,
uploadURL,
anonymizeCheck,
systemInfoCheck,
runForDurationCheck,
durationInput,
uiControls,
w,
)
content := container.NewVBox( content := container.NewVBox(
widget.NewLabel("Create a debug bundle to help troubleshoot issues with NetBird"), widget.NewLabel("Create a debug bundle to help troubleshoot issues with NetBird"),
@ -62,7 +153,11 @@ func (s *serviceClient) showDebugUI() {
uploadCheck, uploadCheck,
uploadURLContainer, uploadURLContainer,
widget.NewLabel(""), widget.NewLabel(""),
debugModeContainer,
noteLabel,
widget.NewLabel(""),
statusLabel, statusLabel,
progressBar,
createButton, createButton,
) )
@ -72,18 +167,46 @@ func (s *serviceClient) showDebugUI() {
w.Show() w.Show()
} }
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( func (s *serviceClient) getCreateHandler(
createButton *widget.Button,
statusLabel *widget.Label, statusLabel *widget.Label,
progressBar *widget.ProgressBar,
uploadCheck *widget.Check, uploadCheck *widget.Check,
uploadURL *widget.Entry, uploadURL *widget.Entry,
anonymizeCheck *widget.Check, anonymizeCheck *widget.Check,
systemInfoCheck *widget.Check, systemInfoCheck *widget.Check,
runForDurationCheck *widget.Check,
duration *widget.Entry,
uiControls []fyne.Disableable,
w fyne.Window, w fyne.Window,
) func() { ) func() {
return func() { return func() {
createButton.Disable() disableUIControls(uiControls)
statusLabel.SetText("Creating debug bundle...")
statusLabel.Show() statusLabel.Show()
var url string var url string
@ -91,13 +214,320 @@ func (s *serviceClient) getCreateHandler(
url = uploadURL.Text url = uploadURL.Text
if url == "" { if url == "" {
statusLabel.SetText("Error: Upload URL is required when upload is enabled") statusLabel.SetText("Error: Upload URL is required when upload is enabled")
createButton.Enable() enableUIControls(uiControls)
return return
} }
} }
go s.handleDebugCreation(anonymizeCheck.Checked, systemInfoCheck.Checked, uploadCheck.Checked, url, statusLabel, createButton, w) params := &debugCollectionParams{
anonymize: anonymizeCheck.Checked,
systemInfo: systemInfoCheck.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(
anonymizeCheck.Checked,
systemInfoCheck.Checked,
uploadCheck.Checked,
url,
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
}
statusOutput, err := s.collectDebugData(conn, initialState, params, progressUI)
if err != nil {
handleError(progressUI, err.Error())
return
}
if err := s.createDebugBundleFromCollection(conn, params, statusOutput, progressUI); err != nil {
handleError(progressUI, err.Error())
return
}
s.restoreServiceState(conn, initialState)
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,
enablePersistence bool,
) error {
if state.wasDown {
if _, err := conn.Up(s.ctx, &proto.UpRequest{}); err != nil {
return fmt.Errorf("bring service up: %v", err)
}
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 {
return fmt.Errorf("set log level to TRACE: %v", err)
}
log.Info("Log level set to TRACE for debug")
}
if _, err := conn.Down(s.ctx, &proto.DownRequest{}); err != nil {
return fmt.Errorf("bring service down: %v", err)
}
time.Sleep(time.Second)
if enablePersistence {
if _, err := conn.SetNetworkMapPersistence(s.ctx, &proto.SetNetworkMapPersistenceRequest{
Enabled: true,
}); err != nil {
return fmt.Errorf("enable network map persistence: %v", err)
}
log.Info("Network map persistence enabled for debug")
}
if _, err := conn.Up(s.ctx, &proto.UpRequest{}); err != nil {
return fmt.Errorf("bring service back up: %v", err)
}
time.Sleep(time.Second * 3)
return nil
}
func (s *serviceClient) collectDebugData(
conn proto.DaemonServiceClient,
state *debugInitialState,
params *debugCollectionParams,
progress *progressUI,
) (string, error) {
ctx, cancel := context.WithTimeout(s.ctx, params.duration)
defer cancel()
var wg sync.WaitGroup
startProgressTracker(ctx, &wg, params.duration, progress)
if err := s.configureServiceForDebug(conn, state, params.enablePersistence); err != nil {
return "", err
}
postUpStatus, err := conn.Status(s.ctx, &proto.StatusRequest{GetFullPeerStatus: true})
if err != nil {
log.Warnf("Failed to get post-up status: %v", err)
}
var postUpStatusOutput string
if postUpStatus != nil {
overview := nbstatus.ConvertToStatusOutputOverview(postUpStatus, params.anonymize, "", nil, nil, nil)
postUpStatusOutput = nbstatus.ParseToFullDetailSummary(overview)
}
headerPostUp := fmt.Sprintf("----- NetBird post-up - Timestamp: %s", time.Now().Format(time.RFC3339))
statusOutput := fmt.Sprintf("%s\n%s", headerPostUp, postUpStatusOutput)
wg.Wait()
progress.progressBar.Hide()
progress.statusLabel.SetText("Collecting debug data...")
preDownStatus, err := conn.Status(s.ctx, &proto.StatusRequest{GetFullPeerStatus: true})
if err != nil {
log.Warnf("Failed to get pre-down status: %v", err)
}
var preDownStatusOutput string
if preDownStatus != nil {
overview := nbstatus.ConvertToStatusOutputOverview(preDownStatus, params.anonymize, "", nil, nil, nil)
preDownStatusOutput = nbstatus.ParseToFullDetailSummary(overview)
}
headerPreDown := fmt.Sprintf("----- NetBird pre-down - Timestamp: %s - Duration: %s",
time.Now().Format(time.RFC3339), params.duration)
statusOutput = fmt.Sprintf("%s\n%s\n%s", statusOutput, headerPreDown, preDownStatusOutput)
return statusOutput, nil
}
// Create the debug bundle with collected data
func (s *serviceClient) createDebugBundleFromCollection(
conn proto.DaemonServiceClient,
params *debugCollectionParams,
statusOutput string,
progress *progressUI,
) error {
progress.statusLabel.SetText("Creating debug bundle with collected logs...")
request := &proto.DebugBundleRequest{
Anonymize: params.anonymize,
Status: statusOutput,
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(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.wasDown {
if _, err := conn.Down(s.ctx, &proto.DownRequest{}); err != nil {
log.Errorf("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.Errorf("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( func (s *serviceClient) handleDebugCreation(
@ -106,7 +536,7 @@ func (s *serviceClient) handleDebugCreation(
upload bool, upload bool,
uploadURL string, uploadURL string,
statusLabel *widget.Label, statusLabel *widget.Label,
createButton *widget.Button, uiControls []fyne.Disableable,
w fyne.Window, w fyne.Window,
) { ) {
log.Infof("Creating debug bundle (Anonymized: %v, System Info: %v, Upload Attempt: %v)...", log.Infof("Creating debug bundle (Anonymized: %v, System Info: %v, Upload Attempt: %v)...",
@ -116,7 +546,7 @@ func (s *serviceClient) handleDebugCreation(
if err != nil { if err != nil {
log.Errorf("Failed to create debug bundle: %v", err) log.Errorf("Failed to create debug bundle: %v", err)
statusLabel.SetText(fmt.Sprintf("Error creating bundle: %v", err)) statusLabel.SetText(fmt.Sprintf("Error creating bundle: %v", err))
createButton.Enable() enableUIControls(uiControls)
return return
} }
@ -134,7 +564,7 @@ func (s *serviceClient) handleDebugCreation(
showBundleCreatedDialog(w, localPath) showBundleCreatedDialog(w, localPath)
} }
createButton.Enable() enableUIControls(uiControls)
statusLabel.SetText("Bundle created successfully") statusLabel.SetText("Bundle created successfully")
} }
@ -173,32 +603,47 @@ func (s *serviceClient) createDebugBundle(anonymize bool, systemInfo bool, uploa
return resp, nil 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 // showUploadFailedDialog displays a dialog when upload fails
func showUploadFailedDialog(parent fyne.Window, localPath, failureReason string) { func showUploadFailedDialog(w fyne.Window, localPath, failureReason string) {
content := container.NewVBox( content := container.NewVBox(
widget.NewLabel(fmt.Sprintf("Bundle upload failed:\n%s\n\n"+ widget.NewLabel(fmt.Sprintf("Bundle upload failed:\n%s\n\n"+
"A local copy was saved at:\n%s", failureReason, localPath)), "A local copy was saved at:\n%s", failureReason, localPath)),
) )
customDialog := dialog.NewCustom("Upload Failed", "Cancel", content, parent) customDialog := dialog.NewCustom("Upload Failed", "Cancel", content, w)
buttonBox := container.NewHBox( buttonBox := container.NewHBox(
widget.NewButton("Open File", func() { createButtonWithAction("Open file", func() {
log.Infof("Attempting to open local file: %s", localPath) log.Infof("Attempting to open local file: %s", localPath)
if openErr := open.Start(localPath); openErr != nil { if openErr := open.Start(localPath); openErr != nil {
log.Errorf("Failed to open local file '%s': %v", localPath, openErr) log.Errorf("Failed to open local file '%s': %v", localPath, openErr)
dialog.ShowError(fmt.Errorf("Failed to open the local file:\n%s\n\nError: %v", localPath, openErr), parent) dialog.ShowError(fmt.Errorf("open the local file:\n%s\n\nError: %v", localPath, openErr), w)
} }
customDialog.Hide()
}), }),
widget.NewButton("Open Folder", func() { createButtonWithAction("Open folder", func() {
folderPath := filepath.Dir(localPath) folderPath := filepath.Dir(localPath)
log.Infof("Attempting to open local folder: %s", folderPath) log.Infof("Attempting to open local folder: %s", folderPath)
if openErr := open.Start(folderPath); openErr != nil { if openErr := open.Start(folderPath); openErr != nil {
log.Errorf("Failed to open local folder '%s': %v", folderPath, openErr) log.Errorf("Failed to open local folder '%s': %v", folderPath, openErr)
dialog.ShowError(fmt.Errorf("Failed to open the local folder:\n%s\n\nError: %v", folderPath, openErr), parent) dialog.ShowError(fmt.Errorf("open the local folder:\n%s\n\nError: %v", folderPath, openErr), w)
} }
customDialog.Hide()
}), }),
) )
@ -207,7 +652,8 @@ func showUploadFailedDialog(parent fyne.Window, localPath, failureReason string)
} }
// showUploadSuccessDialog displays a dialog when upload succeeds // showUploadSuccessDialog displays a dialog when upload succeeds
func showUploadSuccessDialog(parent fyne.Window, localPath, uploadedKey string) { func showUploadSuccessDialog(w fyne.Window, localPath, uploadedKey string) {
log.Infof("Upload key: %s", uploadedKey)
keyEntry := widget.NewEntry() keyEntry := widget.NewEntry()
keyEntry.SetText(uploadedKey) keyEntry.SetText(uploadedKey)
keyEntry.Disable() keyEntry.Disable()
@ -215,62 +661,63 @@ func showUploadSuccessDialog(parent fyne.Window, localPath, uploadedKey string)
content := container.NewVBox( content := container.NewVBox(
widget.NewLabel("Bundle uploaded successfully!"), widget.NewLabel("Bundle uploaded successfully!"),
widget.NewLabel(""), widget.NewLabel(""),
widget.NewLabel("Upload Key:"), widget.NewLabel("Upload key:"),
keyEntry, keyEntry,
widget.NewLabel(""), widget.NewLabel(""),
widget.NewLabel(fmt.Sprintf("Local copy saved at:\n%s", localPath)), widget.NewLabel(fmt.Sprintf("Local copy saved at:\n%s", localPath)),
) )
customDialog := dialog.NewCustom("Upload Successful", "OK", content, parent) customDialog := dialog.NewCustom("Upload Successful", "OK", content, w)
buttonBox := container.NewHBox( copyBtn := createButtonWithAction("Copy key", func() {
widget.NewButton("Copy Key", func() { w.Clipboard().SetContent(uploadedKey)
parent.Clipboard().SetContent(uploadedKey)
log.Info("Upload key copied to clipboard") log.Info("Upload key copied to clipboard")
}), })
widget.NewButton("Open Local 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("Failed to open the local folder:\n%s\n\nError: %v", folderPath, openErr), parent)
}
}),
)
buttonBox := createButtonBox(localPath, w, copyBtn)
content.Add(buttonBox) content.Add(buttonBox)
customDialog.Show() customDialog.Show()
} }
// showBundleCreatedDialog displays a dialog when bundle is created without upload // showBundleCreatedDialog displays a dialog when bundle is created without upload
func showBundleCreatedDialog(parent fyne.Window, localPath string) { func showBundleCreatedDialog(w fyne.Window, localPath string) {
content := container.NewVBox( content := container.NewVBox(
widget.NewLabel(fmt.Sprintf("Bundle created locally at:\n%s\n\n"+ widget.NewLabel(fmt.Sprintf("Bundle created locally at:\n%s\n\n"+
"Administrator privileges may be required to access the file.", localPath)), "Administrator privileges may be required to access the file.", localPath)),
) )
customDialog := dialog.NewCustom("Debug Bundle Created", "Cancel", content, parent) customDialog := dialog.NewCustom("Debug Bundle Created", "Cancel", content, w)
buttonBox := container.NewHBox( buttonBox := createButtonBox(localPath, w, nil)
widget.NewButton("Open File", func() { 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) log.Infof("Attempting to open local file: %s", localPath)
if openErr := open.Start(localPath); openErr != nil { if openErr := open.Start(localPath); openErr != nil {
log.Errorf("Failed to open local file '%s': %v", localPath, openErr) log.Errorf("Failed to open local file '%s': %v", localPath, openErr)
dialog.ShowError(fmt.Errorf("Failed to open the local file:\n%s\n\nError: %v", localPath, openErr), parent) dialog.ShowError(fmt.Errorf("open the local file:\n%s\n\nError: %v", localPath, openErr), w)
} }
customDialog.Hide() })
}),
widget.NewButton("Open Folder", func() { folderBtn := createButtonWithAction("Open folder", func() {
folderPath := filepath.Dir(localPath) folderPath := filepath.Dir(localPath)
log.Infof("Attempting to open local folder: %s", folderPath) log.Infof("Attempting to open local folder: %s", folderPath)
if openErr := open.Start(folderPath); openErr != nil { if openErr := open.Start(folderPath); openErr != nil {
log.Errorf("Failed to open local folder '%s': %v", folderPath, openErr) log.Errorf("Failed to open local folder '%s': %v", folderPath, openErr)
dialog.ShowError(fmt.Errorf("Failed to open the local folder:\n%s\n\nError: %v", folderPath, openErr), parent) dialog.ShowError(fmt.Errorf("open the local folder:\n%s\n\nError: %v", folderPath, openErr), w)
} }
customDialog.Hide() })
}),
)
content.Add(buttonBox) box.Add(fileBtn)
customDialog.Show() box.Add(folderBtn)
return box
} }

View File

@ -34,7 +34,8 @@ const (
type filter string type filter string
func (s *serviceClient) showNetworksUI() { func (s *serviceClient) showNetworksUI() {
s.wRoutes = s.app.NewWindow("Networks") s.wNetworks = s.app.NewWindow("Networks")
s.wNetworks.SetOnClosed(s.cancel)
allGrid := container.New(layout.NewGridLayout(3)) allGrid := container.New(layout.NewGridLayout(3))
go s.updateNetworks(allGrid, allNetworks) go s.updateNetworks(allGrid, allNetworks)
@ -78,8 +79,8 @@ func (s *serviceClient) showNetworksUI() {
content := container.NewBorder(nil, buttonBox, nil, nil, scrollContainer) content := container.NewBorder(nil, buttonBox, nil, nil, scrollContainer)
s.wRoutes.SetContent(content) s.wNetworks.SetContent(content)
s.wRoutes.Show() s.wNetworks.Show()
s.startAutoRefresh(10*time.Second, tabs, allGrid, overlappingGrid, exitNodeGrid) s.startAutoRefresh(10*time.Second, tabs, allGrid, overlappingGrid, exitNodeGrid)
} }
@ -148,7 +149,7 @@ func (s *serviceClient) updateNetworks(grid *fyne.Container, f filter) {
grid.Add(resolvedIPsSelector) grid.Add(resolvedIPsSelector)
} }
s.wRoutes.Content().Refresh() s.wNetworks.Content().Refresh()
grid.Refresh() grid.Refresh()
} }
@ -305,7 +306,7 @@ func (s *serviceClient) getNetworksRequest(f filter, appendRoute bool) *proto.Se
func (s *serviceClient) showError(err error) { func (s *serviceClient) showError(err error) {
wrappedMessage := wrapText(err.Error(), 50) wrappedMessage := wrapText(err.Error(), 50)
dialog.ShowError(fmt.Errorf("%s", wrappedMessage), s.wRoutes) dialog.ShowError(fmt.Errorf("%s", wrappedMessage), s.wNetworks)
} }
func (s *serviceClient) startAutoRefresh(interval time.Duration, tabs *container.AppTabs, allGrid, overlappingGrid, exitNodesGrid *fyne.Container) { func (s *serviceClient) startAutoRefresh(interval time.Duration, tabs *container.AppTabs, allGrid, overlappingGrid, exitNodesGrid *fyne.Container) {
@ -316,14 +317,15 @@ func (s *serviceClient) startAutoRefresh(interval time.Duration, tabs *container
} }
}() }()
s.wRoutes.SetOnClosed(func() { s.wNetworks.SetOnClosed(func() {
ticker.Stop() ticker.Stop()
s.cancel()
}) })
} }
func (s *serviceClient) updateNetworksBasedOnDisplayTab(tabs *container.AppTabs, allGrid, overlappingGrid, exitNodesGrid *fyne.Container) { func (s *serviceClient) updateNetworksBasedOnDisplayTab(tabs *container.AppTabs, allGrid, overlappingGrid, exitNodesGrid *fyne.Container) {
grid, f := getGridAndFilterFromTab(tabs, allGrid, overlappingGrid, exitNodesGrid) grid, f := getGridAndFilterFromTab(tabs, allGrid, overlappingGrid, exitNodesGrid)
s.wRoutes.Content().Refresh() s.wNetworks.Content().Refresh()
s.updateNetworks(grid, f) s.updateNetworks(grid, f)
} }
@ -373,7 +375,7 @@ func (s *serviceClient) recreateExitNodeMenu(exitNodes []*proto.Network) {
node.Selected, node.Selected,
) )
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(s.ctx)
s.mExitNodeItems = append(s.mExitNodeItems, menuHandler{ s.mExitNodeItems = append(s.mExitNodeItems, menuHandler{
MenuItem: menuItem, MenuItem: menuItem,
cancel: cancel, cancel: cancel,