Fix orbit active GUI session detection to start Fleet Desktop and key escrowing on Linux (#39777)

Resolves #36024 and #34501.

Main change is about stop using the first user in the `users` command
output [*] and instead use `loginctl` commands to pick the correct
current active GUI user.

[*] `users` was returning empty on some new distributions, and in
multi-sessions we were always picking the first one (even if it wasn't
active).

- [X] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.

## Testing

- [x] QA'd all new/changed functionality manually

## fleetd/orbit/Fleet Desktop

- [x] Verified compatibility 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] If the change applies to only one platform, confirmed that
`runtime.GOOS` is used as needed to isolate changes
- [x] Verified that fleetd runs on macOS, Linux and Windows
- [x] Verified auto-update works from the released version of component
to the new version (see [tools/tuf/test](../tools/tuf/test/README.md))


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **Bug Fixes**
* Fixed Fleet Desktop startup to correctly detect and use the active GUI
session on Linux systems.
* Improved GUI user detection for dialog prompts, ensuring system
dialogs run in the proper user context.

* **Improvements**
* Enhanced error reporting and logging clarity for GUI session detection
failures.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Lucas Manuel Rodriguez 2026-02-16 11:41:16 -03:00 committed by GitHub
parent 77e4004133
commit 0823cc7e76
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 285 additions and 215 deletions

View file

@ -0,0 +1 @@
* Improved GUI user detection in orbit to use the correct active GUI session when starting Fleet Desktop.

View file

@ -1870,20 +1870,25 @@ func (d *desktopRunner) Execute() error {
for {
// First retry logic to start fleet-desktop.
if done := retry(30*time.Second, false, d.interruptCh, func() bool {
// On MacOS, if we attempt to run Fleet Desktop while the user is not logged in through
// the GUI, MacOS returns an error. See https://github.com/fleetdm/fleet/issues/14698
// for more details.
//
// - On MacOS, if we attempt to run Fleet Desktop while the user is not logged in through
// the GUI, MacOS returns an error. See https://github.com/fleetdm/fleet/issues/14698
// for more details.
// - On Linux, we also don't want to start the Fleet Desktop unless there's an active GUI session.
// - On Windows, user.UserLoggedInViaGui is a no-op, and execuser.Run will take care of
// starting Fleet Desktop with the correct GUI user.
//
loggedInUser, err := user.UserLoggedInViaGui()
if err != nil {
log.Debug().Err(err).Msg("desktop.IsUserLoggedInGui")
log.Debug().Err(err).Msg("desktop.IsUserLoggedViaGui")
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))
log.Debug().Msgf("found GUI user: %q, attempting fleet-desktop start", *loggedInUser)
if *loggedInUser != "" {
opts = append(opts, execuser.WithUser(*loggedInUser))
}
@ -2418,7 +2423,7 @@ func executeFleetDesktopWithPermanentError(desktopPath string, errorMessage stri
log.Debug().Msg("No GUI user found, skipping fleet-desktop start")
return nil
}
log.Debug().Msg(fmt.Sprintf("Found GUI user: %v, attempting fleet-desktop start", loggedInUser))
log.Debug().Msgf("found GUI user: %q, attempting fleet-desktop start", *loggedInUser)
log.Info().Msg("killing any pre-existing fleet-desktop instances")
if err := platform.SignalProcessBeforeTerminate(constant.DesktopAppExecName); err != nil &&

View file

@ -3,7 +3,6 @@
package execuser
import (
"io"
"time"
)
@ -70,11 +69,3 @@ func RunWithOutput(path string, opts ...Option) (output []byte, exitCode int, er
}
return runWithOutput(path, o)
}
func RunWithStdin(path string, opts ...Option) (io.WriteCloser, error) {
var o eopts
for _, fn := range opts {
fn(&o)
}
return runWithStdin(path, o)
}

View file

@ -62,7 +62,3 @@ func run(path string, opts eopts) (lastLogs string, err error) {
func runWithOutput(path string, opts eopts) (output []byte, exitCode int, err error) {
return nil, 0, errors.New("not implemented")
}
func runWithStdin(path string, opts eopts) (io.WriteCloser, error) {
return nil, errors.New("not implemented")
}

View file

@ -11,7 +11,6 @@ import (
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
userpkg "github.com/fleetdm/fleet/v4/orbit/pkg/user"
@ -20,7 +19,11 @@ import (
// base command to setup an exec.Cmd using `runuser`
func baserun(path string, opts eopts) (cmd *exec.Cmd, err error) {
args, env, err := getConfigForCommand(path)
if opts.user == "" {
return nil, errors.New("missing user")
}
args, env, err := getConfigForCommand(opts.user, path)
if err != nil {
return nil, fmt.Errorf("get args: %w", err)
}
@ -84,7 +87,7 @@ func run(path string, opts eopts) (lastLogs string, err error) {
return "", nil
}
// run a command and return its output and exit code.
// runWithOutput runs a command and return its output and exit code.
func runWithOutput(path string, opts eopts) (output []byte, exitCode int, err error) {
cmd, err := baserun(path, opts)
if err != nil {
@ -103,85 +106,80 @@ func runWithOutput(path string, opts eopts) (output []byte, exitCode int, err er
return output, exitCode, nil
}
// run a command that requires stdin input, returning a pipe to write to stdin.
func runWithStdin(path string, opts eopts) (io.WriteCloser, error) {
cmd, err := baserun(path, opts)
func getUserID(user string) (string, error) {
uid_, err := exec.Command("id", "-u", user).Output()
if err != nil {
return nil, err
return "", fmt.Errorf("failed to execute id command for %q: %w", user, err)
}
stdin, err := cmd.StdinPipe()
if err != nil {
return nil, fmt.Errorf("stdin pipe: %w", err)
uid := strings.TrimSpace(string(uid_))
if uid == "" {
return "", errors.New("failed to get uid")
}
if err := cmd.Start(); err != nil {
return nil, fmt.Errorf("open path %q: %w", path, err)
}
return stdin, nil
return uid, nil
}
func getConfigForCommand(path string) (args []string, env []string, err error) {
user, err := userpkg.GetLoginUser()
if err != nil {
return nil, 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")
// Get user's display session type (x11 vs. wayland).
uid := strconv.FormatInt(user.ID, 10)
userDisplaySessionType, err := userpkg.GetUserDisplaySessionType(uid)
if userDisplaySessionType == userpkg.GuiSessionTypeTty {
return nil, 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 = userpkg.GuiSessionTypeWayland
}
var display string
if userDisplaySessionType == userpkg.GuiSessionTypeX11 {
x11Display, err := getUserX11Display(user.Name)
func getDisplayVariableForSession(user string, userID string, displaySessionType userpkg.GuiSessionType) string {
if displaySessionType == userpkg.GuiSessionTypeX11 {
x11Display, err := getUserX11Display(user)
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.
// Default to display ':0' if user display could not be found.
// This assumes there's only one desktop session and belongs to the
// user returned in `getLoginUID'.
display = ":0"
} else {
display = x11Display
}
} else {
waylandDisplay, err := getUserWaylandDisplay(uid)
if err != nil {
log.Error().Err(err).Msg("failed to get wayland display, using default wayland-0")
// TODO(lucas): Revisit when working on multi-user/multi-session support.
// Default to display 'wayland-0' if user display could not be found.
// This assumes there's only one desktop session and belongs to the
// user returned in `getLoginUID'.
display = "wayland-0"
} else {
display = waylandDisplay
return ":0"
}
return x11Display
}
waylandDisplay, err := getUserWaylandDisplay(userID)
if err != nil {
log.Error().Err(err).Msg("failed to get wayland display, using default wayland-0")
// TODO(lucas): Revisit when working on multi-user/multi-session support.
// Default to display 'wayland-0' if user display could not be found.
// This assumes there's only one desktop session and belongs to the
// user returned in `getLoginUID'.
return "wayland-0"
}
return waylandDisplay
}
func getConfigForCommand(user string, path string) (args []string, env []string, err error) {
// Get user ID
userID, err := getUserID(user)
if err != nil {
return nil, nil, fmt.Errorf("get user ID: %w", err)
}
log.Info().Str("user", user).Str("id", userID).Msg("attempting to get user session type and display")
// Get user's display session type.
userDisplaySession, err := userpkg.GetUserDisplaySessionType(userID)
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")
userDisplaySession = &userpkg.UserDisplaySession{
Type: userpkg.GuiSessionTypeWayland,
}
} else if userDisplaySession.Type == userpkg.GuiSessionTypeTty {
return nil, nil, fmt.Errorf("user %q (%s) is not running a GUI session", user, userID)
}
// Get user's "display" variable for the GUI session.
display := getDisplayVariableForSession(user, userID, userDisplaySession.Type)
log.Info().
Str("path", path).
Str("user", user.Name).
Int64("id", user.ID).
Str("user", user).
Str("id", userID).
Str("display", display).
Str("session_type", userDisplaySessionType.String()).
Str("session_type", userDisplaySession.Type.String()).
Msg("running sudo")
args = []string{"-n", "-i", "-u", user.Name, "-H"}
args = []string{"-n", "-i", "-u", user, "-H"}
env = make([]string, 0)
if userDisplaySessionType == userpkg.GuiSessionTypeWayland {
if userDisplaySession.Type == userpkg.GuiSessionTypeWayland {
env = append(env, "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-")
@ -197,7 +195,7 @@ func getConfigForCommand(path string) (args []string, env []string, err 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/%s/bus", userID),
)
return args, env, nil

View file

@ -9,7 +9,6 @@ package execuser
import (
"errors"
"fmt"
"io"
"os"
"strings"
"unsafe"
@ -136,10 +135,6 @@ func runWithOutput(path string, opts eopts) (output []byte, exitCode int, err er
return nil, 0, errors.New("not implemented")
}
func runWithStdin(path string, opts eopts) (io.WriteCloser, error) {
return nil, errors.New("not implemented")
}
// getCurrentUserSessionId will attempt to resolve
// the session ID of the user currently active on
// the system.

View file

@ -2,11 +2,14 @@ package kdialog
import (
"errors"
"fmt"
"strings"
"time"
"github.com/fleetdm/fleet/v4/orbit/pkg/dialog"
"github.com/fleetdm/fleet/v4/orbit/pkg/execuser"
"github.com/fleetdm/fleet/v4/orbit/pkg/user"
"github.com/rs/zerolog/log"
)
const kdialogProcessName = "kdialog"
@ -79,6 +82,17 @@ func execCmdWithOutput(timeout time.Duration, args ...string) ([]byte, int, erro
opts = append(opts, execuser.WithTimeout(timeout))
}
// Retrieve and set active GUI user.
loggedInUser, err := user.UserLoggedInViaGui()
if err != nil {
return nil, 0, fmt.Errorf("user logged in via GUI: %w", err)
}
if loggedInUser == nil || *loggedInUser == "" {
return nil, 0, errors.New("no GUI user found")
}
log.Debug().Msgf("found GUI user: %s, attempting kdialog", *loggedInUser)
opts = append(opts, execuser.WithUser(*loggedInUser))
output, exitCode, err := execuser.RunWithOutput(kdialogProcessName, opts...)
if err != nil {
return nil, exitCode, err

View file

@ -135,7 +135,7 @@ func (lr *LuksRunner) getEscrowKey(ctx context.Context, devicePath string) ([]by
device := luksdevice.New(luksdevice.AESXTSPlain64Cipher)
// Prompt user for existing LUKS passphrase
passphrase, err := lr.entryPrompt(ctx, entryDialogTitle, entryDialogText)
passphrase, err := lr.entryPrompt(entryDialogTitle, entryDialogText)
if err != nil {
return nil, nil, fmt.Errorf("Failed to show passphrase entry prompt: %w", err)
}
@ -157,7 +157,7 @@ func (lr *LuksRunner) getEscrowKey(ctx context.Context, devicePath string) ([]by
break
}
passphrase, err = lr.entryPrompt(ctx, entryDialogTitle, retryEntryDialogText)
passphrase, err = lr.entryPrompt(entryDialogTitle, retryEntryDialogText)
if err != nil {
return nil, nil, fmt.Errorf("Failed re-prompting for passphrase: %w", err)
}
@ -275,7 +275,7 @@ func generateRandomPassphrase() ([]byte, error) {
return passphrase, nil
}
func (lr *LuksRunner) entryPrompt(ctx context.Context, title, text string) ([]byte, error) {
func (lr *LuksRunner) entryPrompt(title, text string) ([]byte, error) {
passphrase, err := lr.notifier.ShowEntry(dialog.EntryOptions{
Title: title,
Text: text,

View file

@ -7,7 +7,6 @@ import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"strconv"
"strings"
@ -20,170 +19,146 @@ type User struct {
ID int64
}
// UserLoggedInViaGui returns the username that has an active GUI session.
// It returns nil, nil if there's no user with an active GUI session.
func UserLoggedInViaGui() (*string, error) {
user, err := GetLoginUser()
users, err := getLoginUsers()
if err != nil {
return nil, fmt.Errorf("get login user: %w", err)
return nil, fmt.Errorf("get login users: %w", err)
}
// Bail out if the user is gdm or root, since they aren't GUI users.
// User gdm-greeter is active during the GUI log-in prompt (GNOME 49).
if user.Name == "gdm" || user.Name == "root" || user.Name == "gdm-greeter" {
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
for _, user := range users {
// Skip system/display manager users since they aren't GUI users.
// User gdm-greeter is active during the GUI log-in prompt (GNOME 49).
if user.Name == "gdm" || user.Name == "root" || user.Name == "gdm-greeter" {
continue
}
// Check if the user has an active GUI session.
session, err := GetUserDisplaySessionType(strconv.FormatInt(user.ID, 10))
if err != nil {
log.Debug().Err(err).Msgf("failed to get user display session for user %s", user.Name)
continue
}
if !session.Active {
log.Debug().Msgf("user %s has an inactive display session, skipping", user.Name)
continue
}
if session.Type == GuiSessionTypeTty {
log.Debug().Msgf("user %s is logged in via TTY, not GUI", user.Name)
continue
}
return &user.Name, nil
}
return &user.Name, nil
// No valid user found
return nil, 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()
// getLoginUsers returns all logged-in users as reported by
// `loginctl list-users`.
func getLoginUsers() ([]User, error) {
out, err := exec.Command("loginctl", "list-users", "--no-legend", "--no-pager").CombinedOutput()
if err != nil {
return nil, fmt.Errorf("users exec failed: %w", err)
return nil, fmt.Errorf("loginctl list-users exec failed: %w, output: %s", err, string(out))
}
usernames := parseUsersOutput(string(out))
username := usernames[0]
if username == "" {
return parseLoginctlUsersOutput(string(out))
}
// parseLoginctlUsersOutput parses the output of `loginctl list-users --no-legend`.
// Each line has the format: UID USERNAME
func parseLoginctlUsersOutput(s string) ([]User, error) {
var users []User
for line := range strings.SplitSeq(strings.TrimSpace(s), "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
fields := strings.Fields(line)
if len(fields) < 2 {
continue
}
uid, err := strconv.ParseInt(fields[0], 10, 64)
if err != nil {
log.Debug().Err(err).Msgf("failed to parse uid from loginctl output: %q", line)
continue
}
users = append(users, User{
Name: fields[1],
ID: uid,
})
}
if len(users) == 0 {
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
return users, nil
}
// GetSELinuxUserContext returns the SELinux context for the given user.
// Example: `unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023`
//
// If SELinux is not enabled, the `runcon` command is not available,
// or context cannot be determined, it returns nil.
func GetSELinuxUserContext(user *User) *string {
// If SELinux is not enabled, return nil right away.
if _, err := os.Stat("/sys/fs/selinux/enforce"); err != nil {
return nil
}
// If runcon is not available, we won't be able to switch contexts,
// so return nil.
if _, err := exec.LookPath("runcon"); err != nil {
log.Warn().Msg("runcon not available, returning nil for user context since we can't switch contexts")
return nil
}
// Find the first systemd process for the user and read its SELinux context.
pidBytes, err := exec.Command("pgrep", "-u", strconv.FormatInt(user.ID, 10), "-nx", "systemd").Output() // #nosec G204
if err != nil {
log.Debug().Msgf("Error finding systemd process for user %s: %v", user.Name, err)
return nil
}
pid := strings.TrimSpace(string(pidBytes))
if pid == "" {
log.Debug().Msgf("No systemd process found for user %s", user.Name)
return nil
}
ctx, err := os.ReadFile("/proc/" + pid + "/attr/current")
if err != nil {
log.Debug().Msgf("Error reading SELinux context for user %s: %v", user.Name, err)
return nil
}
context := strings.TrimSpace(string(ctx))
// Remove any null byte at the end
context = strings.TrimSuffix(context, "\x00")
if context == "" {
log.Debug().Msg("Empty SELinux context for user " + user.Name)
return nil
}
return &context
// UserDisplaySession holds the display session type and active status for a user.
type UserDisplaySession struct {
Type GuiSessionType
Active bool
}
// 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) {
// GetUserDisplaySessionType returns the display session type (X11 or Wayland)
// and active status of the given user. Returns an error if the user doesn't have
// a Display session.
func GetUserDisplaySessionType(uid string) (*UserDisplaySession, error) {
// Get the "Display" session ID of the user.
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)
return nil, fmt.Errorf("run 'loginctl' to get user GUI session: %w", err)
}
guiSessionID := strings.TrimSpace(stdout.String())
if guiSessionID == "" {
return 0, nil
return nil, errors.New("empty display session")
}
// Get the "Type" of 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)
return nil, fmt.Errorf("run 'loginctl' to get user GUI session type: %w", err)
}
guiSessionType := strings.TrimSpace(stdout.String())
switch guiSessionType {
var sessionType GuiSessionType
switch t := strings.TrimSpace(stdout.String()); t {
case "":
return 0, errors.New("empty GUI session type")
return nil, errors.New("empty GUI session type")
case "x11":
return GuiSessionTypeX11, nil
sessionType = GuiSessionTypeX11
case "wayland":
return GuiSessionTypeWayland, nil
sessionType = GuiSessionTypeWayland
case "tty":
return GuiSessionTypeTty, nil
sessionType = GuiSessionTypeTty
default:
return 0, fmt.Errorf("unknown GUI session type: %q", guiSessionType)
return nil, fmt.Errorf("unknown GUI session type: %q", t)
}
// Get the "Active" property of the session.
cmd = exec.Command("loginctl", "show-session", guiSessionID, "-p", "Active", "--value")
stdout.Reset()
cmd.Stdout = &stdout
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("run 'loginctl' to get session active status: %w", err)
}
active := strings.TrimSpace(stdout.String()) == "yes"
return &UserDisplaySession{
Type: sessionType,
Active: active,
}, nil
}
type guiSessionType int
type GuiSessionType int
const (
GuiSessionTypeX11 guiSessionType = iota + 1
GuiSessionTypeX11 GuiSessionType = iota + 1
GuiSessionTypeWayland
GuiSessionTypeTty
)
func (s guiSessionType) String() string {
func (s GuiSessionType) String() string {
if s == GuiSessionTypeX11 {
return "x11"
}

View file

@ -0,0 +1,83 @@
//go:build linux
package user
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParseLoginctlUsersOutput(t *testing.T) {
tests := []struct {
name string
input string
want []User
wantErr string
}{
{
name: "single user",
input: " 1000 alice\n",
want: []User{{Name: "alice", ID: 1000}},
},
{
name: "multiple users",
input: " 1000 alice\n 1001 bob\n",
want: []User{
{Name: "alice", ID: 1000},
{Name: "bob", ID: 1001},
},
},
{
name: "includes system user",
input: " 0 root\n 1000 alice\n 120 gdm\n",
want: []User{
{Name: "root", ID: 0},
{Name: "alice", ID: 1000},
{Name: "gdm", ID: 120},
},
},
{
name: "no leading whitespace",
input: "1000 alice\n1001 bob\n",
want: []User{
{Name: "alice", ID: 1000},
{Name: "bob", ID: 1001},
},
},
{
name: "extra columns (some systemd versions)",
input: "1000 alice extra-stuff\n",
want: []User{{Name: "alice", ID: 1000}},
},
{
name: "empty output",
input: "",
wantErr: "no user session found",
},
{
name: "whitespace only",
input: " \n \n",
wantErr: "no user session found",
},
{
name: "skips malformed lines",
input: "not-a-uid alice\n1000 bob\n",
want: []User{{Name: "bob", ID: 1000}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseLoginctlUsersOutput(tt.input)
if tt.wantErr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
return
}
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}

View file

@ -1,11 +1,9 @@
//go:build windows
// +build windows
package user
// UserLoggedInViaGui returns the name of the user logged into the machine via the GUI. This
// function is only relevant on MacOS, where it is used to prevent errors when launching Fleet
// Desktop. For other platforms we return an empty string which can be ignored.
// function is only relevant on MacOS and Linux, where it is used to prevent errors when launching Fleet Desktop.
func UserLoggedInViaGui() (*string, error) {
user := ""
return &user, nil

View file

@ -7,6 +7,8 @@ import (
"github.com/fleetdm/fleet/v4/orbit/pkg/dialog"
"github.com/fleetdm/fleet/v4/orbit/pkg/execuser"
"github.com/fleetdm/fleet/v4/orbit/pkg/user"
"github.com/rs/zerolog/log"
)
const zenityProcessName = "zenity"
@ -88,6 +90,18 @@ func execCmdWithOutput(args ...string) ([]byte, int, error) {
opts = append(opts, execuser.WithArg(arg, "")) // Using empty value for positional args
}
// Retrieve and set active GUI user.
loggedInUser, err := user.UserLoggedInViaGui()
if err != nil {
return nil, 0, fmt.Errorf("user logged in via GUI: %w", err)
}
if loggedInUser == nil || *loggedInUser == "" {
return nil, 0, errors.New("no GUI user found")
}
log.Debug().Msgf("found GUI user: %s, attempting zenity", *loggedInUser)
opts = append(opts, execuser.WithUser(*loggedInUser))
// Execute zenity.
output, exitCode, err := execuser.RunWithOutput(zenityProcessName, opts...)
// Trim the newline from zenity output