diff --git a/orbit/changes/29942-dont-launch-desktop-on-linux-incorrectly b/orbit/changes/29942-dont-launch-desktop-on-linux-incorrectly new file mode 100644 index 0000000000..faed64bb56 --- /dev/null +++ b/orbit/changes/29942-dont-launch-desktop-on-linux-incorrectly @@ -0,0 +1 @@ +- Fixed an issue where Orbit would attempt to launch Fleet Desktop on Linux systems without a logged-in GUI user diff --git a/orbit/cmd/orbit/orbit.go b/orbit/cmd/orbit/orbit.go index 90d9e0da9c..24ff5a0a64 100644 --- a/orbit/cmd/orbit/orbit.go +++ b/orbit/cmd/orbit/orbit.go @@ -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)) diff --git a/orbit/pkg/execuser/execuser_linux.go b/orbit/pkg/execuser/execuser_linux.go index cda04b829e..9ece110589 100644 --- a/orbit/pkg/execuser/execuser_linux.go +++ b/orbit/pkg/execuser/execuser_linux.go @@ -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-*") diff --git a/orbit/pkg/user/user_linux.go b/orbit/pkg/user/user_linux.go new file mode 100644 index 0000000000..62ab333774 --- /dev/null +++ b/orbit/pkg/user/user_linux.go @@ -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" +} diff --git a/orbit/pkg/user/user_notdarwin.go b/orbit/pkg/user/user_windows.go similarity index 90% rename from orbit/pkg/user/user_notdarwin.go rename to orbit/pkg/user/user_windows.go index 28d48b220d..7ecfe2f3ea 100644 --- a/orbit/pkg/user/user_notdarwin.go +++ b/orbit/pkg/user/user_windows.go @@ -1,5 +1,5 @@ -//go:build !darwin -// +build !darwin +//go:build windows +// +build windows package user