fix(single-instance): unconventional dbus names (fixes #3184) (#3194)

Co-authored-by: FabianLars <github@fabianlars.de>
Co-authored-by: Amr Bashir <github@amrbashir.me>
This commit is contained in:
Demir Yerli
2026-02-04 01:36:48 +03:00
committed by GitHub
parent 50b159f668
commit 98e2c11eef
9 changed files with 114 additions and 51 deletions
@@ -0,0 +1,9 @@
---
"single-instance": minor:fix
---
**Breaking Change:** On Linux, the DBus ID/name will now be `<bundle-id>.SingleInstance` instead of `org.<bundle_id_underscores>.SingleInstance` to follow DBus specifications.
This will break the single-instance mechanism across different app versions if the app was installed multiple times.
Added `dbus_id` builder method, which can be used to restore previous behavior. For a bundle identifier of `com.tauri.my-example` this would be `dbus_id("org.com_tauri_my_example")`.
+1
View File
@@ -21,6 +21,7 @@ target/
package-lock.json package-lock.json
yarn.lock yarn.lock
bun.lockb bun.lockb
bun.lock
# rust compiled folders # rust compiled folders
target/ target/
+4
View File
@@ -59,6 +59,10 @@ fn main() {
Note that currently, plugins run in the order they were added in to the builder, so make sure that this plugin is registered first. Note that currently, plugins run in the order they were added in to the builder, so make sure that this plugin is registered first.
## Usage with Flatpak/Snap
If you use Flatpak/Snap to publish your package and your Tauri identifier doesn't match the package id, set the `DBUS_ID` variable using the builder for the plugin, look at example.
## Contributing ## Contributing
PRs accepted. Please make sure to read the Contributing Guide before making a pull request. PRs accepted. Please make sure to read the Contributing Guide before making a pull request.
@@ -9,9 +9,14 @@
fn main() { fn main() {
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_single_instance::init(|app, argv, cwd| { .plugin(
println!("{}, {argv:?}, {cwd}", app.package_info().name); tauri_plugin_single_instance::Builder::new()
})) .callback(move |app, argv, cwd| {
println!("{}, {argv:?}, {cwd}", app.package_info().name);
})
.dbus_id("org.Tauri.SIExampleApp".to_owned())
.build(),
)
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }
+61 -8
View File
@@ -29,17 +29,70 @@ pub(crate) type SingleInstanceCallback<R> =
dyn FnMut(&AppHandle<R>, Vec<String>, String) + Send + Sync + 'static; dyn FnMut(&AppHandle<R>, Vec<String>, String) + Send + Sync + 'static;
pub fn init<R: Runtime, F: FnMut(&AppHandle<R>, Vec<String>, String) + Send + Sync + 'static>( pub fn init<R: Runtime, F: FnMut(&AppHandle<R>, Vec<String>, String) + Send + Sync + 'static>(
mut f: F, f: F,
) -> TauriPlugin<R> { ) -> TauriPlugin<R> {
platform_impl::init(Box::new(move |app, args, cwd| { Builder::new().callback(f).build()
#[cfg(feature = "deep-link")]
if let Some(deep_link) = app.try_state::<tauri_plugin_deep_link::DeepLink<R>>() {
deep_link.handle_cli_arguments(args.iter());
}
f(app, args, cwd)
}))
} }
pub fn destroy<R: Runtime, M: Manager<R>>(manager: &M) { pub fn destroy<R: Runtime, M: Manager<R>>(manager: &M) {
platform_impl::destroy(manager) platform_impl::destroy(manager)
} }
pub struct Builder<R: Runtime> {
callback: Box<SingleInstanceCallback<R>>,
dbus_id: Option<String>,
}
impl<R: Runtime> Default for Builder<R> {
fn default() -> Self {
Self {
callback: Box::new(move |_app, _args, _| {
#[cfg(feature = "deep-link")]
if let Some(deep_link) = _app.try_state::<tauri_plugin_deep_link::DeepLink<R>>() {
deep_link.handle_cli_arguments(_args.iter());
}
}),
dbus_id: None,
}
}
}
impl<R: Runtime> Builder<R> {
pub fn new() -> Self {
Default::default()
}
/// Function to call when a secondary instance was opened by the user and killed by the plugin.
/// If the `deep-link` feature is enabled, the plugin triggers the deep-link plugin before executing the callback.
pub fn callback<F: FnMut(&AppHandle<R>, Vec<String>, String) + Send + Sync + 'static>(
mut self,
mut f: F,
) -> Self {
self.callback = Box::new(move |app, args, cwd| {
#[cfg(feature = "deep-link")]
if let Some(deep_link) = app.try_state::<tauri_plugin_deep_link::DeepLink<R>>() {
deep_link.handle_cli_arguments(args.iter());
}
f(app, args, cwd)
});
self
}
/// Set a custom D-Bus ID, used on Linux. The plugin will append a `.SingleInstance` subname.
/// For example `com.mycompany.myapp` will result in the plugin registering its D-Bus service on `com.mycompany.myapp.SingleInstance`.
/// Usually you want the same base ID across all components in your app.
///
/// Defaults to the app's bundle identifier set in tauri.conf.json.
pub fn dbus_id(mut self, dbus_id: impl Into<String>) -> Self {
self.dbus_id = Some(dbus_id.into());
self
}
pub fn build(self) -> TauriPlugin<R> {
platform_impl::init(
self.callback,
#[cfg(target_os = "linux")]
self.dbus_id,
)
}
}
@@ -8,12 +8,9 @@ use crate::semver_compat::semver_compat_string;
use crate::SingleInstanceCallback; use crate::SingleInstanceCallback;
use tauri::{ use tauri::{
plugin::{self, TauriPlugin}, plugin::{self, TauriPlugin},
AppHandle, Config, Manager, RunEvent, Runtime, AppHandle, Manager, RunEvent, Runtime,
};
use zbus::{
blocking::{connection::Builder, Connection},
interface,
}; };
use zbus::{blocking::Connection, interface, names::WellKnownName};
struct ConnectionHandle(Connection); struct ConnectionHandle(Connection);
@@ -29,35 +26,31 @@ impl<R: Runtime> SingleInstanceDBus<R> {
} }
} }
#[cfg(feature = "semver")] struct DBusName(String);
fn dbus_id(config: &Config, version: semver::Version) -> String {
let mut id = config.identifier.replace(['.', '-'], "_");
id.push('_');
id.push_str(semver_compat_string(version).as_str());
id
}
#[cfg(not(feature = "semver"))] pub fn init<R: Runtime>(
fn dbus_id(config: &Config) -> String { callback: Box<SingleInstanceCallback<R>>,
config.identifier.replace(['.', '-'], "_") dbus_id: Option<String>,
} ) -> TauriPlugin<R> {
pub fn init<R: Runtime>(f: Box<SingleInstanceCallback<R>>) -> TauriPlugin<R> {
plugin::Builder::new("single-instance") plugin::Builder::new("single-instance")
.setup(|app, _api| { .setup(move |app, _api| {
let mut dbus_name = dbus_id.unwrap_or_else(|| app.config().identifier.clone());
dbus_name.push_str(".SingleInstance");
#[cfg(feature = "semver")] #[cfg(feature = "semver")]
let id = dbus_id(app.config(), app.package_info().version.clone()); {
#[cfg(not(feature = "semver"))] dbus_name.push('_');
let id = dbus_id(app.config()); dbus_name.push_str(semver_compat_string(&app.package_info().version).as_str());
}
let dbus_path = dbus_name.replace('.', "/");
let single_instance_dbus = SingleInstanceDBus { let single_instance_dbus = SingleInstanceDBus {
callback: f, callback,
app_handle: app.clone(), app_handle: app.clone(),
}; };
let dbus_name = format!("org.{id}.SingleInstance");
let dbus_path = format!("/org/{id}/SingleInstance");
match Builder::session() match zbus::blocking::connection::Builder::session()
.unwrap() .unwrap()
.name(dbus_name.as_str()) .name(dbus_name.as_str())
.unwrap() .unwrap()
@@ -92,9 +85,11 @@ pub fn init<R: Runtime>(f: Box<SingleInstanceCallback<R>>) -> TauriPlugin<R> {
_ => {} _ => {}
} }
app.manage(DBusName(dbus_name));
Ok(()) Ok(())
}) })
.on_event(|app, event| { .on_event(move |app, event| {
if let RunEvent::Exit = event { if let RunEvent::Exit = event {
destroy(app); destroy(app);
} }
@@ -104,15 +99,11 @@ pub fn init<R: Runtime>(f: Box<SingleInstanceCallback<R>>) -> TauriPlugin<R> {
pub fn destroy<R: Runtime, M: Manager<R>>(manager: &M) { pub fn destroy<R: Runtime, M: Manager<R>>(manager: &M) {
if let Some(connection) = manager.try_state::<ConnectionHandle>() { if let Some(connection) = manager.try_state::<ConnectionHandle>() {
#[cfg(feature = "semver")] if let Some(dbus_name) = manager
let id = dbus_id( .try_state::<DBusName>()
manager.config(), .and_then(|name| WellKnownName::try_from(name.0.clone()).ok())
manager.app_handle().package_info().version.clone(), {
); let _ = connection.0.release_name(dbus_name);
#[cfg(not(feature = "semver"))] }
let id = dbus_id(manager.config());
let dbus_name = format!("org.{id}.SingleInstance",);
let _ = connection.0.release_name(dbus_name);
} }
} }
@@ -63,7 +63,7 @@ fn socket_path(config: &Config, _package_info: &tauri::PackageInfo) -> PathBuf {
#[cfg(feature = "semver")] #[cfg(feature = "semver")]
let identifier = format!( let identifier = format!(
"{identifier}_{}", "{identifier}_{}",
semver_compat_string(_package_info.version.clone()), semver_compat_string(&_package_info.version),
); );
// Use /tmp as socket path must be shorter than 100 chars. // Use /tmp as socket path must be shorter than 100 chars.
@@ -59,7 +59,7 @@ pub fn init<R: Runtime>(callback: Box<SingleInstanceCallback<R>>) -> TauriPlugin
#[cfg(feature = "semver")] #[cfg(feature = "semver")]
{ {
id.push('_'); id.push('_');
id.push_str(semver_compat_string(app.package_info().version.clone()).as_str()); id.push_str(semver_compat_string(&app.package_info().version).as_str());
} }
let class_name = encode_wide(format!("{id}-sic")); let class_name = encode_wide(format!("{id}-sic"));
+1 -1
View File
@@ -4,7 +4,7 @@
/// Takes a version and spits out a String with trailing _x, thus only considering the digits /// Takes a version and spits out a String with trailing _x, thus only considering the digits
/// relevant regarding semver compatibility /// relevant regarding semver compatibility
pub fn semver_compat_string(version: semver::Version) -> String { pub fn semver_compat_string(version: &semver::Version) -> String {
// for pre-release always treat each version separately // for pre-release always treat each version separately
if !version.pre.is_empty() { if !version.pre.is_empty() {
return version.to_string().replace(['.', '-'], "_"); return version.to_string().replace(['.', '-'], "_");