fleet/orbit/pkg/user/user_linux.go
Scott Gress 43e4207fef
Don't attempt to start fleet desktop if no user is logged in on Linux (#30261)
for #29942 

# Details

This PR addresses an issue on Linux where Orbit repeatedly attempts to
launch Fleet Desktop even though no GUI user is logged in. The fix is
similar to one implemented for MacOS, where we have Orbit check for the
presence of a real user (not a system user like `gdm` or `root`) before
trying to launch the desktop app.

Part of this work involved moving some functionality from the `execuser`
package to the `user` package, to avoid duplicating functionality.

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [X] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [x] Make sure fleetd is compatible with the latest released version of
Fleet (see [Must
rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md)).
- [x] Orbit runs on macOS, Linux and Windows. Check if the orbit
feature/bugfix should only apply to one platform (`runtime.GOOS`).
- [x] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
The changed code is only executed on Linux, so I tested on Ubuntu,
Fedora and Debian. Also verified that it still works on MacOS and
Windows.

---------

Co-authored-by: Lucas Manuel Rodriguez <lucas@fleetdm.com>
2025-06-27 08:41:13 -05:00

149 lines
3.9 KiB
Go

//go:build linux
// +build linux
package user
import (
"bytes"
"errors"
"fmt"
"os/exec"
"strconv"
"strings"
"github.com/rs/zerolog/log"
)
type User struct {
Name string
ID int64
}
func UserLoggedInViaGui() (*string, error) {
user, err := GetLoginUser()
if err != nil {
return nil, fmt.Errorf("get login user: %w", err)
}
// Bail out if the user is gdm or root, since they aren't GUI users.
if user.Name == "gdm" || user.Name == "root" {
return nil, nil
}
// Check if the user has a GUI session.
displaySessionType, err := GetUserDisplaySessionType(strconv.FormatInt(user.ID, 10))
if err != nil {
log.Debug().Err(err).Msgf("failed to get user display session type for user %s", user.Name)
return nil, nil
}
if displaySessionType == GuiSessionTypeTty {
log.Debug().Msgf("user %s is logged in via TTY, not GUI", user.Name)
return nil, nil
}
return &user.Name, nil
}
// GetLoginUser returns the name and uid of the first login user
// as reported by the `users' command.
//
// NOTE(lucas): It is always picking first login user as returned
// by `users', revisit when working on multi-user/multi-session support.
func GetLoginUser() (*User, error) {
out, err := exec.Command("users").CombinedOutput()
if err != nil {
return nil, fmt.Errorf("users exec failed: %w", err)
}
usernames := parseUsersOutput(string(out))
username := usernames[0]
if username == "" {
return nil, errors.New("no user session found")
}
out, err = exec.Command("id", "-u", username).CombinedOutput()
if err != nil {
return nil, fmt.Errorf("id exec failed: %w", err)
}
uid, err := parseIDOutput(string(out))
if err != nil {
return nil, err
}
return &User{
Name: username,
ID: uid,
}, nil
}
// parseUsersOutput parses the output of the `users' command.
//
// `users' command prints on a single line a blank-separated list of user names of
// users currently logged in to the current host. Each user name
// corresponds to a login session, so if a user has more than one login
// session, that user's name will appear the same number of times in the
// output.
//
// Returns the list of usernames.
func parseUsersOutput(s string) []string {
var users []string
users = append(users, strings.Split(strings.TrimSpace(s), " ")...)
return users
}
// parseIDOutput parses the output of the `id' command.
//
// Returns the parsed uid.
func parseIDOutput(s string) (int64, error) {
uid, err := strconv.ParseInt(strings.TrimSpace(s), 10, 0)
if err != nil {
return 0, fmt.Errorf("failed to parse uid: %w", err)
}
return uid, nil
}
// getUserDisplaySessionType returns the display session type (X11 or Wayland) of the given user.
func GetUserDisplaySessionType(uid string) (guiSessionType, error) {
cmd := exec.Command("loginctl", "show-user", uid, "-p", "Display", "--value")
var stdout bytes.Buffer
cmd.Stdout = &stdout
if err := cmd.Run(); err != nil {
return 0, fmt.Errorf("run 'loginctl' to get user GUI session: %w", err)
}
guiSessionID := strings.TrimSpace(stdout.String())
if guiSessionID == "" {
return 0, nil
}
cmd = exec.Command("loginctl", "show-session", guiSessionID, "-p", "Type", "--value")
stdout.Reset()
cmd.Stdout = &stdout
if err := cmd.Run(); err != nil {
return 0, fmt.Errorf("run 'loginctl' to get user GUI session type: %w", err)
}
guiSessionType := strings.TrimSpace(stdout.String())
switch guiSessionType {
case "":
return 0, errors.New("empty GUI session type")
case "x11":
return GuiSessionTypeX11, nil
case "wayland":
return GuiSessionTypeWayland, nil
case "tty":
return GuiSessionTypeTty, nil
default:
return 0, fmt.Errorf("unknown GUI session type: %q", guiSessionType)
}
}
type guiSessionType int
const (
GuiSessionTypeX11 guiSessionType = iota + 1
GuiSessionTypeWayland
GuiSessionTypeTty
)
func (s guiSessionType) String() string {
if s == GuiSessionTypeX11 {
return "x11"
}
if s == GuiSessionTypeTty {
return "tty"
}
return "wayland"
}