diff --git a/docs/Using Fleet/Supported-host-operating-systems.md b/docs/Using Fleet/Supported-host-operating-systems.md index ce1567877e..f3dbf77196 100644 --- a/docs/Using Fleet/Supported-host-operating-systems.md +++ b/docs/Using Fleet/Supported-host-operating-systems.md @@ -22,10 +22,11 @@ If a table is not available for your host, Fleet will generally handle things be Fleet's agent (fleetd) generated for MacOS by `fleetctl package` does not include native support for M1 Macs. Some values returned may reflect the information returned by Rosetta rather than the system. For example, a CPU will show up as `i486`. ### Linux -Fleet's agent (fleetd) will run on Linux distributions where `glibc` is >= 2.2 (there is ongoing work to make osquery work with `glibc` 2.12+). -If you aren't sure what version of `glibc` your distribution is using, [DistroWatch](https://distrowatch.com/) is a great resource. -> On Linux, Fleet Desktop only supports $DISPLAY `:0`. +> Ubuntu Linux: +> Fleet Desktop currently supports Xorg as X11 server, Wayland is currently not supported. +> Ubuntu 24.04 comes with Wayland enabled by default. To use X11 instead of Wayland you can set +> `WaylandEnable=false` in `/etc/gdm3/custom.conf` and reboot. > Fedora, CentOS 8 and 9 require a [gnome extension](https://extensions.gnome.org/extension/615/appindicator-support/) and Google Chrome set to the default browser for Fleet Desktop. diff --git a/orbit/changes/18925-fleet-desktop-support-ubuntu-24.04-x11 b/orbit/changes/18925-fleet-desktop-support-ubuntu-24.04-x11 new file mode 100644 index 0000000000..acf781e91a --- /dev/null +++ b/orbit/changes/18925-fleet-desktop-support-ubuntu-24.04-x11 @@ -0,0 +1 @@ +* Added code to detect value of `DISPLAY` variable of user instead of defaulting to `:0` (to support Ubuntu 24.04 with Xorg). diff --git a/orbit/pkg/execuser/execuser_linux.go b/orbit/pkg/execuser/execuser_linux.go index dc95667d17..748576e7ff 100644 --- a/orbit/pkg/execuser/execuser_linux.go +++ b/orbit/pkg/execuser/execuser_linux.go @@ -1,11 +1,15 @@ package execuser import ( + "bufio" + "bytes" "errors" "fmt" + "io" "os" "os/exec" "path/filepath" + "regexp" "strconv" "strings" @@ -19,23 +23,44 @@ func run(path string, opts eopts) error { return fmt.Errorf("get user: %w", err) } + // TODO(lucas): Default to display :0 if user DISPLAY environment variable + // could not be found, revisit when working on multi-user/multi-session support. + // This assumes there's only one desktop session and belongs to the + // user returned in `getLoginUID'. + defaultDisplay := ":0" + log.Info(). Str("user", user.name). Int64("id", user.id). - Msg("running sudo") + Msg("attempting to get user's DISPLAY") - // Flag `-i` is 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" - arg := []string{"-i", "-u", user.name, "-H"} - for _, nv := range opts.env { - arg = append(arg, fmt.Sprintf("%s=%s", nv[0], nv[1])) + display, err := getUserDisplay(user.name, opts) + if err != nil { + log.Error(). + Str("user", user.name). + Int64("id", user.id). + Err(err). + Msgf("failed to get user's DISPLAY, using default %s", defaultDisplay) + display = defaultDisplay + } else if display == "" { + log.Warn(). + Str("user", user.name). + Int64("id", user.id). + Msgf("user's DISPLAY not found, using default %s", defaultDisplay) + display = defaultDisplay } - arg = append(arg, - // TODO(lucas): Default to display 0, revisit when working on - // multi-user/multi-session support. This assumes there's only - // one desktop session and belongs to the user returned in `getLoginUID'. - "DISPLAY=:0", + log.Info(). + Str("path", path). + Str("user", user.name). + Int64("id", user.id). + Str("display", display). + Msg("running sudo") + + args := argsForSudo(user, opts) + + args = append(args, + "DISPLAY="+display, // 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. @@ -44,11 +69,14 @@ func run(path string, opts eopts) error { // (because it's already part of the user). fmt.Sprintf("DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/%d/bus", user.id), // 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, ) - cmd := exec.Command("sudo", arg...) + cmd := exec.Command("sudo", args...) cmd.Stderr = os.Stderr cmd.Stdout = os.Stdout log.Printf("cmd=%s", cmd.String()) @@ -64,6 +92,18 @@ type user struct { id int64 } +func argsForSudo(u *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 +} + // getLoginUID returns the name and uid of the first login user // as reported by the `users' command. // @@ -120,3 +160,30 @@ func parseIDOutput(s string) (int64, error) { } return uid, nil } + +var whoLineRegexp = regexp.MustCompile(`(\w+)\s+(:\d+)\s+`) + +func getUserDisplay(user string, opts eopts) (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 "", nil +} diff --git a/orbit/pkg/execuser/execuser_linux_test.go b/orbit/pkg/execuser/execuser_linux_test.go new file mode 100644 index 0000000000..c227df3512 --- /dev/null +++ b/orbit/pkg/execuser/execuser_linux_test.go @@ -0,0 +1,57 @@ +package execuser + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParseWhoOutputForDisplay(t *testing.T) { + testCases := []struct { + name string + output string + user string + expectedDisplay string + }{ + { + "Ubuntu 22.04.2 (X11)", + `foo :0 2024-05-14 17:34 (:0)`, + "foo", + ":0", + }, + { + "Ubuntu 22.04.2 (X11) - user not listed", + `foo :0 2024-05-14 17:34 (:0)`, + "bar", + "", + }, + { + "Ubuntu 24.04 (X11)", + `foo seat0 2024-05-14 17:42 (login screen) +foo :1 2024-05-14 17:42 (:1)`, + "foo", + ":1", + }, + { + "Ubuntu 24.04 (Wayland) - DISPLAY not found", + `foo seat0 2024-05-14 18:11 (login screen) +foo tty2 2024-05-14 18:11 (tty2)`, + "foo", + "", + }, + { + "Empty", + ``, + "foo", + "", + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + display, err := parseWhoOutputForDisplay(bytes.NewReader([]byte(tc.output)), tc.user) + require.NoError(t, err) + require.Equal(t, tc.expectedDisplay, display) + }) + } +} diff --git a/tools/tuf/test/create_repository.sh b/tools/tuf/test/create_repository.sh index 9131852d5b..203eb791bc 100755 --- a/tools/tuf/test/create_repository.sh +++ b/tools/tuf/test/create_repository.sh @@ -29,7 +29,7 @@ SWIFT_DIALOG_MACOS_APP_VERSION=2.2.1 SWIFT_DIALOG_MACOS_APP_BUILD_VERSION=4591 if [[ -z "$OSQUERY_VERSION" ]]; then - OSQUERY_VERSION=5.11.0 + OSQUERY_VERSION=5.12.2 fi mkdir -p $TUF_PATH/tmp