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) <noreply@anthropic.com>
This commit is contained in:
2026-10-06 11:52:10 +02:00
parent d0086fa38d
commit ba4c4d52b3
2 changed files with 157 additions and 6 deletions
+4
View File
@@ -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
+153 -6
View File
@@ -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<Item = (String, String)>,
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<String> {
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();