diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d3e20c393d..c613924e57 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -629,7 +629,7 @@ jobs: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} - - name: Fix AppImage (remove infra libs, symlink system GStreamer) + - name: Fix AppImage (remove infra libs, shim host GStreamer) run: | mapfile -t APPIMAGES < <(find desktop/src-tauri/target/release/bundle/appimage -name '*.AppImage' -type f) if [[ ${#APPIMAGES[@]} -eq 0 ]]; then diff --git a/desktop/scripts/fix-appimage.sh b/desktop/scripts/fix-appimage.sh index 45f537bb72..c51b85723a 100755 --- a/desktop/scripts/fix-appimage.sh +++ b/desktop/scripts/fix-appimage.sh @@ -19,12 +19,19 @@ # skew causes eglGetDisplay to return EGL_BAD_PARAMETER under Wayland, which # WebKitWebProcess treats as fatal and aborts before the window ever appears. # -# 2. GStreamer crash: linuxdeploy also bundles libgst*.so* (GStreamer core libs). -# AppRun unconditionally sets GST_PLUGIN_SYSTEM_PATH_1_0 to a dir inside the -# AppImage that the bundler never populates (bundleMediaFramework is false by -# default), so GStreamer's plugin discovery yields an empty registry. The -# "GStreamer element appsink not found" error kills the render process; as a -# side effect the broken run poisons ~/.cache/gstreamer-1.0/registry.x86_64.bin. +# 2. GStreamer crash: linuxdeploy's compiled AppRun.wrapped force-sets +# GST_PLUGIN_SYSTEM_PATH_1_0 to $APPDIR/usr/lib/gstreamer-1.0 -- a dir the +# bundler never populates (bundleMediaFramework is off, and we strip the +# bundled libgst* core below to use the host's). Crucially, once that variable +# is set it *replaces* GStreamer's compiled-in default search path rather than +# adding to it, so the app finds ZERO plugins on every distro: +# "GStreamer element appsink not found" kills the WebKitWebProcess and the +# window never paints. An earlier revision of this script hid the failure on +# Debian only by symlinking usr/lib/gstreamer-1.0 to the Debian multiarch dir +# (/usr/lib/x86_64-linux-gnu/gstreamer-1.0); that symlink dangles on Arch and +# Fedora, and the "safe fallback to default discovery" it assumed does not +# exist -- a set GST_PLUGIN_SYSTEM_PATH_1_0 disables the default. A broken run +# also poisons ~/.cache/gstreamer-1.0/registry.x86_64.bin. # # 3. WebKit helper mismatch (latent): the bundled WebKit helpers # (WebKitNetworkProcess/WebKitWebProcess) have RUNPATH=$ORIGIN only, and @@ -35,11 +42,17 @@ # wrong -- spawning nothing, dying on unresolved bundled libs, or spawning # the system helpers -- and the window never appears. # -# Fix: remove the offending libs so the app uses the system copies (which are -# newer and ABI-compatible on any distro shipping glib >= 2.72 / Ubuntu 22.04+), -# and symlink the system GStreamer plugin directory so discovery works correctly. -# No tauri.conf.json knob can do this — bundle.linux.appimage only exposes -# bundleMediaFramework, files (copy-only, no remove/symlink), and bundleXdgOpen. +# Fix: (a) remove the offending libs so the app uses the system copies (newer and +# ABI-compatible on any distro shipping glib >= 2.72 / Ubuntu 22.04+), and +# (b) install a launcher shim in front of the app binary that strips the +# bundle-pointing GST_PLUGIN_* overrides AppRun.wrapped injects, letting the host +# GStreamer resolve plugins via its own default path (correct on Debian, Arch, and +# Fedora alike). The shim has to run *after* AppRun.wrapped: the wrapper rewrites +# the variable last -- after every apprun-hook -- so any value set before it is +# discarded (verified empirically; a runtime GST_PLUGIN_SYSTEM_PATH_1_0 passed +# into the AppImage does not survive). No tauri.conf.json knob can do this -- +# bundle.linux.appimage only exposes bundleMediaFramework, files (copy-only, no +# remove/symlink), and bundleXdgOpen. set -euo pipefail @@ -61,16 +74,6 @@ APPIMAGE_NAME="$(basename "$APPIMAGE_ABS")" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DESKTOP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -# Detect multiarch triplet for GStreamer plugin path. -case "$(uname -m)" in - x86_64) MULTIARCH="x86_64-linux-gnu" ;; - aarch64) MULTIARCH="aarch64-linux-gnu" ;; - *) - echo "Error: unsupported architecture: $(uname -m)" >&2 - exit 1 - ;; -esac - WORKDIR="$(mktemp -d)" trap 'rm -rf "$WORKDIR"' EXIT @@ -107,12 +110,58 @@ rm -f \ "$LIBDIR"/libelf.so* \ "$LIBDIR"/libffi.so* -echo "==> Symlinking system GStreamer plugin directory" -# On distros without the Debian multiarch layout (e.g. Arch), this symlink -# dangles — GStreamer then falls back to its default plugin discovery, which -# is a safe degradation (unlike the original empty in-bundle dir). -rm -rf "$LIBDIR/gstreamer-1.0" -ln -s "/usr/lib/$MULTIARCH/gstreamer-1.0" "$LIBDIR/gstreamer-1.0" +echo "==> Installing GStreamer launcher shim on the app binary" +# AppRun.wrapped force-sets GST_PLUGIN_SYSTEM_PATH_1_0 (and the 0.10-era +# GST_PLUGIN_SYSTEM_PATH) to $APPDIR/usr/lib/gstreamer-1.0 — a dir we bundle no +# plugins into. Because a set path *replaces* GStreamer's default instead of +# extending it, the app finds zero plugins on any distro and WebKit aborts. The +# wrapper rewrites the variable after every apprun-hook, so the only place to undo +# it is a shim between AppRun.wrapped and the real binary. First confirm the +# wrapper still injects the override; if a tauri/linuxdeploy bump drops it, the +# shim becomes a harmless no-op, but we want a human to re-verify rather than +# silently ship — so fail loudly (mirrors the libwayland guard above). +APPRUN_WRAPPED="$WORKDIR/squashfs-root/AppRun.wrapped" +if ! grep -aq "GST_PLUGIN_SYSTEM_PATH_1_0" "$APPRUN_WRAPPED"; then + echo "Error: AppRun.wrapped is missing or no longer references GST_PLUGIN_SYSTEM_PATH_1_0 — GStreamer path injection changed; re-verify fix-appimage.sh" >&2 + exit 1 +fi + +APP_BIN="$WORKDIR/squashfs-root/usr/bin/buzz-desktop" +if [[ ! -f "$APP_BIN" ]]; then + echo "Error: app binary usr/bin/buzz-desktop not found — bundler layout changed; update fix-appimage.sh" >&2 + exit 1 +fi +if [[ -e "$APP_BIN.bin" ]]; then + echo "Error: usr/bin/buzz-desktop.bin already exists — shim already installed?" >&2 + exit 1 +fi + +# The real binary moves aside; buzz-desktop becomes a shim AppRun.wrapped execs. +mv "$APP_BIN" "$APP_BIN.bin" +cat > "$APP_BIN" <<'SHIM' +#!/usr/bin/env bash +# GStreamer shim installed by desktop/scripts/fix-appimage.sh. +# +# linuxdeploy's AppRun.wrapped force-sets GST_PLUGIN_SYSTEM_PATH_1_0 to an empty +# in-bundle dir ($APPDIR/usr/lib/gstreamer-1.0). A set path *replaces* the host's +# default GStreamer search path, so the app finds zero plugins and WebKit aborts +# (blank window). Drop the bundle-pointing GST_PLUGIN_* overrides so the system +# GStreamer — which we use, having removed the bundled core libs — resolves +# plugins via its own default path on any distro. Values that don't point into +# this AppImage are the user's own and are preserved. +here="$(dirname "$(readlink -f "$0")")" +appdir="$(readlink -f "$here/../..")" +for var in GST_PLUGIN_SYSTEM_PATH_1_0 GST_PLUGIN_SYSTEM_PATH \ + GST_PLUGIN_PATH_1_0 GST_PLUGIN_PATH \ + GST_PLUGIN_SCANNER GST_PLUGIN_SCANNER_1_0; do + val="${!var-}" + if [[ -n "$val" && "$val" == *"$appdir/"* ]]; then + unset "$var" + fi +done +exec -a "buzz-desktop" "$here/buzz-desktop.bin" "$@" +SHIM +chmod +x "$APP_BIN" echo "==> Repacking AppImage" # Pass a pinned type2 runtime when provided (CI sets APPIMAGETOOL_RUNTIME_FILE); diff --git a/desktop/src-tauri/src/managed_agents/runtime.rs b/desktop/src-tauri/src/managed_agents/runtime.rs index 31b31e00d0..8e4dfaa385 100644 --- a/desktop/src-tauri/src/managed_agents/runtime.rs +++ b/desktop/src-tauri/src/managed_agents/runtime.rs @@ -758,7 +758,14 @@ pub(crate) fn collect_same_instance_orphans( /// Binary names for the Buzz desktop/Tauri process. Used by dead-instance /// detection to confirm the owning desktop is still alive. -const DESKTOP_BINARY_NAMES: &[&str] = &["Buzz", "buzz-desktop", "buzz_desktop"]; +const DESKTOP_BINARY_NAMES: &[&str] = &[ + "Buzz", + "buzz-desktop", + "buzz_desktop", + // Linux limits /proc//comm to 15 visible bytes, truncating the + // AppImage shim's real executable name, `buzz-desktop.bin`. + "buzz-desktop.bi", +]; /// Check if a process name matches a known Buzz desktop binary. fn is_desktop_binary(name: &str) -> bool { diff --git a/desktop/src-tauri/src/managed_agents/runtime/tests.rs b/desktop/src-tauri/src/managed_agents/runtime/tests.rs index 06d3ac6db3..66c4fc113b 100644 --- a/desktop/src-tauri/src/managed_agents/runtime/tests.rs +++ b/desktop/src-tauri/src/managed_agents/runtime/tests.rs @@ -1,5 +1,12 @@ use crate::managed_agents::known_acp_runtime; +// ── desktop binary name tests ─────────────────────────────────────────── + +#[test] +fn appimage_binary_matches_truncated_linux_comm_name() { + assert!(super::is_desktop_binary("buzz-desktop.bi")); +} + // ── buffer_contains_identifier tests ──────────────────────────────────── #[test]