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
|
## 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
|
## 1.10.0 – 2026-10-06
|
||||||
|
|
||||||
### Neu
|
### Neu
|
||||||
|
|||||||
@@ -776,7 +776,7 @@ fn spawn_version_check(app: AppHandle, server_url: String, install_after: bool)
|
|||||||
fn open_download_page(app: &AppHandle) {
|
fn open_download_page(app: &AppHandle) {
|
||||||
if let Some(server) = stored_server_url(app) {
|
if let Some(server) = stored_server_url(app) {
|
||||||
let target = format!("{}/settings/general/desktop", server.trim_end_matches('/'));
|
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
|
/// Ziel fuer den System-Browser, wenn die Seite ein neues Fenster verlangt
|
||||||
/// (`window.open`, Links mit `target="_blank"`). Nur http/https -- andere
|
/// (`window.open`, Links mit `target="_blank"`). Nur http/https -- andere
|
||||||
/// Schemata (file:, javascript:, eigene Protokolle) werden verworfen.
|
/// 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> {
|
fn external_target(url: &tauri::Url) -> Option<String> {
|
||||||
match url.scheme() {
|
match url.scheme() {
|
||||||
"http" | "https" => Some(url.to_string()),
|
"http" | "https" => Some(url.to_string()),
|
||||||
@@ -1047,7 +1152,7 @@ pub fn run() {
|
|||||||
let handle = app.handle().clone();
|
let handle = app.handle().clone();
|
||||||
move |url, _features| {
|
move |url, _features| {
|
||||||
if let Some(target) = external_target(&url) {
|
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
|
tauri::webview::NewWindowResponse::Deny
|
||||||
}
|
}
|
||||||
@@ -1062,10 +1167,7 @@ pub fn run() {
|
|||||||
if saves_in_app(&url) {
|
if saves_in_app(&url) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
let _ = webview
|
open_in_system_browser(webview.app_handle(), url.to_string());
|
||||||
.app_handle()
|
|
||||||
.opener()
|
|
||||||
.open_url(url.to_string(), None::<&str>);
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
tauri::webview::DownloadEvent::Finished { url, path, success } => {
|
tauri::webview::DownloadEvent::Finished { url, path, success } => {
|
||||||
@@ -1591,6 +1693,51 @@ mod tests {
|
|||||||
assert_eq!(body, "Die Datei wurde gespeichert.");
|
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]
|
#[test]
|
||||||
fn external_target_nur_http_und_https() {
|
fn external_target_nur_http_und_https() {
|
||||||
let https = tauri::Url::parse("https://www.google.com/search?q=tessera").unwrap();
|
let https = tauri::Url::parse("https://www.google.com/search?q=tessera").unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user