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-*")