Add Fleet Desktop support for Wayland display sessions (#25998)
For #19043. See the versions and distributions tested during development on the QA notes of #19043. --- - [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/Committing-Changes.md#changes-files) for more information. - [x] A detailed QA plan exists on the associated ticket (if it isn't there, work with the product group's QA engineer to add it) - [X] Manual QA for all new/changed functionality - For Orbit and Fleet Desktop changes: - [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. - [X] Auto-update manual QA, from released version of component to new version (see [tools/tuf/test](../tools/tuf/test/README.md)).
This commit is contained in:
@@ -0,0 +1 @@
|
||||
* Added support for Wayland display sessions.
|
||||
@@ -537,7 +537,7 @@ func (m *mdmMigrationHandler) NotifyRemote() error {
|
||||
func (m *mdmMigrationHandler) ShowInstructions() error {
|
||||
openURL := m.client.BrowserDeviceURL(m.tokenReader.GetCached())
|
||||
if err := open.Browser(openURL); err != nil {
|
||||
log.Error().Err(err).Str("url", openURL).Msg("open browser")
|
||||
log.Error().Err(err).Str("url", openURL).Msg("open browser my device (mdm migration handler)")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -123,31 +124,43 @@ func getUserAndDisplayArgs(path string, opts eopts) ([]string, error) {
|
||||
return nil, 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("attempting to get user session type and display")
|
||||
|
||||
log.Info().
|
||||
Str("user", user.name).
|
||||
Int64("id", user.id).
|
||||
Msg("attempting to get user's DISPLAY")
|
||||
|
||||
display, err := getUserDisplay(user.name, opts)
|
||||
// Get user's display session type (x11 vs. wayland).
|
||||
uid := strconv.FormatInt(user.id, 10)
|
||||
userDisplaySessionType, err := getUserDisplaySessionType(uid)
|
||||
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
|
||||
// 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
|
||||
}
|
||||
|
||||
var display string
|
||||
if userDisplaySessionType == 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().
|
||||
@@ -155,12 +168,21 @@ func getUserAndDisplayArgs(path string, opts eopts) ([]string, error) {
|
||||
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 {
|
||||
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,
|
||||
"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.
|
||||
@@ -247,7 +269,71 @@ func parseIDOutput(s string) (int64, error) {
|
||||
|
||||
var whoLineRegexp = regexp.MustCompile(`(\w+)\s+(:\d+)\s+`)
|
||||
|
||||
func getUserDisplay(user string, opts eopts) (string, error) {
|
||||
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-*")
|
||||
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
|
||||
@@ -269,5 +355,5 @@ func parseWhoOutputForDisplay(output io.Reader, user string) (string, error) {
|
||||
if err := scanner.Err(); err != nil {
|
||||
return "", fmt.Errorf("scanner error: %w", err)
|
||||
}
|
||||
return "", nil
|
||||
return "", errors.New("display not found on who output")
|
||||
}
|
||||
|
||||
@@ -13,18 +13,21 @@ func TestParseWhoOutputForDisplay(t *testing.T) {
|
||||
output string
|
||||
user string
|
||||
expectedDisplay string
|
||||
expectedErr bool
|
||||
}{
|
||||
{
|
||||
"Ubuntu 22.04.2 (X11)",
|
||||
`foo :0 2024-05-14 17:34 (:0)`,
|
||||
"foo",
|
||||
":0",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Ubuntu 22.04.2 (X11) - user not listed",
|
||||
`foo :0 2024-05-14 17:34 (:0)`,
|
||||
"bar",
|
||||
"",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Ubuntu 24.04 (X11)",
|
||||
@@ -32,6 +35,7 @@ func TestParseWhoOutputForDisplay(t *testing.T) {
|
||||
foo :1 2024-05-14 17:42 (:1)`,
|
||||
"foo",
|
||||
":1",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Ubuntu 24.04 (Wayland) - DISPLAY not found",
|
||||
@@ -39,19 +43,25 @@ foo :1 2024-05-14 17:42 (:1)`,
|
||||
foo tty2 2024-05-14 18:11 (tty2)`,
|
||||
"foo",
|
||||
"",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Empty",
|
||||
``,
|
||||
"foo",
|
||||
"",
|
||||
true,
|
||||
},
|
||||
}
|
||||
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)
|
||||
if tc.expectedErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+59
-2
@@ -1,8 +1,65 @@
|
||||
package open
|
||||
|
||||
import "os/exec"
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/orbit/pkg/platform"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func browser(url string) error {
|
||||
// xdg-open requires XAUTHORITY set when running on a Wayland session (compatibility mode).
|
||||
// We get XAUTHORITY from the Xwayland process environment.
|
||||
//
|
||||
// We have to do this here instead of when executing fleet-desktop because the Xwayland process
|
||||
// may not be running yet when orbit is executing fleet-desktop.
|
||||
xAuthority, err := getXWaylandAuthority()
|
||||
log.Info().Str("XAUTHORITY", xAuthority).Err(err).Msg("Xwayland process")
|
||||
if err == nil {
|
||||
os.Setenv("XAUTHORITY", xAuthority)
|
||||
}
|
||||
// xdg-open is available on most Linux-y systems
|
||||
return exec.Command("xdg-open", url).Run()
|
||||
cmd := exec.Command("xdg-open", url)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
// Must be asynchronous (Start, not Run) because xdg-open will continue running
|
||||
// and block this goroutine if it was the process that opened the browser.
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("xdg-open failed to start: %w", err)
|
||||
}
|
||||
go func() {
|
||||
// We must call wait to avoid defunct processes.
|
||||
cmd.Wait() //nolint:errcheck
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// getXWaylandAuthority retrieves the X authority file path from
|
||||
// the running XWayland process environment.
|
||||
func getXWaylandAuthority() (xAuthorityPath string, err error) {
|
||||
xWaylandProcess, err := platform.GetProcessByName("Xwayland")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get process by name: %w", err)
|
||||
}
|
||||
executablePath, err := xWaylandProcess.Exe()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get executable path: %w", err)
|
||||
}
|
||||
if executablePath != "/usr/bin/Xwayland" {
|
||||
return "", fmt.Errorf("invalid Xwayland path: %q", executablePath)
|
||||
}
|
||||
envs, err := xWaylandProcess.Environ()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get environment: %w", err)
|
||||
}
|
||||
for _, env := range envs {
|
||||
if strings.HasPrefix(env, "XAUTHORITY=") {
|
||||
return strings.TrimPrefix(env, "XAUTHORITY="), nil
|
||||
}
|
||||
}
|
||||
return "", errors.New("XAUTHORITY not found")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user