From ba4c4d52b3f884dc3f3e5e852e81e52b9b5d853d Mon Sep 17 00:00:00 2001 From: Schalli Date: Tue, 6 Oct 2026 11:52:10 +0200 Subject: [PATCH] fix(desktop): Linux-AppImage oeffnet Links ueber das xdg-open des Systems tauri-plugin-opener rief das xdg-open aus dem AppImage mit dessen Umgebung (LD_LIBRARY_PATH, GTK_PATH, GIO_EXTRA_MODULES, GDK_BACKEND ...) auf; das Oeffnen-Programm der Arbeitsoberflaeche erbt das. Im AppImage jetzt das System-xdg-open mit bereinigter Umgebung, sonst wie bisher der Opener. Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 4 + apps/desktop/src-tauri/src/lib.rs | 159 ++++++++++++++++++++++++++++-- 2 files changed, 157 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 537937c..1b4411d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ Diese Liste beschreibt in einfachen Worten, was sich von Version zu Version an T ## Unveröffentlicht +### Behoben + +- Linux-App: Links, die in einem neuen Fenster aufgehen (zum Beispiel aus Notizen oder Favoriten), übergibt die App jetzt an das Öffnen-Programm Ihres Systems statt an eine mitgebrachte Kopie. Auf manchen Systemen öffnete sich dadurch vorher kein Browser. + ## 1.10.0 – 2026-10-06 ### Neu diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index e98c442..9e079d1 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -776,7 +776,7 @@ fn spawn_version_check(app: AppHandle, server_url: String, install_after: bool) fn open_download_page(app: &AppHandle) { if let Some(server) = stored_server_url(app) { let target = format!("{}/settings/general/desktop", server.trim_end_matches('/')); - let _ = app.opener().open_url(target, None::<&str>); + open_in_system_browser(app, target); } } @@ -940,6 +940,111 @@ fn get_client_info() -> String { /// Ziel fuer den System-Browser, wenn die Seite ein neues Fenster verlangt /// (`window.open`, Links mit `target="_blank"`). Nur http/https -- andere /// Schemata (file:, javascript:, eigene Protokolle) werden verworfen. +/// Oeffnet eine Adresse im System-Browser. +/// +/// Linux-AppImage (06.10.2026, Link aus der Notiz-Kachel oeffnete auf +/// EndeavourOS nichts): `tauri-plugin-opener` ruft dort das `xdg-open` +/// AUS dem AppImage auf, und zwar mit der Umgebung des AppImage +/// (`LD_LIBRARY_PATH` auf die mitgebrachten Debian-Bibliotheken, `GTK_PATH`, +/// `GIO_EXTRA_MODULES`, `GDK_BACKEND=x11` ...). Das Oeffnen-Programm der +/// Arbeitsoberflaeche bzw. der Browser erbt das und kann still scheitern. +/// Nachgewiesen per strace im Container: Aufruf von +/// `$APPDIR/usr/bin/xdg-open` mit 33 geerbten Variablen. Deshalb hier im +/// AppImage das `xdg-open` des SYSTEMS mit bereinigter Umgebung; gelingt +/// das nicht (kein System-`xdg-open`), wie bisher ueber den Opener. +fn open_in_system_browser(app: &AppHandle, url: String) { + #[cfg(target_os = "linux")] + { + if appimage_system_open(&url) { + return; + } + } + let _ = app.opener().open_url(url, None::<&str>); +} + +/// Variablen, die der AppRun-Hook von linuxdeploy (plugin-gtk) bzw. die +/// AppImage-Laufzeit setzt -- fuer Programme ausserhalb der App falsch. +#[cfg(target_os = "linux")] +const APPIMAGE_ONLY_VARS: &[&str] = &[ + "LD_LIBRARY_PATH", + "LD_PRELOAD", + "GTK_DATA_PREFIX", + "GTK_THEME", + "GTK_EXE_PREFIX", + "GTK_PATH", + "GTK_IM_MODULE_FILE", + "GDK_PIXBUF_MODULE_FILE", + "GDK_BACKEND", + "GIO_EXTRA_MODULES", + "GSETTINGS_SCHEMA_DIR", + "GST_PLUGIN_PATH", + "GST_PLUGIN_SYSTEM_PATH", + "GST_PLUGIN_SYSTEM_PATH_1_0", + "GST_PLUGIN_SCANNER", + "APPDIR", + "APPIMAGE", + "ARGV0", + "OWD", +]; + +/// Bereinigte Umgebung fuer ein Programm ausserhalb des AppImage: die +/// AppImage-eigenen Variablen fallen weg, aus Pfadlisten (PATH, +/// XDG_DATA_DIRS ...) verschwinden alle Eintraege unterhalb von `appdir`. +/// Rein, damit testbar. +#[cfg(target_os = "linux")] +fn cleaned_env( + vars: impl Iterator, + appdir: &str, +) -> Vec<(String, String)> { + let appdir = appdir.trim_end_matches('/'); + vars.filter(|(k, _)| !APPIMAGE_ONLY_VARS.contains(&k.as_str())) + .filter_map(|(k, v)| { + if appdir.is_empty() || !v.contains(appdir) { + return Some((k, v)); + } + let kept: Vec<&str> = v + .split(':') + .filter(|part| !part.is_empty() && !part.starts_with(appdir)) + .collect(); + if kept.is_empty() { + None + } else { + Some((k, kept.join(":"))) + } + }) + .collect() +} + +#[cfg(target_os = "linux")] +fn appimage_system_open(url: &str) -> bool { + let Ok(appdir) = std::env::var("APPDIR") else { + return false; // kein AppImage (z. B. Entwicklungsstart): Opener genuegt + }; + let Some(bin) = [ + "/usr/bin/xdg-open", + "/usr/local/bin/xdg-open", + "/bin/xdg-open", + ] + .into_iter() + .find(|p| std::path::Path::new(p).exists()) else { + return false; + }; + let mut cmd = std::process::Command::new(bin); + cmd.arg(url) + .env_clear() + .envs(cleaned_env(std::env::vars(), &appdir)); + match cmd.spawn() { + Ok(mut child) => { + // Prozess abholen, sonst bleibt er bis zum App-Ende als Zombie. + std::thread::spawn(move || { + let _ = child.wait(); + }); + true + } + Err(_) => false, + } +} + fn external_target(url: &tauri::Url) -> Option { match url.scheme() { "http" | "https" => Some(url.to_string()), @@ -1047,7 +1152,7 @@ pub fn run() { let handle = app.handle().clone(); move |url, _features| { if let Some(target) = external_target(&url) { - let _ = handle.opener().open_url(target, None::<&str>); + open_in_system_browser(&handle, target); } tauri::webview::NewWindowResponse::Deny } @@ -1062,10 +1167,7 @@ pub fn run() { if saves_in_app(&url) { return true; } - let _ = webview - .app_handle() - .opener() - .open_url(url.to_string(), None::<&str>); + open_in_system_browser(webview.app_handle(), url.to_string()); false } tauri::webview::DownloadEvent::Finished { url, path, success } => { @@ -1591,6 +1693,51 @@ mod tests { assert_eq!(body, "Die Datei wurde gespeichert."); } + #[test] + #[cfg(target_os = "linux")] + fn cleaned_env_entfernt_appimage_umgebung() { + let appdir = "/tmp/.mount_TesseraAbc"; + let vars = vec![ + ("HOME".to_string(), "/home/u".to_string()), + ("LD_LIBRARY_PATH".to_string(), format!("{appdir}/usr/lib/")), + ("GDK_BACKEND".to_string(), "x11".to_string()), + ("APPDIR".to_string(), appdir.to_string()), + ( + "PATH".to_string(), + format!("{appdir}/usr/bin/:/usr/local/bin:/usr/bin"), + ), + ( + "XDG_DATA_DIRS".to_string(), + format!("{appdir}/usr/share:/usr/share:/usr/local/share"), + ), + ( + "GSETTINGS_SCHEMA_DIR".to_string(), + format!("{appdir}//usr/share/glib-2.0/schemas"), + ), + ("ONLY_APP".to_string(), format!("{appdir}/x")), + ]; + let env: std::collections::HashMap<_, _> = + cleaned_env(vars.into_iter(), appdir).into_iter().collect(); + assert_eq!(env.get("HOME").map(String::as_str), Some("/home/u")); + assert_eq!( + env.get("PATH").map(String::as_str), + Some("/usr/local/bin:/usr/bin") + ); + assert_eq!( + env.get("XDG_DATA_DIRS").map(String::as_str), + Some("/usr/share:/usr/local/share") + ); + for gone in [ + "LD_LIBRARY_PATH", + "GDK_BACKEND", + "APPDIR", + "GSETTINGS_SCHEMA_DIR", + "ONLY_APP", + ] { + assert!(!env.contains_key(gone), "{gone}"); + } + } + #[test] fn external_target_nur_http_und_https() { let https = tauri::Url::parse("https://www.google.com/search?q=tessera").unwrap();