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>
This commit is contained in:
co-authored by
Lucas Manuel Rodriguez
parent
bd2f22ca2e
commit
43e4207fef
@@ -0,0 +1 @@
|
||||
- Fixed an issue where Orbit would attempt to launch Fleet Desktop on Linux systems without a logged-in GUI user
|
||||
@@ -1640,10 +1640,11 @@ func (d *desktopRunner) Execute() error {
|
||||
log.Debug().Err(err).Msg("desktop.IsUserLoggedInGui")
|
||||
return true
|
||||
}
|
||||
|
||||
if loggedInUser == nil {
|
||||
log.Debug().Msg("No GUI user found, skipping fleet-desktop start")
|
||||
return true
|
||||
}
|
||||
log.Debug().Msg(fmt.Sprintf("Found GUI user: %v, attempting fleet-desktop start", loggedInUser))
|
||||
|
||||
if *loggedInUser != "" {
|
||||
opts = append(opts, execuser.WithUser(*loggedInUser))
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
userpkg "github.com/fleetdm/fleet/v4/orbit/pkg/user"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
@@ -119,26 +120,29 @@ func runWithStdin(path string, opts eopts) (io.WriteCloser, error) {
|
||||
}
|
||||
|
||||
func getUserAndDisplayArgs(path string, opts eopts) ([]string, error) {
|
||||
user, err := getLoginUID()
|
||||
user, err := userpkg.GetLoginUser()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get user: %w", err)
|
||||
}
|
||||
|
||||
log.Info().Str("user", user.name).Int64("id", user.id).Msg("attempting to get user session type and display")
|
||||
log.Info().Str("user", user.Name).Int64("id", user.ID).Msg("attempting to get user session type and display")
|
||||
|
||||
// Get user's display session type (x11 vs. wayland).
|
||||
uid := strconv.FormatInt(user.id, 10)
|
||||
userDisplaySessionType, err := getUserDisplaySessionType(uid)
|
||||
uid := strconv.FormatInt(user.ID, 10)
|
||||
userDisplaySessionType, err := userpkg.GetUserDisplaySessionType(uid)
|
||||
if userDisplaySessionType == userpkg.GuiSessionTypeTty {
|
||||
return nil, fmt.Errorf("user %q (%d) is not running a GUI session", user.Name, user.ID)
|
||||
}
|
||||
if err != nil {
|
||||
// Wayland is the default for most distributions, thus we assume
|
||||
// wayland if we couldn't determine the session type.
|
||||
log.Error().Err(err).Msg("assuming wayland session")
|
||||
userDisplaySessionType = guiSessionTypeWayland
|
||||
userDisplaySessionType = userpkg.GuiSessionTypeWayland
|
||||
}
|
||||
|
||||
var display string
|
||||
if userDisplaySessionType == guiSessionTypeX11 {
|
||||
x11Display, err := getUserX11Display(user.name)
|
||||
if userDisplaySessionType == userpkg.GuiSessionTypeX11 {
|
||||
x11Display, err := getUserX11Display(user.Name)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to get X11 display, using default :0")
|
||||
// TODO(lucas): Revisit when working on multi-user/multi-session support.
|
||||
@@ -165,15 +169,15 @@ func getUserAndDisplayArgs(path string, opts eopts) ([]string, error) {
|
||||
|
||||
log.Info().
|
||||
Str("path", path).
|
||||
Str("user", user.name).
|
||||
Int64("id", user.id).
|
||||
Str("user", user.Name).
|
||||
Int64("id", user.ID).
|
||||
Str("display", display).
|
||||
Str("session_type", userDisplaySessionType.String()).
|
||||
Msg("running sudo")
|
||||
|
||||
args := argsForSudo(user, opts)
|
||||
|
||||
if userDisplaySessionType == guiSessionTypeWayland {
|
||||
if userDisplaySessionType == userpkg.GuiSessionTypeWayland {
|
||||
args = append(args, "WAYLAND_DISPLAY="+display)
|
||||
// For xdg-open to work on a Wayland session we still need to set the DISPLAY variable.
|
||||
x11Display := ":" + strings.TrimPrefix(display, "wayland-")
|
||||
@@ -189,131 +193,26 @@ func getUserAndDisplayArgs(path string, opts eopts) ([]string, error) {
|
||||
//
|
||||
// This is required for Ubuntu 18, and not required for Ubuntu 21/22
|
||||
// (because it's already part of the user).
|
||||
fmt.Sprintf("DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/%d/bus", user.id),
|
||||
fmt.Sprintf("DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/%d/bus", user.ID),
|
||||
)
|
||||
|
||||
return args, nil
|
||||
}
|
||||
|
||||
type user struct {
|
||||
name string
|
||||
id int64
|
||||
}
|
||||
|
||||
func argsForSudo(u *user, opts eopts) []string {
|
||||
func argsForSudo(u *userpkg.User, opts eopts) []string {
|
||||
// -H: "[...] to set HOME environment to what's specified in the target's user password database entry."
|
||||
// -i: needed to run the command with the user's context, from `man sudo`:
|
||||
// "The command is run with an environment similar to the one a user would receive at log in"
|
||||
// -u: "[..]Run the command as a user other than the default target user (usually root)."
|
||||
args := []string{"-i", "-u", u.name, "-H"}
|
||||
args := []string{"-i", "-u", u.Name, "-H"}
|
||||
for _, nv := range opts.env {
|
||||
args = append(args, fmt.Sprintf("%s=%s", nv[0], nv[1]))
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
// getLoginUID 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 getLoginUID() (*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
|
||||
}
|
||||
|
||||
var whoLineRegexp = regexp.MustCompile(`(\w+)\s+(:\d+)\s+`)
|
||||
|
||||
type guiSessionType int
|
||||
|
||||
const (
|
||||
guiSessionTypeX11 guiSessionType = iota + 1
|
||||
guiSessionTypeWayland
|
||||
)
|
||||
|
||||
func (s guiSessionType) String() string {
|
||||
if s == guiSessionTypeX11 {
|
||||
return "x11"
|
||||
}
|
||||
return "wayland"
|
||||
}
|
||||
|
||||
// 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, errors.New("empty GUI session")
|
||||
}
|
||||
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
|
||||
default:
|
||||
return 0, fmt.Errorf("unknown GUI session type: %q", guiSessionType)
|
||||
}
|
||||
}
|
||||
|
||||
// getUserWaylandDisplay returns the value to set on WAYLAND_DISPLAY for the given user.
|
||||
func getUserWaylandDisplay(uid string) (string, error) {
|
||||
matches, err := filepath.Glob("/run/user/" + uid + "/wayland-*")
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
//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"
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
//go:build !darwin
|
||||
// +build !darwin
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package user
|
||||
|
||||
Reference in New Issue
Block a user