From fa380635900a5e713c5dafbd1e0bfd7a8a89237e Mon Sep 17 00:00:00 2001 From: Allen Houchins <32207388+allenhouchins@users.noreply.github.com> Date: Fri, 1 May 2026 23:26:56 -0500 Subject: [PATCH] Fix Fleet Desktop not launching on OpenSUSE 16 (#44482) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 -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 ''`. 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. --- ...nstall-fleet-desktop-required-extension.sh | 170 +++++++++++++++--- .../fleet-desktop-linux-no-login-shell | 1 + orbit/pkg/execuser/execuser_linux.go | 53 +++++- 3 files changed, 201 insertions(+), 23 deletions(-) create mode 100644 orbit/changes/fleet-desktop-linux-no-login-shell diff --git a/it-and-security/lib/linux/scripts/install-fleet-desktop-required-extension.sh b/it-and-security/lib/linux/scripts/install-fleet-desktop-required-extension.sh index 3def14e124..b2f5f1dd8e 100755 --- a/it-and-security/lib/linux/scripts/install-fleet-desktop-required-extension.sh +++ b/it-and-security/lib/linux/scripts/install-fleet-desktop-required-extension.sh @@ -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 ''` 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 diff --git a/orbit/changes/fleet-desktop-linux-no-login-shell b/orbit/changes/fleet-desktop-linux-no-login-shell new file mode 100644 index 0000000000..afbccc1db9 --- /dev/null +++ b/orbit/changes/fleet-desktop-linux-no-login-shell @@ -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. diff --git a/orbit/pkg/execuser/execuser_linux.go b/orbit/pkg/execuser/execuser_linux.go index e0b1282262..82679abf53 100644 --- a/orbit/pkg/execuser/execuser_linux.go +++ b/orbit/pkg/execuser/execuser_linux.go @@ -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-*")