Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#!/bin/bash

# Script assumes one user is using the desktop environment (no multi-session).
# It was tested on Fedora 38, 39, Debian 12, and OpenSUSE Leap/Tumbleweed.
# It was tested on Fedora 38, 39, Debian 12, openSUSE Leap 15/Tumbleweed, and
# openSUSE Leap 16.

set -x

Expand Down Expand Up @@ -29,11 +30,42 @@ if [ -f /etc/os-release ]; then
. /etc/os-release
distro_id="$ID"
distro_name="$NAME"
distro_version_id="$VERSION_ID"
else
distro_id="unknown"
distro_name="unknown"
distro_version_id=""
fi

# Detect openSUSE Leap 16+. On Leap 16 the GNOME extension install path differs
# from other distros: extensions.gnome.org doesn't list a compatible build of
# this extension, and `sudo -i` exec's are denied, so we install the extension
# directly from the upstream tarball and use a `sudo -u ... env` invocation
# instead of `sudo -i`. Other distros keep the previously QA'd behavior.
is_opensuse_leap_16_plus=false
if [ "$distro_id" = "opensuse-leap" ]; then
major_version="${distro_version_id%%.*}"
if [ -n "$major_version" ] && [ "$major_version" -ge 16 ] 2>/dev/null; then
is_opensuse_leap_16_plus=true
fi
fi

# run_as_user runs a command as the GUI user with the session DBus address set.
# On openSUSE Leap 16+ we drop sudo's -i flag because, in that environment,
# `sudo -i` wraps the command in `bash --login -c '<escaped>'` and the exec of
# /bin/bash is denied (the same root cause that breaks fleet-desktop launch).
# Other distros keep the previous "-i + DBUS=val command" form so we don't
# change behavior on already-QA'd platforms.
run_as_user() {
if [ "$is_opensuse_leap_16_plus" = true ]; then
sudo -u "$username" -H \
env DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$uid/bus" "$@"
else
sudo -i -u "$username" -H \
DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$uid/bus" "$@"
fi
}

# Determine extension name and installation method based on distribution
case "$distro_id" in
"opensuse-leap"|"opensuse-tumbleweed"|"opensuse")
Expand All @@ -51,19 +83,24 @@ case "$distro_id" in
;;
esac

# Check if the AppIndicator extension is already installed
# Check if the AppIndicator extension is already installed. We look for
# metadata.json (not just the directory) so that a half-baked stub left over
# from an earlier failed install — e.g. one where gnome-shell created the
# directory but bailed before writing metadata.json — is treated as
# "not installed" and gets re-installed properly. Without this, downstream
# `gnome-extensions enable` would fail with "Extension does not exist".
extension_path="/home/$username/.local/share/gnome-shell/extensions/$extension_name"
extension_metadata="$extension_path/metadata.json"

extension_installed=false
if [ -d "$extension_path" ]; then
if [ -f "$extension_metadata" ]; then
extension_installed=true
fi

# If no extension is installed, install the appropriate one
if [ "$extension_installed" = false ]; then
# Show notification to user before the prompt.
sudo -i -u $username -H DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$uid/bus \
gdbus call --session \
run_as_user gdbus call --session \
--dest org.freedesktop.Notifications \
--object-path /org/freedesktop/Notifications \
--method org.freedesktop.Notifications.Notify \
Expand All @@ -72,25 +109,114 @@ if [ "$extension_installed" = false ]; then
# Give some time to user to see notification.
sleep 10

# Use GNOME Extensions for all distributions (including OpenSUSE)
sudo -i -u $username -H DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$uid/bus \
gdbus call --session \
--dest org.gnome.Shell.Extensions \
--object-path /org/gnome/Shell/Extensions \
--method org.gnome.Shell.Extensions.InstallRemoteExtension \
"$extension_name"

# Wait until the extension is accepted by the user ("gdbus call" command above is asynchronous).
while [ ! -d "/home/$username/.local/share/gnome-shell/extensions/$extension_name" ]; do
sleep 1
done

# Sleep to give some time for files to be downloaded.
sleep 15
if [ "$is_opensuse_leap_16_plus" = true ]; then
# On openSUSE Leap 16+, skip the dbus InstallRemoteExtension call and go
# straight to the upstream tarball: extensions.gnome.org doesn't list a
# compatible build of this extension for Leap 16's GNOME, so the dbus
# call returns "Remote peer disconnected" immediately and at best leaves
# a half-baked stub directory behind. Skipping it avoids ~90s of
# dead-end waiting and a useless install prompt the user can't complete.
#
# Clear any stub directory left from a previous run so our copy below
# isn't laying files on top of a half-baked install.
if [ -d "$extension_path" ] && [ ! -f "$extension_metadata" ]; then
sudo rm -rf "$extension_path"
fi

extensions_dir="/home/$username/.local/share/gnome-shell/extensions"
tarball_url="https://github.com/ubuntu/gnome-shell-extension-appindicator/archive/refs/heads/master.tar.gz"
tmp_dir=$(mktemp -d /tmp/fleet-appindicator.XXXXXX)
tarball="$tmp_dir/extension.tar.gz"
Comment thread
allenhouchins marked this conversation as resolved.

# Download tarball — curl preferred, wget as fallback. Bail out cleanly
# if neither can fetch it (e.g. no network) so we don't leave a
# half-installed extension.
fetched=false
if command -v curl >/dev/null 2>&1; then
if curl -fsSL --max-time 60 -o "$tarball" "$tarball_url"; then
fetched=true
fi
elif command -v wget >/dev/null 2>&1; then
if wget -q --timeout=60 -O "$tarball" "$tarball_url"; then
fetched=true
fi
fi

if [ "$fetched" = true ] && [ -s "$tarball" ]; then
# tar isn't part of a minimal openSUSE Leap 16 install, so pull it
# in via zypper if it's missing. We're already running as root here
# (this whole branch runs from the root-detached script invocation
# at the top of this file).
if ! command -v tar >/dev/null 2>&1; then
zypper --non-interactive install --no-recommends tar \
>/dev/null 2>&1 || true
fi

if command -v tar >/dev/null 2>&1; then
sudo -u $username -H mkdir -p "$extensions_dir"
# Extract into a staging dir under our root-owned tmp_dir, then
# copy the contents into the user's UUID-named extension path
# and hand ownership to the user. We do the copy as root rather
# than `sudo -u $username` because mktemp's tmp_dir is mode 700
# owned by root, which the user can't traverse.
staging="$tmp_dir/staging"
mkdir -p "$staging"
if tar -xzf "$tarball" -C "$staging" --strip-components=1; then
mkdir -p "$extension_path"
cp -r "$staging/." "$extension_path/"
chown -R "$username":"$username" "$extension_path"
if [ -d "$extension_path/schemas" ] && command -v glib-compile-schemas >/dev/null 2>&1; then
sudo -u $username -H glib-compile-schemas "$extension_path/schemas/"
fi
Comment thread
allenhouchins marked this conversation as resolved.
fi
fi
fi

rm -rf "$tmp_dir"
else
# Other distributions: prompt the user via gnome-shell's
# InstallRemoteExtension and wait indefinitely for them to accept.
# This is the previously QA'd behavior on Fedora / Debian / Ubuntu /
# openSUSE Tumbleweed.
run_as_user gdbus call --session \
--dest org.gnome.Shell.Extensions \
--object-path /org/gnome/Shell/Extensions \
--method org.gnome.Shell.Extensions.InstallRemoteExtension \
"$extension_name"

while [ ! -f "$extension_metadata" ]; do
sleep 1
done

# Give gnome-shell a moment to finish writing extension files after
# the directory shows up. Not needed on the Leap 16 path above, where
# we already wrote everything synchronously via curl + tar.
sleep 15
Comment thread
allenhouchins marked this conversation as resolved.
fi
fi

# Enable the extension
if [ -d "$extension_path" ]; then
sudo -i -u $username -H DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$uid/bus \
gnome-extensions enable "$extension_name"
# Enable the extension.
if [ -f "$extension_metadata" ]; then
if [ "$is_opensuse_leap_16_plus" = true ]; then
# gnome-shell on Leap 16 (Wayland) doesn't rescan
# ~/.local/share/gnome-shell/extensions while running, so
# `gnome-extensions enable` (which talks to the live gnome-shell)
# reports the extension as missing. Pre-seed the dconf list directly;
# gnome-shell will pick it up and enable it on the user's next login.
current_extensions=$(run_as_user gsettings get org.gnome.shell enabled-extensions)
case "$current_extensions" in
*"'$extension_name'"*)
;;
"@as []"|"[]")
run_as_user gsettings set org.gnome.shell enabled-extensions \
"['$extension_name']"
;;
*)
run_as_user gsettings set org.gnome.shell enabled-extensions \
"${current_extensions%]}, '$extension_name']"
;;
esac
else
run_as_user gnome-extensions enable "$extension_name"
fi
fi
1 change: 1 addition & 0 deletions orbit/changes/fleet-desktop-linux-no-login-shell
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Fixed Fleet Desktop failing to start on openSUSE Leap 16, by dropping the `-i` (login shell) flag from the sudo invocation used to launch Fleet Desktop and key-escrow dialogs as the logged-in user.
53 changes: 52 additions & 1 deletion orbit/pkg/execuser/execuser_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,22 @@ func getConfigForCommand(user string, path string) (args []string, env []string,
Str("session_type", userDisplaySession.Type.String()).
Msg("running sudo")

args = []string{"-n", "-i", "-u", user, "-H"}
// On openSUSE Leap 16+ we drop -i (login shell). With -i, sudo runs the target
// user's shell as a login shell and passes the rest of the command via
// `bash --login -c`, which sources /etc/profile and /etc/profile.d/* and
// shell-escapes the inline command. On Leap 16 that environment indirection
// causes our `env KEY=val ... fleet-desktop` invocation to lose env vars, so
// fleet-desktop exits with "missing URL environment ..." and Orbit respawns it
// in a tight loop. -H sets HOME to the target user; sudo's default env_reset
// already sets USER/LOGNAME/SHELL.
//
// We keep -i on every other supported distribution to preserve the previously
// QA'd behavior.
if isOpenSUSELeap16Plus() {
args = []string{"-n", "-u", user, "-H"}
} else {
args = []string{"-n", "-i", "-u", user, "-H"}
}
Comment on lines +178 to +193
env = make([]string, 0)

if userDisplaySession.Type == userpkg.GuiSessionTypeWayland {
Expand All @@ -200,6 +215,42 @@ func getConfigForCommand(user string, path string) (args []string, env []string,
return args, env, nil
}

// isOpenSUSELeap16Plus reports whether the host is running openSUSE Leap 16 or
// newer. We scope the no-login-shell sudo workaround to that distribution since
// it is the one observed to break under sudo -i; other distributions retain the
// previous (login-shell) launch path so we don't have to re-QA them.
func isOpenSUSELeap16Plus() bool {
data, err := os.ReadFile("/etc/os-release")
if err != nil {
return false
}
var id, versionID string
for line := range strings.SplitSeq(string(data), "\n") {
key, value, ok := strings.Cut(line, "=")
if !ok {
continue
}
// /etc/os-release values may be quoted.
value = strings.Trim(value, `"'`)
switch key {
case "ID":
id = value
case "VERSION_ID":
versionID = value
}
}
if id != "opensuse-leap" {
return false
}
// VERSION_ID is typically "16" or "16.0"; compare the major component.
major, _, _ := strings.Cut(versionID, ".")
n, err := strconv.Atoi(major)
if err != nil {
return false
}
return n >= 16
}
Comment on lines +218 to +252

// 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-*")
Expand Down
Loading