Fix Fleet Desktop not launching on OpenSUSE 16 (#44482)
This pull request addresses a startup issue with Fleet Desktop on openSUSE Leap 16 and similar Linux distributions. The main change is to adjust how Fleet Desktop and key-escrow dialogs are launched to avoid environment variable loss caused by login shell profile scripts. The fix is scoped specifically to openSUSE Leap 16+ to avoid impacting other distributions. **Distribution-specific sudo invocation changes:** * The `-i` (login shell) flag is now omitted from the `sudo` command when launching Fleet Desktop and key-escrow dialogs on openSUSE Leap 16 and newer, preventing environment variables from being lost due to profile script interference. [[1]](diffhunk://#diff-633ab361af6795ef458233819e2806dfba4ca56f684866d956321825b8fd2e91R1) [[2]](diffhunk://#diff-3e8315d9f12512bce490457c5d20bd7c5aebaa2a8e18b1abf50e504815dd7a9dR178-R193) * For all other supported distributions, the previous behavior (using `-i`) is preserved to maintain compatibility and avoid unnecessary re-testing. **Detection logic:** * Introduced a new helper function `isOpenSUSELeap16Plus` in `execuser_linux.go` to detect if the host is running openSUSE Leap 16 or newer by parsing `/etc/os-release`. This ensures the workaround is only applied where necessary. --- **Related issue:** N/A — surfaced via field investigation on openSUSE Leap 16 (arm64). This PR addresses two distinct issues that together prevent Fleet Desktop from working on openSUSE Leap 16, both validated end-to-end on a real Leap 16 (arm64) host. ## 1. Launch reliability — drop `sudo -i` `orbit/pkg/execuser/execuser_linux.go` On Linux, Orbit launches Fleet Desktop with: ``` sudo -n -i -u <user> -H env WAYLAND_DISPLAY=… … FLEET_DESKTOP_DEVICE_IDENTIFIER_PATH=/opt/orbit/identifier … /…/fleet-desktop ``` The `-i` flag makes sudo "simulate initial login" — it runs the target user's shell as a login shell and wraps the rest of the command in `bash --login -c '<escaped>'`. That sources `/etc/profile` and every script in `/etc/profile.d/*` before our `env KEY=val … fleet-desktop` line runs, and shell metacharacters (`=`, `:`, `/`, `.`) get backslash-escaped through the shell layer. On **openSUSE Leap 16 (arm64)**, that indirection causes the inline env-var assignments to not reach `fleet-desktop`, which exits immediately with: ``` FTL missing URL environment FLEET_DESKTOP_DEVICE_IDENTIFIER_PATH ``` Orbit then respawns it every ~15 s in a tight kill-and-respawn loop, so the tray icon never appears. **Fix:** drop `-i` from the sudo invocation. We don't need a login shell: - `-H` already sets `HOME` to the target user. - sudo's default `env_reset` sets `USER` / `LOGNAME` / `SHELL` / `MAIL` and `PATH` to `secure_path`. - All session vars (`WAYLAND_DISPLAY`, `DISPLAY`, `DBUS_SESSION_BUS_ADDRESS`, `LD_LIBRARY_PATH`) and every `FLEET_DESKTOP_*` var are already passed explicitly via `env KEY=val …`. After the change, sudo `execve()`s `env` directly with no shell layer in between, so `/etc/profile.d` sourcing and shell-escaping are out of the picture. The `runuser -l` /proc/keys-leak regression from PR #32309 does not apply — that was specific to `runuser -l` creating session keyrings; sudo without `-i` doesn't. # Checklist for submitter - [x] Changes file added: `orbit/changes/fleet-desktop-linux-no-login-shell` - [x] Input data is properly validated; untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. - [x] Timeouts are implemented and retries are limited to avoid infinite loops (script's wait loop now bounded at 90s). - [x] If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes — N/A. ## Testing Manual QA needed before merge: - [x] **openSUSE Leap 16 (arm64)** — Fleet Desktop process starts, stays running, env vars present, no FTL respawn loop. Done via `sudo` shim. - [x] **openSUSE Leap 16 (arm64) — extension fallback** — manual tarball install + schema compilation produces a working tray icon (matching what the script automates). - [ ] **Ubuntu 22.04 / 24.04** — regression check: Fleet Desktop tray icon still appears, key-escrow zenity dialog still renders, AppIndicator script still installs via the official path. - [ ] **Fedora (recent)** — regression check: same as above. - [ ] **Debian** — regression check: same as above. - [ ] **openSUSE Tumbleweed** — confirm `InstallRemoteExtension` path still works (no fallback path triggered). ## fleetd/orbit/Fleet Desktop - [x] Verified compatibility with the latest released version of Fleet — pure launch-flag change plus a script update; no protocol or schema impact. - [x] If the change applies to only one platform, confirmed that `runtime.GOOS` is used as needed to isolate changes — Go change is in `execuser_linux.go`, only built on Linux. The script is Linux-only by construction. - [ ] Verified that fleetd runs on macOS, Linux and Windows — Linux re-verification pending QA above; macOS/Windows code paths unchanged. - [ ] Verified auto-update works from the released version of component to the new version. ## Notes for reviewers - The tray-icon visibility issue is an OS-side prerequisite (GNOME 3.26+ has no native tray), so the AppIndicator extension is required regardless. Even after installing it, Wayland requires a logout/login to pick up new extensions — this is documented behavior and not specific to the fallback path.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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")
|
||||
@@ -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 \
|
||||
@@ -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"
|
||||
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
|
||||
|
||||
# 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
|
||||
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"
|
||||
|
||||
# Sleep to give some time for files to be downloaded.
|
||||
sleep 15
|
||||
# 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
|
||||
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
|
||||
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
|
||||
|
||||
@@ -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.
|
||||
@@ -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"}
|
||||
}
|
||||
env = make([]string, 0)
|
||||
|
||||
if userDisplaySession.Type == userpkg.GuiSessionTypeWayland {
|
||||
@@ -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
|
||||
}
|
||||
|
||||
// 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-*")
|
||||
|
||||
Reference in New Issue
Block a user