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:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user