Skip to content

Commit

Permalink
Only allow once instance of fleet desktop at once (#25821)
Browse files Browse the repository at this point in the history
#25396

---------

Co-authored-by: Lucas Manuel Rodriguez <[email protected]>
  • Loading branch information
dantecatalfamo and lucasmrod authored Feb 25, 2025
1 parent ae00add commit a1e7523
Show file tree
Hide file tree
Showing 9 changed files with 98 additions and 37 deletions.
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
1 change: 1 addition & 0 deletions orbit/changes/25396-fleet-desktop-lockfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Ensure only one copy of fleet desktop is running at a time
42 changes: 42 additions & 0 deletions orbit/cmd/desktop/desktop.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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() {
Expand Down
17 changes: 8 additions & 9 deletions orbit/cmd/orbit/orbit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand All @@ -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):
Expand Down
8 changes: 5 additions & 3 deletions orbit/pkg/platform/platform.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions orbit/pkg/platform/platform_notwindows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand All @@ -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 {
Expand All @@ -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) {
Expand Down
41 changes: 26 additions & 15 deletions orbit/pkg/platform/platform_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand All @@ -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
Expand All @@ -180,28 +183,36 @@ 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
// https://learn.microsoft.com/en-us/windows/win32/api/tlhelp32/nf-tlhelp32-process32next
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
Expand Down
6 changes: 3 additions & 3 deletions pkg/open/open_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down

0 comments on commit a1e7523

Please sign in to comment.