mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 21:47:20 +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>
258 lines
7.6 KiB
Go
258 lines
7.6 KiB
Go
package execuser
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
userpkg "github.com/fleetdm/fleet/v4/orbit/pkg/user"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// run uses sudo to run the given path as login user.
|
|
func run(path string, opts eopts) (lastLogs string, err error) {
|
|
args, err := getUserAndDisplayArgs(path, opts)
|
|
if err != nil {
|
|
return "", fmt.Errorf("get args: %w", err)
|
|
}
|
|
|
|
args = append(args,
|
|
// Append the packaged libayatana-appindicator3 libraries path to LD_LIBRARY_PATH.
|
|
//
|
|
// Fleet Desktop doesn't use libayatana-appindicator3 since 1.18.3, but we need to
|
|
// keep this to support older versions of Fleet Desktop.
|
|
fmt.Sprintf("LD_LIBRARY_PATH=%s:%s", filepath.Dir(path), os.ExpandEnv("$LD_LIBRARY_PATH")),
|
|
path,
|
|
)
|
|
|
|
if len(opts.args) > 0 {
|
|
for _, arg := range opts.args {
|
|
args = append(args, arg[0], arg[1])
|
|
}
|
|
}
|
|
|
|
cmd := exec.Command("sudo", args...)
|
|
cmd.Stderr = os.Stderr
|
|
cmd.Stdout = os.Stdout
|
|
log.Printf("cmd=%s", cmd.String())
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
return "", fmt.Errorf("open path %q: %w", path, err)
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
// run uses sudo to run the given path as login user and waits for the process to finish.
|
|
func runWithOutput(path string, opts eopts) (output []byte, exitCode int, err error) {
|
|
args, err := getUserAndDisplayArgs(path, opts)
|
|
if err != nil {
|
|
return nil, -1, fmt.Errorf("get args: %w", err)
|
|
}
|
|
|
|
args = append(args, path)
|
|
|
|
if len(opts.args) > 0 {
|
|
for _, arg := range opts.args {
|
|
args = append(args, arg[0], arg[1])
|
|
}
|
|
}
|
|
|
|
// Prefix with "timeout" and "sudo" if applicable
|
|
var cmdArgs []string
|
|
if opts.timeout > 0 {
|
|
cmdArgs = append(cmdArgs, "timeout", fmt.Sprintf("%ds", int(opts.timeout.Seconds())))
|
|
}
|
|
cmdArgs = append(cmdArgs, "sudo")
|
|
cmdArgs = append(cmdArgs, args...)
|
|
|
|
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...) // #nosec G204
|
|
|
|
log.Printf("cmd=%s", cmd.String())
|
|
|
|
output, err = cmd.Output()
|
|
if err != nil {
|
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
|
exitCode = exitErr.ExitCode()
|
|
return output, exitCode, fmt.Errorf("%q exited with code %d: %w", path, exitCode, err)
|
|
}
|
|
return output, -1, fmt.Errorf("%q error: %w", path, err)
|
|
}
|
|
|
|
return output, exitCode, nil
|
|
}
|
|
|
|
func runWithStdin(path string, opts eopts) (io.WriteCloser, error) {
|
|
args, err := getUserAndDisplayArgs(path, opts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get args: %w", err)
|
|
}
|
|
|
|
args = append(args, path)
|
|
|
|
if len(opts.args) > 0 {
|
|
for _, arg := range opts.args {
|
|
args = append(args, arg[0], arg[1])
|
|
}
|
|
}
|
|
|
|
cmd := exec.Command("sudo", args...)
|
|
log.Printf("cmd=%s", cmd.String())
|
|
|
|
stdin, err := cmd.StdinPipe()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("stdin pipe: %w", err)
|
|
}
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
return nil, fmt.Errorf("open path %q: %w", path, err)
|
|
}
|
|
|
|
return stdin, nil
|
|
}
|
|
|
|
func getUserAndDisplayArgs(path string, opts eopts) ([]string, error) {
|
|
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")
|
|
|
|
// 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, 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)
|
|
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
|
|
}
|
|
}
|
|
|
|
log.Info().
|
|
Str("path", path).
|
|
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 == 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-")
|
|
args = append(args, "DISPLAY="+x11Display)
|
|
} else {
|
|
args = append(args, "DISPLAY="+display)
|
|
}
|
|
|
|
args = append(args,
|
|
// DBUS_SESSION_BUS_ADDRESS sets the location of the user login session bus.
|
|
// Required by the libayatana-appindicator3 library to display a tray icon
|
|
// on the desktop session.
|
|
//
|
|
// 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),
|
|
)
|
|
|
|
return args, nil
|
|
}
|
|
|
|
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"}
|
|
for _, nv := range opts.env {
|
|
args = append(args, fmt.Sprintf("%s=%s", nv[0], nv[1]))
|
|
}
|
|
return args
|
|
}
|
|
|
|
var whoLineRegexp = regexp.MustCompile(`(\w+)\s+(:\d+)\s+`)
|
|
|
|
// 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-*")
|
|
if err != nil {
|
|
return "", fmt.Errorf("list wayland socket files: %w", err)
|
|
}
|
|
sort.Slice(matches, func(i, j int) bool {
|
|
return matches[i] < matches[j]
|
|
})
|
|
for _, match := range matches {
|
|
if strings.HasSuffix(match, ".lock") {
|
|
continue
|
|
}
|
|
return filepath.Base(match), nil
|
|
}
|
|
return "", errors.New("wayland socket not found")
|
|
}
|
|
|
|
// getUserX11Display returns the value to set on DISPLAY for the given user.
|
|
func getUserX11Display(user string) (string, error) {
|
|
cmd := exec.Command("who")
|
|
var stdout bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
if err := cmd.Run(); err != nil {
|
|
return "", fmt.Errorf("run 'who' to get user display: %w", err)
|
|
}
|
|
return parseWhoOutputForDisplay(&stdout, user)
|
|
}
|
|
|
|
func parseWhoOutputForDisplay(output io.Reader, user string) (string, error) {
|
|
scanner := bufio.NewScanner(output)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
matches := whoLineRegexp.FindStringSubmatch(line)
|
|
if len(matches) > 1 && matches[1] == user {
|
|
return matches[2], nil
|
|
}
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
return "", fmt.Errorf("scanner error: %w", err)
|
|
}
|
|
return "", errors.New("display not found on who output")
|
|
}
|