mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Resolves #36526 Set static system tray title to avoid system tray settings not persisting across process restarts on Debian.
777 lines
25 KiB
Go
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")
|
|
})
|
|
}
|