* Initial scaffolding * Applies MDM override * Unit tests * Helpers business logic * Return error if trying to modify any config that is gated by MDM * Add ManagedFields to returned config over GetConfig * Adds initial 101 MDM policy business logic testing * gRPC MDM changes * MDM Name scoping for clarity * Implements windows loading of MDM policy * Adds missing WGPort config * Cleanup setupKey to align to linear * Align split tunnel code * Adds some log * Prefix every log with MDM * Adds debug config cobra command This can be useful for troubleshooting and checking config now that its resolution is not trivial defaults > config > env cars > CLI/UI > MDM * Adds MDM 1m diff checker & reloader * Adds also up/start after cancel * Publishes event for UI to sync upon MDM changes * Add events to resync UI to actual config This also provide fixup for UI no aligning to changed config when coming from cli up with config flags. * UI behavior conflicts relaxation UI sends full config snapshot with all values. It doesn't make sense to block it if the values are aligned with the values constrained by the MDM policy. It's just simplier to allow values that are compliant. (this goes for the CLI as well at this point) * Lock toggle Settngs * Advanced Settings locking * Fixup presharedkey * Apply MDM locks * Toggle gray in/out for Advanced Settings * Adds support for disabling of Profiles and UpdateSettings feature flags * Adds Gate Login as well when --disable-update-settings=true is given to service This commit tries to settle things with an old PR-4237 which had relaxed the case where the SetConfig returned an `Unavailable` code error. Under this circumnstance the PR allowed the upFunc to just emit a warning and progress further with the login gRPC. Since the login call is consuming the --management-url coming from the `up` command, it might be possible to abuse the "Unavailable" code to inject a management URL that is different from the configured one even though the --disable-update-settings is set to true (?) * Evaluate disable-update-settings errors only when there's an actual override * [UI] Fixup advanced Settings * [UI] Fixup for preshared key * [UI] Fixup for profile enable/disable toggle We need to align the initial state to evaluate the delta in case. The initial state has to be "true" since the profile starts visible. Then we receive MDM and transition the cache bool value to the actual MDM imposed state * Enforces disable networks * [UI] Aligns to "enable/disable once on change only" * Fixup: MDM wins. always * Removes --disable-advanced-settings It was a typo in our meetings. the actual thing is --disable-update-settings * [PROTO] Removes --disable-advanced-settings * [UI] Removes --disable-advanced-settings * Pins feat profile retrieval to notif event * [UI] Fix for "hide" not working when propagating to parent with children * Adds dep for reading plist files * Introduces support for darwing plist loading * Tests MDM config reload via ticker * [PROVISIONING] ADMX/ADML/PS/bash scripts/templates * CI fixes - Add docstrings to `mdm_integration` - refactor for cognitive complexity - mod tidy * Linting * Add docstrings to `mdm_integration` * nil,nil is no policy and no error. Allow it * nil,nil is no policy and no error. Allow it * exclude MDM profile adminstrated keys data from debug bundle * Fixes Rosenpass left disable after MDM unlock * Partial revert coderabbit added docstrings * Renaming fix * Avoid locking on clientRunning bool when the connection is aborted for whatever reason We want to just signal this through the giveUpChan, we will manage the signal from the waiter side and in case set it to false there. THis way we avoid locking, which should allow the MDM down+wait_for_term_chan_signal_+up procedure clientRunning is used to signal two different conditions here: 1. the initialization procedure is over (we have an engine) 2. the connection being up (or being attempted) Probably these two functionalities should not alias, and the failure of the second condition (because of any error) should just drive a reconnection (currently it's not happening, and we silently go idle). OR, mor probably, the two things are the SAME and there should not exist a case where we did the "Up" initialization and connection attempt but we are not still attempting it. * Moves test helper at te very bottom * Addresses github comments * No lock no copy * Prevents engine not stopping within 10 secs from being paired by another instance We instead juts SKIP updating the policy, so 1. the MDM ticker will kick in 1 minute time, 2. find the policy misaligned, 3. enter the onMDMPolicyChange, 4. find the s.clientRunning == true (because it is set to false only in server cleanupConnection, and not by s.actCancel()) 5. call s.actCancel() again if not nil 6. immediately return from <-s.clientGiveUpChan 7. finally call s.restartEngineForMDMLocked() * Since we ARE running there should be a config If the config was cancelled midflight, connect will abort later on * DisableAutoConnect should not stop a running connection. DisableAutoConnect should just avoid the connection attempts *when the service starts*. If we are started and we are up and running, DisableAutoConnect should not kick in. Another PR will follow about this topic * Removes unused vars * Moves callback into Run method arg * align comment to removal of DisableAutoConnect DisableAutoConnect should just avoid the connection attempts *when the service starts*. If we are started and we are up and running, DisableAutoConnect should not kick in * Removes unused managed_fields data. This was initially used to drive the UI but approach changed to reload config/features upon notifications which makes this data redundant. * Reorder stuff * Unexport unrequired vars/functions PoliciesEqual → policiesEqual AllKeys → allKeys * Adds list of MDM managed fields in the debug bundle
752 lines
18 KiB
Go
752 lines
18 KiB
Go
//go:build !(linux && 386)
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os/user"
|
|
"slices"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
"fyne.io/fyne/v2"
|
|
"fyne.io/fyne/v2/container"
|
|
"fyne.io/fyne/v2/dialog"
|
|
"fyne.io/fyne/v2/layout"
|
|
"fyne.io/fyne/v2/widget"
|
|
"fyne.io/systray"
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/netbirdio/netbird/client/internal"
|
|
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
|
"github.com/netbirdio/netbird/client/proto"
|
|
)
|
|
|
|
// showProfilesUI creates and displays the Profiles window with a list of existing profiles,
|
|
// a button to add new profiles, allows removal, and lets the user switch the active profile.
|
|
func (s *serviceClient) showProfilesUI() {
|
|
|
|
profiles, err := s.getProfiles()
|
|
if err != nil {
|
|
log.Errorf("get profiles: %v", err)
|
|
return
|
|
}
|
|
|
|
var refresh func()
|
|
// List widget for profiles
|
|
list := widget.NewList(
|
|
func() int { return len(profiles) },
|
|
func() fyne.CanvasObject {
|
|
// Each item: Selected indicator, Name, spacer, Select, Logout & Remove buttons
|
|
return container.NewHBox(
|
|
widget.NewLabel(""), // indicator
|
|
widget.NewLabel(""), // profile name
|
|
layout.NewSpacer(),
|
|
widget.NewButton("Select", nil),
|
|
widget.NewButton("Deregister", nil),
|
|
widget.NewButton("Remove", nil),
|
|
)
|
|
},
|
|
func(i widget.ListItemID, item fyne.CanvasObject) {
|
|
// Populate each row
|
|
row := item.(*fyne.Container)
|
|
indicator := row.Objects[0].(*widget.Label)
|
|
nameLabel := row.Objects[1].(*widget.Label)
|
|
selectBtn := row.Objects[3].(*widget.Button)
|
|
logoutBtn := row.Objects[4].(*widget.Button)
|
|
removeBtn := row.Objects[5].(*widget.Button)
|
|
|
|
profile := profiles[i]
|
|
// Show a checkmark if selected
|
|
if profile.IsActive {
|
|
indicator.SetText("✓")
|
|
} else {
|
|
indicator.SetText("")
|
|
}
|
|
nameLabel.SetText(profile.Name)
|
|
|
|
// Configure Select/Active button
|
|
selectBtn.SetText(func() string {
|
|
if profile.IsActive {
|
|
return "Active"
|
|
}
|
|
return "Select"
|
|
}())
|
|
selectBtn.OnTapped = func() {
|
|
if profile.IsActive {
|
|
return // already active
|
|
}
|
|
// confirm switch
|
|
dialog.ShowConfirm(
|
|
"Switch Profile",
|
|
fmt.Sprintf("Are you sure you want to switch to '%s'?", profile.Name),
|
|
func(confirm bool) {
|
|
if !confirm {
|
|
return
|
|
}
|
|
// switch
|
|
err = s.switchProfile(profile.Name)
|
|
if err != nil {
|
|
log.Errorf("failed to switch profile: %v", err)
|
|
dialog.ShowError(errors.New("failed to select profile"), s.wProfiles)
|
|
return
|
|
}
|
|
|
|
dialog.ShowInformation(
|
|
"Profile Switched",
|
|
fmt.Sprintf("Profile '%s' switched successfully", profile.Name),
|
|
s.wProfiles,
|
|
)
|
|
|
|
conn, err := s.getSrvClient(defaultFailTimeout)
|
|
if err != nil {
|
|
log.Errorf("failed to get daemon client: %v", err)
|
|
return
|
|
}
|
|
|
|
status, err := conn.Status(s.ctx, &proto.StatusRequest{})
|
|
if err != nil {
|
|
log.Errorf("failed to get status after switching profile: %v", err)
|
|
return
|
|
}
|
|
|
|
if status.Status == string(internal.StatusConnected) {
|
|
if err := s.menuDownClick(); err != nil {
|
|
log.Errorf("failed to handle down click after switching profile: %v", err)
|
|
dialog.ShowError(fmt.Errorf("failed to handle down click"), s.wProfiles)
|
|
return
|
|
}
|
|
}
|
|
// update slice flags
|
|
refresh()
|
|
},
|
|
s.wProfiles,
|
|
)
|
|
}
|
|
|
|
logoutBtn.Show()
|
|
logoutBtn.SetText("Deregister")
|
|
logoutBtn.OnTapped = func() {
|
|
s.handleProfileLogout(profile.Name, refresh)
|
|
}
|
|
|
|
// Remove profile
|
|
removeBtn.SetText("Remove")
|
|
removeBtn.OnTapped = func() {
|
|
dialog.ShowConfirm(
|
|
"Delete Profile",
|
|
fmt.Sprintf("Are you sure you want to delete '%s'?", profile.Name),
|
|
func(confirm bool) {
|
|
if !confirm {
|
|
return
|
|
}
|
|
|
|
err = s.removeProfile(profile.Name)
|
|
if err != nil {
|
|
log.Errorf("failed to remove profile: %v", err)
|
|
dialog.ShowError(fmt.Errorf("failed to remove profile"), s.wProfiles)
|
|
return
|
|
}
|
|
dialog.ShowInformation(
|
|
"Profile Removed",
|
|
fmt.Sprintf("Profile '%s' removed successfully", profile.Name),
|
|
s.wProfiles,
|
|
)
|
|
// update slice
|
|
refresh()
|
|
},
|
|
s.wProfiles,
|
|
)
|
|
}
|
|
},
|
|
)
|
|
|
|
refresh = func() {
|
|
newProfiles, err := s.getProfiles()
|
|
if err != nil {
|
|
dialog.ShowError(err, s.wProfiles)
|
|
return
|
|
}
|
|
profiles = newProfiles // update the slice
|
|
list.Refresh() // tell Fyne to re-call length/update on every visible row
|
|
}
|
|
|
|
// Button to add a new profile
|
|
newBtn := widget.NewButton("New Profile", func() {
|
|
nameEntry := widget.NewEntry()
|
|
nameEntry.SetPlaceHolder("Enter Profile Name")
|
|
|
|
formItems := []*widget.FormItem{{Text: "Name:", Widget: nameEntry}}
|
|
dlg := dialog.NewForm(
|
|
"New Profile",
|
|
"Create",
|
|
"Cancel",
|
|
formItems,
|
|
func(confirm bool) {
|
|
if !confirm {
|
|
return
|
|
}
|
|
name := nameEntry.Text
|
|
if name == "" {
|
|
dialog.ShowError(errors.New("profile name cannot be empty"), s.wProfiles)
|
|
return
|
|
}
|
|
|
|
// add profile
|
|
err = s.addProfile(name)
|
|
if err != nil {
|
|
log.Errorf("failed to create profile: %v", err)
|
|
dialog.ShowError(fmt.Errorf("failed to create profile"), s.wProfiles)
|
|
return
|
|
}
|
|
dialog.ShowInformation(
|
|
"Profile Created",
|
|
fmt.Sprintf("Profile '%s' created successfully", name),
|
|
s.wProfiles,
|
|
)
|
|
// update slice
|
|
refresh()
|
|
},
|
|
s.wProfiles,
|
|
)
|
|
// make dialog wider
|
|
dlg.Resize(fyne.NewSize(350, 150))
|
|
dlg.Show()
|
|
})
|
|
|
|
// Assemble window content
|
|
content := container.NewBorder(nil, newBtn, nil, nil, list)
|
|
s.wProfiles = s.app.NewWindow("NetBird Profiles")
|
|
s.wProfiles.SetContent(content)
|
|
s.wProfiles.Resize(fyne.NewSize(400, 300))
|
|
s.wProfiles.SetOnClosed(s.cancel)
|
|
|
|
s.wProfiles.Show()
|
|
}
|
|
|
|
func (s *serviceClient) addProfile(profileName string) error {
|
|
conn, err := s.getSrvClient(defaultFailTimeout)
|
|
if err != nil {
|
|
return fmt.Errorf(getClientFMT, err)
|
|
}
|
|
|
|
currUser, err := user.Current()
|
|
if err != nil {
|
|
return fmt.Errorf("get current user: %w", err)
|
|
}
|
|
|
|
_, err = conn.AddProfile(s.ctx, &proto.AddProfileRequest{
|
|
ProfileName: profileName,
|
|
Username: currUser.Username,
|
|
})
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("add profile: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *serviceClient) switchProfile(profileName string) error {
|
|
conn, err := s.getSrvClient(defaultFailTimeout)
|
|
if err != nil {
|
|
return fmt.Errorf(getClientFMT, err)
|
|
}
|
|
|
|
currUser, err := user.Current()
|
|
if err != nil {
|
|
return fmt.Errorf("get current user: %w", err)
|
|
}
|
|
|
|
if _, err := conn.SwitchProfile(s.ctx, &proto.SwitchProfileRequest{
|
|
ProfileName: &profileName,
|
|
Username: &currUser.Username,
|
|
}); err != nil {
|
|
return fmt.Errorf("switch profile failed: %w", err)
|
|
}
|
|
|
|
err = s.profileManager.SwitchProfile(profileName)
|
|
if err != nil {
|
|
return fmt.Errorf("switch profile: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *serviceClient) removeProfile(profileName string) error {
|
|
conn, err := s.getSrvClient(defaultFailTimeout)
|
|
if err != nil {
|
|
return fmt.Errorf(getClientFMT, err)
|
|
}
|
|
|
|
currUser, err := user.Current()
|
|
if err != nil {
|
|
return fmt.Errorf("get current user: %w", err)
|
|
}
|
|
|
|
_, err = conn.RemoveProfile(s.ctx, &proto.RemoveProfileRequest{
|
|
ProfileName: profileName,
|
|
Username: currUser.Username,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("remove profile: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type Profile struct {
|
|
Name string
|
|
IsActive bool
|
|
}
|
|
|
|
func (s *serviceClient) getProfiles() ([]Profile, error) {
|
|
conn, err := s.getSrvClient(defaultFailTimeout)
|
|
if err != nil {
|
|
return nil, fmt.Errorf(getClientFMT, err)
|
|
}
|
|
|
|
currUser, err := user.Current()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get current user: %w", err)
|
|
}
|
|
profilesResp, err := conn.ListProfiles(s.ctx, &proto.ListProfilesRequest{
|
|
Username: currUser.Username,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list profiles: %w", err)
|
|
}
|
|
|
|
var profiles []Profile
|
|
|
|
for _, profile := range profilesResp.Profiles {
|
|
profiles = append(profiles, Profile{
|
|
Name: profile.Name,
|
|
IsActive: profile.IsActive,
|
|
})
|
|
}
|
|
|
|
return profiles, nil
|
|
}
|
|
|
|
func (s *serviceClient) handleProfileLogout(profileName string, refreshCallback func()) {
|
|
dialog.ShowConfirm(
|
|
"Deregister",
|
|
fmt.Sprintf("Are you sure you want to deregister from '%s'?", profileName),
|
|
func(confirm bool) {
|
|
if !confirm {
|
|
return
|
|
}
|
|
|
|
conn, err := s.getSrvClient(defaultFailTimeout)
|
|
if err != nil {
|
|
log.Errorf("failed to get service client: %v", err)
|
|
dialog.ShowError(fmt.Errorf("failed to connect to service"), s.wProfiles)
|
|
return
|
|
}
|
|
|
|
currUser, err := user.Current()
|
|
if err != nil {
|
|
log.Errorf("failed to get current user: %v", err)
|
|
dialog.ShowError(fmt.Errorf("failed to get current user"), s.wProfiles)
|
|
return
|
|
}
|
|
|
|
username := currUser.Username
|
|
_, err = conn.Logout(s.ctx, &proto.LogoutRequest{
|
|
ProfileName: &profileName,
|
|
Username: &username,
|
|
})
|
|
if err != nil {
|
|
log.Errorf("logout failed: %v", err)
|
|
dialog.ShowError(fmt.Errorf("deregister failed"), s.wProfiles)
|
|
return
|
|
}
|
|
|
|
dialog.ShowInformation(
|
|
"Deregistered",
|
|
fmt.Sprintf("Successfully deregistered from '%s'", profileName),
|
|
s.wProfiles,
|
|
)
|
|
|
|
refreshCallback()
|
|
},
|
|
s.wProfiles,
|
|
)
|
|
}
|
|
|
|
type subItem struct {
|
|
*systray.MenuItem
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
type profileMenu struct {
|
|
mu sync.Mutex
|
|
ctx context.Context
|
|
serviceClient *serviceClient
|
|
profileManager *profilemanager.ProfileManager
|
|
eventHandler *eventHandler
|
|
profileMenuItem *systray.MenuItem
|
|
emailMenuItem *systray.MenuItem
|
|
profileSubItems []*subItem
|
|
manageProfilesSubItem *subItem
|
|
logoutSubItem *subItem
|
|
profilesState []Profile
|
|
downClickCallback func() error
|
|
upClickCallback func(context.Context) error
|
|
getSrvClientCallback func(timeout time.Duration) (proto.DaemonServiceClient, error)
|
|
loadSettingsCallback func()
|
|
app fyne.App
|
|
}
|
|
|
|
type newProfileMenuArgs struct {
|
|
ctx context.Context
|
|
serviceClient *serviceClient
|
|
profileManager *profilemanager.ProfileManager
|
|
eventHandler *eventHandler
|
|
profileMenuItem *systray.MenuItem
|
|
emailMenuItem *systray.MenuItem
|
|
downClickCallback func() error
|
|
upClickCallback func(context.Context) error
|
|
getSrvClientCallback func(timeout time.Duration) (proto.DaemonServiceClient, error)
|
|
loadSettingsCallback func()
|
|
app fyne.App
|
|
}
|
|
|
|
func newProfileMenu(args newProfileMenuArgs) *profileMenu {
|
|
p := profileMenu{
|
|
ctx: args.ctx,
|
|
serviceClient: args.serviceClient,
|
|
profileManager: args.profileManager,
|
|
eventHandler: args.eventHandler,
|
|
profileMenuItem: args.profileMenuItem,
|
|
emailMenuItem: args.emailMenuItem,
|
|
downClickCallback: args.downClickCallback,
|
|
upClickCallback: args.upClickCallback,
|
|
getSrvClientCallback: args.getSrvClientCallback,
|
|
loadSettingsCallback: args.loadSettingsCallback,
|
|
app: args.app,
|
|
}
|
|
|
|
p.emailMenuItem.Disable()
|
|
p.emailMenuItem.Hide()
|
|
p.refresh()
|
|
go p.updateMenu()
|
|
|
|
return &p
|
|
}
|
|
|
|
func (p *profileMenu) getProfiles() ([]Profile, error) {
|
|
conn, err := p.getSrvClientCallback(defaultFailTimeout)
|
|
if err != nil {
|
|
return nil, fmt.Errorf(getClientFMT, err)
|
|
}
|
|
currUser, err := user.Current()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get current user: %w", err)
|
|
}
|
|
|
|
profilesResp, err := conn.ListProfiles(p.ctx, &proto.ListProfilesRequest{
|
|
Username: currUser.Username,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list profiles: %w", err)
|
|
}
|
|
|
|
var profiles []Profile
|
|
|
|
for _, profile := range profilesResp.Profiles {
|
|
profiles = append(profiles, Profile{
|
|
Name: profile.Name,
|
|
IsActive: profile.IsActive,
|
|
})
|
|
}
|
|
|
|
return profiles, nil
|
|
}
|
|
|
|
func (p *profileMenu) refresh() {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
|
|
profiles, err := p.getProfiles()
|
|
if err != nil {
|
|
log.Errorf("failed to list profiles: %v", err)
|
|
return
|
|
}
|
|
|
|
// Clear existing profile items
|
|
p.clear(profiles)
|
|
|
|
currUser, err := user.Current()
|
|
if err != nil {
|
|
log.Errorf("failed to get current user: %v", err)
|
|
return
|
|
}
|
|
|
|
conn, err := p.getSrvClientCallback(defaultFailTimeout)
|
|
if err != nil {
|
|
log.Errorf("failed to get daemon client: %v", err)
|
|
return
|
|
}
|
|
|
|
activeProf, err := conn.GetActiveProfile(p.ctx, &proto.GetActiveProfileRequest{})
|
|
if err != nil {
|
|
log.Errorf("failed to get active profile: %v", err)
|
|
return
|
|
}
|
|
|
|
if activeProf.ProfileName == "default" || activeProf.Username == currUser.Username {
|
|
activeProfState, err := p.profileManager.GetProfileState(activeProf.ProfileName)
|
|
if err != nil {
|
|
log.Warnf("failed to get active profile state: %v", err)
|
|
p.emailMenuItem.Hide()
|
|
} else if activeProfState.Email != "" {
|
|
p.emailMenuItem.SetTitle(fmt.Sprintf("(%s)", activeProfState.Email))
|
|
p.emailMenuItem.Show()
|
|
}
|
|
}
|
|
|
|
for _, profile := range profiles {
|
|
item := p.profileMenuItem.AddSubMenuItem(profile.Name, "")
|
|
if profile.IsActive {
|
|
item.Check()
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
p.profileSubItems = append(p.profileSubItems, &subItem{item, ctx, cancel})
|
|
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return // context cancelled
|
|
case _, ok := <-item.ClickedCh:
|
|
if !ok {
|
|
return // channel closed
|
|
}
|
|
|
|
// Handle profile selection
|
|
if profile.IsActive {
|
|
log.Infof("Profile '%s' is already active", profile.Name)
|
|
return
|
|
}
|
|
conn, err := p.getSrvClientCallback(defaultFailTimeout)
|
|
if err != nil {
|
|
log.Errorf("failed to get daemon client: %v", err)
|
|
return
|
|
}
|
|
|
|
_, err = conn.SwitchProfile(ctx, &proto.SwitchProfileRequest{
|
|
ProfileName: &profile.Name,
|
|
Username: &currUser.Username,
|
|
})
|
|
if err != nil {
|
|
log.Errorf("failed to switch profile: %v", err)
|
|
// show notification dialog
|
|
p.serviceClient.notifier.Send("Error", "Failed to switch profile")
|
|
return
|
|
}
|
|
|
|
err = p.profileManager.SwitchProfile(profile.Name)
|
|
if err != nil {
|
|
log.Errorf("failed to switch profile '%s': %v", profile.Name, err)
|
|
return
|
|
}
|
|
|
|
log.Infof("Switched to profile '%s'", profile.Name)
|
|
|
|
status, err := conn.Status(ctx, &proto.StatusRequest{})
|
|
if err != nil {
|
|
log.Errorf("failed to get status after switching profile: %v", err)
|
|
return
|
|
}
|
|
|
|
if status.Status == string(internal.StatusConnected) {
|
|
if err := p.downClickCallback(); err != nil {
|
|
log.Errorf("failed to handle down click after switching profile: %v", err)
|
|
}
|
|
}
|
|
|
|
if p.serviceClient.connectCancel != nil {
|
|
p.serviceClient.connectCancel()
|
|
}
|
|
|
|
connectCtx, connectCancel := context.WithCancel(p.ctx)
|
|
p.serviceClient.connectCancel = connectCancel
|
|
|
|
if err := p.upClickCallback(connectCtx); err != nil {
|
|
log.Errorf("failed to handle up click after switching profile: %v", err)
|
|
}
|
|
|
|
connectCancel()
|
|
|
|
p.refresh()
|
|
p.loadSettingsCallback()
|
|
}
|
|
}
|
|
}()
|
|
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
manageItem := p.profileMenuItem.AddSubMenuItem("Manage Profiles", "")
|
|
p.manageProfilesSubItem = &subItem{manageItem, ctx, cancel}
|
|
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case _, ok := <-manageItem.ClickedCh:
|
|
if !ok {
|
|
return
|
|
}
|
|
p.eventHandler.runSelfCommand(p.ctx, "profiles", "true")
|
|
p.refresh()
|
|
p.loadSettingsCallback()
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Add Logout menu item
|
|
ctx2, cancel2 := context.WithCancel(context.Background())
|
|
logoutItem := p.profileMenuItem.AddSubMenuItem("Deregister", "")
|
|
p.logoutSubItem = &subItem{logoutItem, ctx2, cancel2}
|
|
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-ctx2.Done():
|
|
return
|
|
case _, ok := <-logoutItem.ClickedCh:
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := p.eventHandler.logout(p.ctx); err != nil {
|
|
log.Errorf("logout failed: %v", err)
|
|
p.serviceClient.notifier.Send("Error", "Failed to deregister")
|
|
} else {
|
|
p.serviceClient.notifier.Send("Success", "Deregistered successfully")
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
if activeProf.ProfileName == "default" || activeProf.Username == currUser.Username {
|
|
p.profileMenuItem.SetTitle(activeProf.ProfileName)
|
|
} else {
|
|
p.profileMenuItem.SetTitle(fmt.Sprintf("Profile: %s (User: %s)", activeProf.ProfileName, activeProf.Username))
|
|
p.emailMenuItem.Hide()
|
|
}
|
|
|
|
}
|
|
|
|
func (p *profileMenu) clear(profiles []Profile) {
|
|
for _, item := range p.profileSubItems {
|
|
item.Remove()
|
|
item.cancel()
|
|
}
|
|
p.profileSubItems = make([]*subItem, 0, len(profiles))
|
|
p.profilesState = profiles
|
|
|
|
if p.manageProfilesSubItem != nil {
|
|
p.manageProfilesSubItem.Remove()
|
|
p.manageProfilesSubItem.cancel()
|
|
p.manageProfilesSubItem = nil
|
|
}
|
|
|
|
if p.logoutSubItem != nil {
|
|
p.logoutSubItem.Remove()
|
|
p.logoutSubItem.cancel()
|
|
p.logoutSubItem = nil
|
|
}
|
|
}
|
|
|
|
// setEnabled greys out (Disable) the profile menu and every existing
|
|
// sub-item when the daemon reports the kill switch active, so the user
|
|
// sees the menu but cannot enter "Manage Profiles" or switch profile.
|
|
// Previously this used Hide() on the parent, but Fyne's systray on
|
|
// Windows does not propagate Hide() to a parent that already has
|
|
// children — the submenu kept popping up and accepting clicks. Disable
|
|
// is the reliable visual lock.
|
|
func (p *profileMenu) setEnabled(enabled bool) {
|
|
if p.profileMenuItem == nil {
|
|
return
|
|
}
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
|
|
if enabled {
|
|
p.profileMenuItem.Enable()
|
|
p.profileMenuItem.SetTooltip("")
|
|
} else {
|
|
p.profileMenuItem.Disable()
|
|
p.profileMenuItem.SetTooltip("Profiles are disabled by daemon")
|
|
}
|
|
|
|
apply := func(item *systray.MenuItem) {
|
|
if item == nil {
|
|
return
|
|
}
|
|
if enabled {
|
|
item.Enable()
|
|
} else {
|
|
item.Disable()
|
|
}
|
|
}
|
|
for _, sub := range p.profileSubItems {
|
|
if sub != nil {
|
|
apply(sub.MenuItem)
|
|
}
|
|
}
|
|
if p.manageProfilesSubItem != nil {
|
|
apply(p.manageProfilesSubItem.MenuItem)
|
|
}
|
|
if p.logoutSubItem != nil {
|
|
apply(p.logoutSubItem.MenuItem)
|
|
}
|
|
}
|
|
|
|
func (p *profileMenu) updateMenu() {
|
|
// check every second
|
|
ticker := time.NewTicker(time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
// get profilesList
|
|
profiles, err := p.getProfiles()
|
|
if err != nil {
|
|
log.Errorf("failed to list profiles: %v", err)
|
|
continue
|
|
}
|
|
|
|
sort.Slice(profiles, func(i, j int) bool {
|
|
return profiles[i].Name < profiles[j].Name
|
|
})
|
|
|
|
p.mu.Lock()
|
|
state := p.profilesState
|
|
p.mu.Unlock()
|
|
|
|
sort.Slice(state, func(i, j int) bool {
|
|
return state[i].Name < state[j].Name
|
|
})
|
|
|
|
if slices.Equal(profiles, state) {
|
|
continue
|
|
}
|
|
|
|
p.refresh()
|
|
case <-p.ctx.Done():
|
|
return // context cancelled
|
|
|
|
}
|
|
}
|
|
}
|