fleet/orbit/pkg/user/user_linux.go
Juan Fernandez d6191b99cf
Icon mis sized on KDE envs (#40259)
Resolves #36522: Icon mis-sized on KDE 

Added new icon artifact to be used on KDE environments due to the fact
that previous icon appeared mis-sized on KDE envs.
2026-02-24 11:59:35 -04:00

213 lines
5.7 KiB
Go

//go:build linux
// +build linux
package user
import (
"bytes"
"errors"
"fmt"
"os/exec"
"os/user"
"strconv"
"strings"
"github.com/rs/zerolog/log"
)
type User struct {
Name string
ID int64
}
func getDisplaySessionFor(user User) *UserDisplaySession {
// 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" {
return nil
}
// Check if the user has an active GUI session.
userID := strconv.FormatInt(user.ID, 10)
session, err := GetUserDisplaySessionType(userID)
if err != nil {
log.Debug().Err(err).Msgf("failed to get user display session for user %s", user.Name)
return nil
}
if !session.Active {
log.Debug().Msgf("user %s has an inactive display session, skipping", user.Name)
return nil
}
if session.Type == GuiSessionTypeTty {
log.Debug().Msgf("user %s is logged in via TTY, not GUI", user.Name)
return nil
}
return session
}
// 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) {
users, err := getLoginUsers()
if err != nil {
return nil, fmt.Errorf("get login users: %w", err)
}
for _, u := range users {
if session := getDisplaySessionFor(u); session != nil {
return &u.Name, nil
}
}
// No valid user found
return nil, nil
}
// GetCurrentUserDisplaySession returns the Display session of the user associated with the current process
func GetCurrentUserDisplaySession() (*UserDisplaySession, error) {
currentUser, err := user.Current()
if err != nil {
return nil, fmt.Errorf("get current user: %w", err)
}
if currentUser == nil {
return nil, nil
}
uid, err := strconv.ParseInt(currentUser.Uid, 10, 64)
if err != nil {
return nil, fmt.Errorf("parse uid: %w", err)
}
if session := getDisplaySessionFor(User{Name: currentUser.Username, ID: uid}); session != nil {
return session, nil
}
// No valid user found
return nil, nil
}
// 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("loginctl list-users exec failed: %w, output: %s", err, string(out))
}
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")
}
return users, nil
}
// UserDisplaySession holds the display session type and active status for a user.
type UserDisplaySession struct {
Type GuiSessionType
Active bool
Desktop string
}
// GetUserDisplaySessionType returns the display session type (X11 or Wayland),
// active status, and desktop session env var 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 nil, fmt.Errorf("run 'loginctl' to get user GUI session: %w", err)
}
guiSessionID := strings.TrimSpace(stdout.String())
if guiSessionID == "" {
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 nil, fmt.Errorf("run 'loginctl' to get user GUI session type: %w", err)
}
var sessionType GuiSessionType
switch t := strings.TrimSpace(stdout.String()); t {
case "":
return nil, errors.New("empty GUI session type")
case "x11":
sessionType = GuiSessionTypeX11
case "wayland":
sessionType = GuiSessionTypeWayland
case "tty":
sessionType = GuiSessionTypeTty
default:
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"
// Get the "Desktop" property of the session.
cmd = exec.Command("loginctl", "show-session", guiSessionID, "-p", "Desktop", "--value")
stdout.Reset()
cmd.Stdout = &stdout
desktop := ""
if err := cmd.Run(); err != nil {
log.Debug().Err(err).Msgf("failed to get desktop session for user %s", uid)
} else {
desktop = strings.TrimSpace(stdout.String())
}
return &UserDisplaySession{
Type: sessionType,
Active: active,
Desktop: desktop,
}, nil
}
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"
}