mirror of
https://github.com/fleetdm/fleet
synced 2026-05-14 20:48:35 +00:00
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>
149 lines
3.9 KiB
Go
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"
|
|
}
|