diff --git a/go.mod b/go.mod index 3f2ed7094917..c63ea27795cd 100644 --- a/go.mod +++ b/go.mod @@ -134,7 +134,7 @@ require ( golang.org/x/net v0.33.0 golang.org/x/oauth2 v0.22.0 golang.org/x/sync v0.10.0 - golang.org/x/sys v0.28.0 + golang.org/x/sys v0.29.0 golang.org/x/term v0.27.0 golang.org/x/text v0.21.0 golang.org/x/tools v0.23.0 @@ -197,6 +197,8 @@ require ( github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/gobwas/glob v0.2.3 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/gofrs/flock v0.12.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/glog v1.2.4 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect diff --git a/go.sum b/go.sum index bcc3c180ff57..fdac1395c871 100644 --- a/go.sum +++ b/go.sum @@ -338,6 +338,8 @@ github.com/gocarina/gocsv v0.0.0-20220310154401-d4df709ca055/go.mod h1:5YoVOkjYA github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -1097,6 +1099,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= diff --git a/orbit/changes/25396-fleet-desktop-lockfile b/orbit/changes/25396-fleet-desktop-lockfile new file mode 100644 index 000000000000..7662dcf6ba45 --- /dev/null +++ b/orbit/changes/25396-fleet-desktop-lockfile @@ -0,0 +1 @@ +- Ensure only one copy of fleet desktop is running at a time diff --git a/orbit/cmd/desktop/desktop.go b/orbit/cmd/desktop/desktop.go index ebb8800ce9b4..bb76e305fd32 100644 --- a/orbit/cmd/desktop/desktop.go +++ b/orbit/cmd/desktop/desktop.go @@ -25,6 +25,7 @@ import ( "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" @@ -110,6 +111,21 @@ func main() { 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() @@ -564,6 +580,32 @@ func (m *mdmMigrationHandler) ShowInstructions() error { 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() { diff --git a/orbit/cmd/orbit/orbit.go b/orbit/cmd/orbit/orbit.go index 318f105f1949..802cf145a78b 100644 --- a/orbit/cmd/orbit/orbit.go +++ b/orbit/cmd/orbit/orbit.go @@ -1585,14 +1585,6 @@ func newDesktopRunner( func (d *desktopRunner) Execute() error { defer close(d.executeDoneCh) - log.Info().Msg("killing any pre-existing fleet-desktop instances") - - if err := platform.SignalProcessBeforeTerminate(constant.DesktopAppExecName); err != nil && - !errors.Is(err, platform.ErrProcessNotFound) && - !errors.Is(err, platform.ErrComChannelNotFound) { - log.Error().Err(err).Msg("desktop early terminate") - } - log.Info().Str("path", d.desktopPath).Msg("opening") url, err := url.Parse(d.fleetURL) if err != nil { @@ -1640,6 +1632,13 @@ func (d *desktopRunner) Execute() error { return true } + log.Info().Msg("killing any pre-existing fleet-desktop instances") + if err := platform.SignalProcessBeforeTerminate(constant.DesktopAppExecName); err != nil && + !errors.Is(err, platform.ErrProcessNotFound) && + !errors.Is(err, platform.ErrComChannelNotFound) { + log.Error().Err(err).Msg("desktop early terminate") + } + // Orbit runs as root user on Unix and as SYSTEM (Windows Service) user on Windows. // To be able to run the desktop application (mostly to register the icon in the system tray) // we need to run the application as the login user. @@ -1657,7 +1656,7 @@ func (d *desktopRunner) Execute() error { // Second retry logic to monitor fleet-desktop. // Call with waitFirst=true to give some time for the process to start. if done := retry(15*time.Second, true, d.interruptCh, func() bool { - switch _, err := platform.GetProcessByName(constant.DesktopAppExecName); { + switch _, err := platform.GetProcessesByName(constant.DesktopAppExecName); { case err == nil: return true // all good, process is running, retry. case errors.Is(err, platform.ErrProcessNotFound): diff --git a/orbit/pkg/platform/platform.go b/orbit/pkg/platform/platform.go index eb1f894811ed..af07c59a84f3 100644 --- a/orbit/pkg/platform/platform.go +++ b/orbit/pkg/platform/platform.go @@ -27,13 +27,15 @@ func killProcessByName(name string) error { return errors.New("process name should not be empty") } - foundProcess, err := GetProcessByName(name) + foundProcesses, err := GetProcessesByName(name) if err != nil { return fmt.Errorf("get process: %w", err) } - if err := foundProcess.Kill(); err != nil { - return fmt.Errorf("kill process %d: %w", foundProcess.Pid, err) + for _, foundProcess := range foundProcesses { + if err := foundProcess.Kill(); err != nil { + return fmt.Errorf("kill process %d: %w", foundProcess.Pid, err) + } } return nil diff --git a/orbit/pkg/platform/platform_notwindows.go b/orbit/pkg/platform/platform_notwindows.go index 930e6dc28a30..d3c442c2a4e3 100644 --- a/orbit/pkg/platform/platform_notwindows.go +++ b/orbit/pkg/platform/platform_notwindows.go @@ -54,9 +54,9 @@ func SignalProcessBeforeTerminate(processName string) error { return nil } -// GetProcessByName gets a single running process object by its name. +// GetProcessesByName gets all running processes by its name. // Returns ErrProcessNotFound if the process was not found running. -func GetProcessByName(name string) (*gopsutil_process.Process, error) { +func GetProcessesByName(name string) ([]*gopsutil_process.Process, error) { if name == "" { return nil, errors.New("process name should not be empty") } @@ -66,7 +66,7 @@ func GetProcessByName(name string) (*gopsutil_process.Process, error) { return nil, err } - var foundProcess *gopsutil_process.Process + var foundProcesses []*gopsutil_process.Process for _, process := range processes { processName, err := process.Name() if err != nil { @@ -75,16 +75,16 @@ func GetProcessByName(name string) (*gopsutil_process.Process, error) { } if strings.HasPrefix(processName, name) { - foundProcess = process + foundProcesses = append(foundProcesses, process) break } } - if foundProcess == nil { + if len(foundProcesses) == 0 { return nil, ErrProcessNotFound } - return foundProcess, nil + return foundProcesses, nil } func GetSMBiosUUID() (string, UUIDSource, error) { diff --git a/orbit/pkg/platform/platform_windows.go b/orbit/pkg/platform/platform_windows.go index a0fc336b0788..03a82ed5d64d 100644 --- a/orbit/pkg/platform/platform_windows.go +++ b/orbit/pkg/platform/platform_windows.go @@ -131,20 +131,23 @@ func SignalProcessBeforeTerminate(processName string) error { return ErrComChannelNotFound } - foundProcess, err := GetProcessByName(processName) + foundProcesses, err := GetProcessesByName(processName) if err != nil { return fmt.Errorf("get process: %w", err) } - if err := foundProcess.Kill(); err != nil { - return fmt.Errorf("kill process %d: %w", foundProcess.Pid, err) + for _, foundProcess := range foundProcesses { + if err := foundProcess.Kill(); err != nil { + return fmt.Errorf("kill process %d: %w", foundProcess.Pid, err) + } } + return nil } -// GetProcessByName gets a single running process object by its name. +// GetProcessesByName returns a list of running process object by name. // Returns ErrProcessNotFound if the process was not found running. -func GetProcessByName(name string) (*gopsutil_process.Process, error) { +func GetProcessesByName(name string) ([]*gopsutil_process.Process, error) { if name == "" { return nil, errors.New("process name should not be empty") } @@ -164,7 +167,7 @@ func GetProcessByName(name string) (*gopsutil_process.Process, error) { // Closing the handle to avoid handle leaks. defer windows.CloseHandle(snapshot) //nolint:errcheck - var foundProcessID uint32 = 0 + var foundProcessIDs []uint32 // Initializing work structure PROCESSENTRY32W // https://learn.microsoft.com/en-us/windows/win32/api/tlhelp32/ns-tlhelp32-processentry32w @@ -180,10 +183,8 @@ func GetProcessByName(name string) (*gopsutil_process.Process, error) { // Process32First() is going to return ERROR_NO_MORE_FILES when no more threads present // it will return FALSE/nil otherwise for err == nil { - if strings.HasPrefix(syscall.UTF16ToString(procEntry.ExeFile[:]), name) { - foundProcessID = procEntry.ProcessID - break + foundProcessIDs = append(foundProcessIDs, procEntry.ProcessID) } // Process32Next() is calling to keep iterating the snapshot @@ -191,17 +192,27 @@ func GetProcessByName(name string) (*gopsutil_process.Process, error) { err = windows.Process32Next(snapshot, &procEntry) } - process, err := gopsutil_process.NewProcess(int32(foundProcessID)) - if err != nil { - return nil, fmt.Errorf("NewProcess: %w", err) + var processes []*gopsutil_process.Process + + for _, foundProcessID := range foundProcessIDs { + process, err := gopsutil_process.NewProcess(int32(foundProcessID)) + if err != nil { + continue + } + + isRunning, err := process.IsRunning() + if err != nil || !isRunning { + continue + } + + processes = append(processes, process) } - isRunning, err := process.IsRunning() - if err != nil || !isRunning { + if len(processes) == 0 { return nil, ErrProcessNotFound } - return process, nil + return processes, nil } // It obtains the BIOS UUID by calling "cmd.exe /c wmic csproduct get UUID" and parsing the results diff --git a/pkg/open/open_linux.go b/pkg/open/open_linux.go index b68c704d19e1..a84d6f8bf1b9 100644 --- a/pkg/open/open_linux.go +++ b/pkg/open/open_linux.go @@ -41,18 +41,18 @@ func browser(url string) error { // getXWaylandAuthority retrieves the X authority file path from // the running XWayland process environment. func getXWaylandAuthority() (xAuthorityPath string, err error) { - xWaylandProcess, err := platform.GetProcessByName("Xwayland") + xWaylandProcess, err := platform.GetProcessesByName("Xwayland") if err != nil { return "", fmt.Errorf("get process by name: %w", err) } - executablePath, err := xWaylandProcess.Exe() + executablePath, err := xWaylandProcess[0].Exe() if err != nil { return "", fmt.Errorf("get executable path: %w", err) } if executablePath != "/usr/bin/Xwayland" { return "", fmt.Errorf("invalid Xwayland path: %q", executablePath) } - envs, err := xWaylandProcess.Environ() + envs, err := xWaylandProcess[0].Environ() if err != nil { return "", fmt.Errorf("get environment: %w", err) }