fleet/orbit/cmd/desktop/desktop.go
Juan Fernandez 39b0d5b507
Fixes bug where Debian system tray settings are not persisted after a restart (#40104)
Resolves #36526

Set static system tray title to avoid system tray settings not
persisting across process restarts on Debian.
2026-02-19 13:13:30 -04:00

777 lines
25 KiB
Go

package main
import (
"context"
_ "embed"
"errors"
"fmt"
"os"
"os/signal"
"path/filepath"
"runtime"
"strings"
"sync/atomic"
"syscall"
"time"
"fyne.io/systray"
"github.com/fleetdm/fleet/v4/orbit/cmd/desktop/menu"
"github.com/fleetdm/fleet/v4/orbit/pkg/constant"
"github.com/fleetdm/fleet/v4/orbit/pkg/go-paniclog"
"github.com/fleetdm/fleet/v4/orbit/pkg/migration"
"github.com/fleetdm/fleet/v4/orbit/pkg/profiles"
"github.com/fleetdm/fleet/v4/orbit/pkg/token"
"github.com/fleetdm/fleet/v4/orbit/pkg/update"
"github.com/fleetdm/fleet/v4/orbit/pkg/useraction"
"github.com/fleetdm/fleet/v4/pkg/certificate"
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
"github.com/fleetdm/fleet/v4/pkg/open"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/service"
"github.com/gofrs/flock"
"github.com/oklog/run"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"gopkg.in/natefinch/lumberjack.v2"
)
// version is set at compile time via -ldflags
var version = "unknown"
func setupRunners() {
var runnerGroup run.Group
// Setting up a watcher for the communication channel
if runtime.GOOS == "windows" {
runnerGroup.Add(
func() error {
// block wait on the communication channel
if err := blockWaitForStopEvent(constant.DesktopAppExecName); err != nil {
log.Error().Err(err).Msg("There was an error on the desktop communication channel")
return err
}
log.Info().Msg("Shutdown was requested!")
return nil
},
func(err error) {
systray.Quit()
},
)
}
if err := runnerGroup.Run(); err != nil {
log.Error().Err(err).Msg("Fleet Desktop runners terminated")
return
}
}
func main() {
// FIXME: we need to do a better job of graceful shutdown, releasing resources, stopping
// tickers, etc. (https://github.com/fleetdm/fleet/issues/21256)
// This context will be used as a general context to handle graceful shutdown in the future.
offlineWatcherCtx, cancelOfflineWatcherCtx := context.WithCancel(context.Background())
// Orbits uses --version to get the fleet-desktop version. Logs do not need to be set up when running this.
if len(os.Args) > 1 && os.Args[1] == "--version" {
// Must work with update.GetVersion
fmt.Println("fleet-desktop", version)
return
}
setupLogs()
setupStderr()
// Our TUF provided targets must support launching with "--help".
if len(os.Args) > 1 && os.Args[1] == "--help" {
fmt.Println("Fleet Desktop application executable")
return
}
log.Info().Msgf("fleet-desktop version=%s", version)
if permanentError := os.Getenv("FLEET_DESKTOP_PERMANENT_ERROR"); permanentError != "" {
runWithPermanentError(permanentError)
}
identifierPath := os.Getenv("FLEET_DESKTOP_DEVICE_IDENTIFIER_PATH")
if identifierPath == "" {
log.Fatal().Msg("missing URL environment FLEET_DESKTOP_DEVICE_IDENTIFIER_PATH")
}
fleetURL := os.Getenv("FLEET_DESKTOP_FLEET_URL")
if fleetURL == "" {
log.Fatal().Msg("missing URL environment FLEET_DESKTOP_FLEET_URL")
}
fleetTLSClientCertificate := os.Getenv("FLEET_DESKTOP_FLEET_TLS_CLIENT_CERTIFICATE")
fleetTLSClientKey := os.Getenv("FLEET_DESKTOP_FLEET_TLS_CLIENT_KEY")
fleetClientCrt, err := certificate.LoadClientCertificate(fleetTLSClientCertificate, fleetTLSClientKey)
if err != nil {
log.Fatal().Err(err).Msg("load fleet tls client certificate")
}
fleetAlternativeBrowserHost := os.Getenv("FLEET_DESKTOP_ALTERNATIVE_BROWSER_HOST")
if fleetClientCrt != nil {
log.Info().Msg("Using TLS client certificate and key to authenticate to the server.")
}
tufUpdateRoot := os.Getenv("FLEET_DESKTOP_TUF_UPDATE_ROOT")
if tufUpdateRoot != "" {
log.Info().Msgf("got a TUF update root: %s", tufUpdateRoot)
}
// We've only seen this bug appear on Linux under certain very
// specific conditions
if runtime.GOOS == "linux" {
// Ensure only one instance of Fleet Desktop is running at a time
lockFile, err := getLockfile()
if err != nil {
log.Fatal().Err(err).Msg("could not secure lock file")
}
defer func() {
if err := lockFile.Unlock(); err != nil {
log.Error().Err(err).Msg("unlocking lockfile")
}
}()
}
// Setting up working runners such as signalHandler runner
go setupRunners()
var mdmMigrator useraction.MDMMigrator
// swiftDialogCh is a channel shared by the migrator and the offline watcher to
// coordinate the display of the dialog and ensure only one dialog is shown at a time.
var swiftDialogCh chan struct{}
var offlineWatcher useraction.MDMOfflineWatcher
// We will execute the summary API every 5 minutes (to refresh policy state).
const desktopSummaryInterval = 5 * time.Minute
// This ticker is used for checking connectivity. It is initialized here because it is
// stopped in `OnExit.`
const pingInterval = 10 * time.Second // same value as default distributed/read
pingTicker := time.NewTicker(pingInterval)
// Used to trigger a policy check when clicking on "My device" or "About Fleet".
var fleetDesktopCheckTrigger atomic.Bool
// we have seen some cases where systray.Run() does not call onReady seemingly due to early
// initialization states with the GUI such as Windows Autopilot first time setup. This ensures
// we don't just hang forever waiting for the GUI to be ready.
trayAppDisplayed := make(chan struct{})
go func() {
select {
case <-trayAppDisplayed:
// The tray app is ready and displayed so there is nothing to do
case <-time.After(1 * time.Minute):
log.Fatal().Msg("onReady was never called - the GUI may not yet be ready")
}
}()
onReady := func() {
close(trayAppDisplayed)
log.Info().Msg("ready")
systray.SetTooltip("Fleet Desktop")
if runtime.GOOS == "linux" {
// Set a static title to ensure a consistent System Tray ID across
// process restarts. By default, fyne.io/systray generates a dynamic
// ID using the process PID if the title is not set, which prevents
// the OS from persisting user settings (like 'Always Hidden' or 'Pinned')
// between sessions on Debian.
systray.SetTitle("Fleet Desktop")
}
// Default to dark theme icon because this seems to be a better fit on Linux (Ubuntu at
// least). On macOS this is used as a template icon anyway.
systray.SetTemplateIcon(iconDark, iconDark)
// Initialize menu manager with systray factory
menuManager := menu.NewManager(version, menu.NewSystrayFactory())
tokenReader := token.Reader{Path: identifierPath}
if _, err := tokenReader.Read(); err != nil {
log.Fatal().Err(err).Msg("error reading device token from file")
}
var insecureSkipVerify bool
if os.Getenv("FLEET_DESKTOP_INSECURE") != "" {
insecureSkipVerify = true
}
rootCA := os.Getenv("FLEET_DESKTOP_FLEET_ROOT_CA")
client, err := service.NewDeviceClient(
fleetURL,
insecureSkipVerify,
rootCA,
fleetClientCrt,
fleetAlternativeBrowserHost,
)
if err != nil {
log.Fatal().Err(err).Msg("unable to initialize request client")
}
client.WithInvalidTokenRetry(func() string {
log.Debug().Msg("refetching token from disk for API retry")
newToken, err := tokenReader.Read()
if err != nil {
log.Error().Err(err).Msg("refetch token from disk for API retry")
return ""
}
log.Debug().Msg("successfully refetched the token from disk for API retry")
return newToken
})
reportError := func(err error, info map[string]any) {
if !client.GetServerCapabilities().Has(fleet.CapabilityErrorReporting) {
log.Info().Msg("skipped reporting error to the server as it doesn't have the capability enabled")
return
}
fleetdErr := fleet.FleetdError{
ErrorSource: "fleet-desktop",
ErrorSourceVersion: version,
ErrorTimestamp: time.Now(),
ErrorMessage: err.Error(),
ErrorAdditionalInfo: info,
}
if err := client.ReportError(tokenReader.GetCached(), fleetdErr); err != nil {
log.Error().Err(err).EmbedObject(fleetdErr).Msg("reporting error to Fleet server")
}
}
if runtime.GOOS == "darwin" {
m, s, o, err := mdmMigrationSetup(offlineWatcherCtx, tufUpdateRoot, fleetURL, client, &tokenReader)
if err != nil {
go reportError(err, nil)
log.Error().Err(err).Msg("setting up MDM migration resources")
}
mdmMigrator = m
swiftDialogCh = s
offlineWatcher = o
}
refetchToken := func() {
if _, err := tokenReader.Read(); err != nil {
log.Error().Err(err).Msg("refetch token")
}
log.Debug().Msg("successfully refetched the token from disk")
}
// checkToken performs API test calls to enable the "My device" item as
// soon as the device auth token is registered by Fleet.
checkToken := func() <-chan interface{} {
menuManager.SetConnecting()
done := make(chan interface{})
go func() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
defer close(done)
for {
refetchToken()
summary, err := client.DesktopSummary(tokenReader.GetCached())
if err == nil || errors.Is(err, service.ErrMissingLicense) {
log.Debug().Msg("enabling tray items")
isFreeTier := errors.Is(err, service.ErrMissingLicense)
var desktopSummary *fleet.DesktopSummary
if summary != nil {
desktopSummary = &summary.DesktopSummary
}
menuManager.SetConnected(desktopSummary, isFreeTier)
return
}
log.Error().Err(err).Msg("get device URL")
<-ticker.C
}
}()
return done
}
// start a check as soon as the app starts
deviceEnabledChan := checkToken()
// this loop checks the `mtime` value of the token file and:
// 1. if the token file was modified, it disables the tray items until we
// verify the token is valid
// 2. calls (blocking) `checkToken` to verify the token is valid
go func() {
<-deviceEnabledChan
tic := time.NewTicker(1 * time.Second)
defer tic.Stop()
for {
<-tic.C
expired, err := tokenReader.HasChanged()
switch {
case err != nil:
log.Error().Err(err).Msg("check token file")
case expired:
log.Info().Msg("token file expired or invalid, rechecking")
<-checkToken()
}
}
}()
// poll the server to check the policy status of the host and update the
// tray icon accordingly.
// We first ping the server to check for connectivity, then get the policy status (every 5 minutes to
// not cause performance issues on the server).
go func() {
<-deviceEnabledChan
var (
pingErrCount = 0
lastDesktopSummaryCheck time.Time
)
for {
<-pingTicker.C
// Reset the ticker to the intended interval,
// in case we reset it to 1ms (when clicking on "My device").
pingTicker.Reset(pingInterval)
if err := client.Ping(); err != nil {
log.Error().Err(err).Int("count", pingErrCount).Msg("ping failed")
pingErrCount++
// We try 5 more times to make sure one bad request doesn't trigger the offline indicator.
// So it might take up to ~1m (6 * 10s) for Fleet Desktop to show the offline indicator.
if pingErrCount >= 6 {
menuManager.SetOffline()
}
continue
}
// Successfully connected to Fleet.
pingErrCount = 0
// Check if we need to fetch the "Fleet desktop" summary from Fleet.
if !menuManager.IsOfflineIndicatorDisplayed() &&
!fleetDesktopCheckTrigger.Load() &&
(!lastDesktopSummaryCheck.IsZero() && time.Since(lastDesktopSummaryCheck) < desktopSummaryInterval) {
continue
}
lastDesktopSummaryCheck = time.Now()
fleetDesktopCheckTrigger.Store(false)
// We set offlineIndicatorDisplayed to false because we do not want to retry the
// Fleet Desktop summary every 10s if Ping works but DesktopSummary doesn't
// (to avoid server load issues).
menuManager.SetOfflineIndicatorDisplayed(false)
sum, err := client.DesktopSummary(tokenReader.GetCached())
if err != nil {
switch {
case errors.Is(err, service.ErrMissingLicense):
// Policy reporting in Fleet Desktop requires a license,
// so we just show the "My device" item as usual.
menuManager.SetConnected(&fleet.DesktopSummary{}, true)
case errors.Is(err, service.ErrUnauthenticated):
log.Debug().Err(err).Msg("get desktop summary auth failure")
// This usually happens every ~1 hour when the token expires.
<-checkToken()
default:
log.Error().Err(err).Msg("get desktop summary failed")
}
continue
}
menuManager.SetConnected(&sum.DesktopSummary, false)
menuManager.UpdateFailingPolicies(sum.DesktopSummary.FailingPolicies)
// Check our file to see if we should migrate
var migrationType string
if runtime.GOOS == "darwin" {
migrationType, err = mdmMigrator.MigrationInProgress()
if err != nil {
go reportError(err, nil)
log.Error().Err(err).Msg("checking if MDM migration is in progress")
}
}
migrationInProgress := migrationType != ""
shouldRunMigrator := sum.Notifications.NeedsMDMMigration || sum.Notifications.RenewEnrollmentProfile || migrationInProgress
if runtime.GOOS == "darwin" && shouldRunMigrator && mdmMigrator.CanRun() {
enrolled, enrollURL, err := profiles.IsEnrolledInMDM()
if err != nil {
log.Error().Err(err).Msg("fetching enrollment status to show mdm migrator")
continue
}
// we perform this check locally on the client too to avoid showing the
// dialog if the client has already migrated but the Fleet server
// doesn't know about this state yet.
enrolledIntoFleet, err := fleethttp.HostnamesMatch(enrollURL, fleetURL)
if err != nil {
log.Error().Err(err).Msg("comparing MDM server URLs")
continue
}
if !enrolledIntoFleet {
// isUnmanaged captures two important bits of information:
//
// - The notification coming from the server, which is based on information that's
// not available in the client (eg: is MDM configured? are migrations enabled?
// is this device elegible for migration?)
// - The current enrollment status of the device.
isUnmanaged := sum.Notifications.RenewEnrollmentProfile && !enrolled
forceModeEnabled := sum.Notifications.NeedsMDMMigration &&
sum.Config.MDM.MacOSMigration.Mode == fleet.MacOSMigrationModeForced
// update org info in case it changed
mdmMigrator.SetProps(useraction.MDMMigratorProps{
OrgInfo: sum.Config.OrgInfo,
IsUnmanaged: isUnmanaged,
})
// enable tray items
if migrationType != constant.MDMMigrationTypeADE {
menuManager.SetMDMMigratorVisibility(true)
} else {
menuManager.SetMDMMigratorVisibility(false)
}
// if the device is unmanaged or we're in force mode and the device needs
// migration, enable aggressive mode.
if isUnmanaged || forceModeEnabled || migrationInProgress {
log.Info().Msg("MDM device is unmanaged or force mode enabled, automatically showing dialog")
if err := mdmMigrator.ShowInterval(); err != nil {
go reportError(err, nil)
log.Error().Err(err).Msg("showing MDM migration dialog at interval")
}
}
} else {
// we're done with the migration, so mark it as complete.
if err := mdmMigrator.MarkMigrationCompleted(); err != nil {
go reportError(err, nil)
log.Error().Err(err).Msg("failed to mark MDM migration as completed")
}
menuManager.SetMDMMigratorVisibility(false)
}
} else {
menuManager.SetMDMMigratorVisibility(false)
}
}
}()
go func() {
for {
select {
case <-menuManager.Items.MyDevice.ClickedCh():
openURL := client.BrowserPoliciesURL(tokenReader.GetCached())
if err := open.Browser(openURL); err != nil {
log.Error().Err(err).Str("url", openURL).Msg("open browser policies")
}
// Also refresh the device status by forcing the polling ticker to fire
fleetDesktopCheckTrigger.Store(true)
pingTicker.Reset(1 * time.Millisecond)
case <-menuManager.Items.Transparency.ClickedCh():
openURL := client.BrowserTransparencyURL(tokenReader.GetCached())
if err := open.Browser(openURL); err != nil {
log.Error().Err(err).Str("url", openURL).Msg("open browser transparency")
}
case <-menuManager.Items.SelfService.ClickedCh():
openURL := client.BrowserSelfServiceURL(tokenReader.GetCached())
if err := open.Browser(openURL); err != nil {
log.Error().Err(err).Str("url", openURL).Msg("open browser self-service")
}
// Also refresh the device status by forcing the polling ticker to fire
fleetDesktopCheckTrigger.Store(true)
pingTicker.Reset(1 * time.Millisecond)
case <-menuManager.Items.MigrateMDM.ClickedCh():
if offline := offlineWatcher.ShowIfOffline(offlineWatcherCtx); offline {
continue
}
if err := mdmMigrator.Show(); err != nil {
go reportError(err, nil)
log.Error().Err(err).Msg("showing MDM migration dialog on user action")
}
}
}
}()
}
// FIXME: it doesn't look like this is actually triggering, at least when desktop gets
// killed (https://github.com/fleetdm/fleet/issues/21256)
onExit := func() {
log.Info().Msg("exiting")
if mdmMigrator != nil {
log.Debug().Err(err).Msg("exiting mdmMigrator")
mdmMigrator.Exit()
}
if swiftDialogCh != nil {
log.Debug().Err(err).Msg("exiting swiftDialogCh")
close(swiftDialogCh)
}
log.Debug().Msg("stopping ping ticker")
pingTicker.Stop()
log.Debug().Msg("canceling offline watcher ctx")
cancelOfflineWatcherCtx()
}
sigChan := make(chan os.Signal, 1)
signal.Notify(
sigChan,
syscall.SIGINT,
syscall.SIGTERM,
syscall.SIGQUIT,
)
// Catch signals and exit gracefully
go func() {
s := <-sigChan
log.Info().Stringer("signal", s).Msg("Caught signal, exiting")
systray.Quit()
}()
// Check for the system tray icon periodically and kill the process if it's missing,
// forcing the parent to restart it. This may happen if a Linux display manager
// is restarted.
if runtime.GOOS == "linux" {
log.Debug().Msg("starting tray icon checker")
go func() {
checkTrayIconTicker := time.NewTicker(5 * time.Second)
for {
<-checkTrayIconTicker.C
if !trayIconExists() {
log.Warn().Msg("system tray icon missing, exiting")
// Cleanly stop systray.
systray.Quit()
// Exit to trigger restart.
os.Exit(75)
}
}
}()
}
systray.Run(onReady, onExit)
}
type mdmMigrationHandler struct {
client *service.DeviceClient
tokenReader *token.Reader
}
func (m *mdmMigrationHandler) NotifyRemote() error {
log.Info().Msg("sending request to trigger mdm migration webhook")
// TODO: Revisit if/when we should hide the migration menu item depending on the
// result of the client request.
if err := m.client.MigrateMDM(m.tokenReader.GetCached()); err != nil {
log.Error().Err(err).Msg("triggering migration webhook")
return fmt.Errorf("on migration start: %w", err)
}
log.Info().Msg("successfully sent request to trigger mdm migration webhook")
return nil
}
func (m *mdmMigrationHandler) ShowInstructions() error {
openURL := m.client.BrowserDeviceURL(m.tokenReader.GetCached())
if err := open.Browser(openURL); err != nil {
log.Error().Err(err).Str("url", openURL).Msg("open browser my device (mdm migration handler)")
return err
}
return nil
}
// getLockfile checks for the fleet desktop lock file, and returns an error if it can't secure it.
func getLockfile() (*flock.Flock, error) {
dir, err := logDir()
if err != nil {
return nil, fmt.Errorf("unable to get logdir for lock: %w", err)
}
// Same as the log dir in setupLogs()
dir = filepath.Join(dir, "Fleet")
lockFilePath := filepath.Join(dir, "fleet-desktop.lock")
log.Debug().Msgf("acquiring fleet desktop lockfile: %s", lockFilePath)
lock := flock.New(lockFilePath)
locked, err := lock.TryLock()
if err != nil {
return nil, fmt.Errorf("error getting lock on %s: %w", lockFilePath, err)
}
if !locked {
return nil, errors.New("another instance of fleet desktop has the lock")
}
log.Debug().Msgf("lock acquired on %s", lockFilePath)
return lock, nil
}
// setupLogs configures our logging system to write logs to rolling files, if for some
// reason we can't write a log file the logs are still printed to stderr.
func setupLogs() {
dir, err := logDir()
if err != nil {
stderrOut := zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339Nano, NoColor: true}
log.Logger = log.Output(stderrOut)
log.Error().Err(err).Msg("find directory for logs")
return
}
dir = filepath.Join(dir, "Fleet")
if err := os.MkdirAll(dir, 0o755); err != nil {
stderrOut := zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339Nano, NoColor: true}
log.Logger = log.Output(stderrOut)
log.Error().Err(err).Msg("make directories for log files")
return
}
logFile := &lumberjack.Logger{
Filename: filepath.Join(dir, "fleet-desktop.log"),
MaxSize: 25, // megabytes
MaxBackups: 3,
MaxAge: 28, // days
}
consoleWriter := zerolog.ConsoleWriter{Out: logFile, TimeFormat: time.RFC3339Nano, NoColor: true}
log.Logger = log.Output(consoleWriter)
}
// setupStderr redirects stderr output to a file.
func setupStderr() {
dir, err := logDir()
if err != nil {
log.Error().Err(err).Msg("find directory for stderr")
return
}
if err := os.MkdirAll(dir, 0o755); err != nil {
log.Error().Err(err).Msg("make directories for stderr")
return
}
stderrFile, err := os.OpenFile(filepath.Join(dir, "Fleet", "fleet-desktop.err"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o666) // nolint:gosec // G302
if err != nil {
log.Error().Err(err).Msg("create file to redirect stderr")
return
}
defer stderrFile.Close()
if _, err := stderrFile.Write([]byte(time.Now().UTC().Format("2006-01-02T15-04-05") + "\n")); err != nil {
log.Error().Err(err).Msg("write to stderr file")
}
// We need to use this method to properly capture golang's panic stderr output.
// Just setting os.Stderr to a file doesn't work (Go's runtime is probably using os.Stderr
// very early).
if _, err := paniclog.RedirectStderr(stderrFile); err != nil {
log.Error().Err(err).Msg("redirect stderr to file")
}
}
// logDir returns the default root directory to use for application-level logs.
//
// On Unix systems, it returns $XDG_STATE_HOME as specified by
// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if
// non-empty, else $HOME/.local/state.
// On Darwin, it returns $HOME/Library/Logs.
// On Windows, it returns %LocalAppData%
//
// If the location cannot be determined (for example, $HOME is not defined),
// then it will return an error.
func logDir() (string, error) {
var dir string
switch runtime.GOOS {
case "windows":
dir = os.Getenv("LocalAppData")
if dir == "" {
return "", errors.New("%LocalAppData% is not defined")
}
case "darwin":
dir = os.Getenv("HOME")
if dir == "" {
return "", errors.New("$HOME is not defined")
}
dir += "/Library/Logs"
default: // Unix
dir = os.Getenv("XDG_STATE_HOME")
if dir == "" {
dir = os.Getenv("HOME")
if dir == "" {
return "", errors.New("neither $XDG_STATE_HOME nor $HOME are defined")
}
dir += "/.local/state"
}
}
return dir, nil
}
func mdmMigrationSetup(ctx context.Context, tufUpdateRoot, fleetURL string, client *service.DeviceClient, tokenReader *token.Reader) (useraction.MDMMigrator, chan struct{}, useraction.MDMOfflineWatcher, error) {
dir, err := migration.Dir()
if err != nil {
return nil, nil, nil, err
}
mrw := migration.NewReadWriter(dir, constant.MigrationFileName)
// we use channel buffer size of 1 to allow one dialog at a time with non-blocking sends.
swiftDialogCh := make(chan struct{}, 1)
_, swiftDialogPath, _ := update.LocalTargetPaths(
tufUpdateRoot,
"swiftDialog",
update.SwiftDialogMacOSTarget,
)
mdmMigrator := useraction.NewMDMMigrator(
swiftDialogPath,
15*time.Minute,
&mdmMigrationHandler{
client: client,
tokenReader: tokenReader,
},
mrw,
fleetURL,
swiftDialogCh,
)
offlineWatcher := useraction.StartMDMMigrationOfflineWatcher(ctx, client, swiftDialogPath, swiftDialogCh, migration.FileWatcher(mrw))
return mdmMigrator, swiftDialogCh, offlineWatcher, nil
}
func runWithPermanentError(errorMessage string) {
onReady := func() {
log.Info().Msg("ready")
systray.SetTooltip("Fleet Desktop")
// Default to dark theme icon because this seems to be a better fit on Linux (Ubuntu at
// least). On macOS this is used as a template icon anyway.
systray.SetTemplateIcon(iconDark, iconDark)
// Add a disabled menu item with the current version
versionItem := systray.AddMenuItem(fmt.Sprintf("Fleet Desktop v%s", version), "")
versionItem.Disable()
systray.AddSeparator()
// We are doing this using two menu items because line breaks
// are not rendered correctly on Windows and MacOS.
for errorMessageLine := range strings.SplitSeq(errorMessage, "\n") {
item := systray.AddMenuItem(strings.TrimSpace(errorMessageLine), "")
item.Disable()
}
}
systray.Run(onReady, func() {
log.Info().Msg("exit")
})
}