diff --git a/.changes/fix-unconventional-dbus-names.md b/.changes/fix-unconventional-dbus-names.md new file mode 100644 index 000000000..56b820d2e --- /dev/null +++ b/.changes/fix-unconventional-dbus-names.md @@ -0,0 +1,9 @@ +--- +"single-instance": minor:fix +--- + +**Breaking Change:** On Linux, the DBus ID/name will now be `.SingleInstance` instead of `org..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")`. \ No newline at end of file diff --git a/.gitignore b/.gitignore index 41022b01c..f5110d5c1 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ target/ package-lock.json yarn.lock bun.lockb +bun.lock # rust compiled folders target/ diff --git a/plugins/single-instance/README.md b/plugins/single-instance/README.md index 711be6ae1..e5f1fd8fb 100644 --- a/plugins/single-instance/README.md +++ b/plugins/single-instance/README.md @@ -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. +## 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 PRs accepted. Please make sure to read the Contributing Guide before making a pull request. diff --git a/plugins/single-instance/examples/vanilla/src-tauri/src/main.rs b/plugins/single-instance/examples/vanilla/src-tauri/src/main.rs index e736c4f3a..835857c99 100644 --- a/plugins/single-instance/examples/vanilla/src-tauri/src/main.rs +++ b/plugins/single-instance/examples/vanilla/src-tauri/src/main.rs @@ -9,9 +9,14 @@ fn main() { tauri::Builder::default() - .plugin(tauri_plugin_single_instance::init(|app, argv, cwd| { - println!("{}, {argv:?}, {cwd}", app.package_info().name); - })) + .plugin( + 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!()) .expect("error while running tauri application"); } diff --git a/plugins/single-instance/src/lib.rs b/plugins/single-instance/src/lib.rs index 03fd1ed7b..ad840caed 100644 --- a/plugins/single-instance/src/lib.rs +++ b/plugins/single-instance/src/lib.rs @@ -29,17 +29,70 @@ pub(crate) type SingleInstanceCallback = dyn FnMut(&AppHandle, Vec, String) + Send + Sync + 'static; pub fn init, Vec, String) + Send + Sync + 'static>( - mut f: F, + f: F, ) -> TauriPlugin { - platform_impl::init(Box::new(move |app, args, cwd| { - #[cfg(feature = "deep-link")] - if let Some(deep_link) = app.try_state::>() { - deep_link.handle_cli_arguments(args.iter()); - } - f(app, args, cwd) - })) + Builder::new().callback(f).build() } pub fn destroy>(manager: &M) { platform_impl::destroy(manager) } + +pub struct Builder { + callback: Box>, + dbus_id: Option, +} + +impl Default for Builder { + fn default() -> Self { + Self { + callback: Box::new(move |_app, _args, _| { + #[cfg(feature = "deep-link")] + if let Some(deep_link) = _app.try_state::>() { + deep_link.handle_cli_arguments(_args.iter()); + } + }), + dbus_id: None, + } + } +} + +impl Builder { + 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, Vec, 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::>() { + 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) -> Self { + self.dbus_id = Some(dbus_id.into()); + self + } + + pub fn build(self) -> TauriPlugin { + platform_impl::init( + self.callback, + #[cfg(target_os = "linux")] + self.dbus_id, + ) + } +} diff --git a/plugins/single-instance/src/platform_impl/linux.rs b/plugins/single-instance/src/platform_impl/linux.rs index 577965c53..593e34b50 100644 --- a/plugins/single-instance/src/platform_impl/linux.rs +++ b/plugins/single-instance/src/platform_impl/linux.rs @@ -8,12 +8,9 @@ use crate::semver_compat::semver_compat_string; use crate::SingleInstanceCallback; use tauri::{ plugin::{self, TauriPlugin}, - AppHandle, Config, Manager, RunEvent, Runtime, -}; -use zbus::{ - blocking::{connection::Builder, Connection}, - interface, + AppHandle, Manager, RunEvent, Runtime, }; +use zbus::{blocking::Connection, interface, names::WellKnownName}; struct ConnectionHandle(Connection); @@ -29,35 +26,31 @@ impl SingleInstanceDBus { } } -#[cfg(feature = "semver")] -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 -} +struct DBusName(String); -#[cfg(not(feature = "semver"))] -fn dbus_id(config: &Config) -> String { - config.identifier.replace(['.', '-'], "_") -} - -pub fn init(f: Box>) -> TauriPlugin { +pub fn init( + callback: Box>, + dbus_id: Option, +) -> TauriPlugin { 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")] - let id = dbus_id(app.config(), app.package_info().version.clone()); - #[cfg(not(feature = "semver"))] - let id = dbus_id(app.config()); + { + dbus_name.push('_'); + dbus_name.push_str(semver_compat_string(&app.package_info().version).as_str()); + } + + let dbus_path = dbus_name.replace('.', "/"); let single_instance_dbus = SingleInstanceDBus { - callback: f, + callback, 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() .name(dbus_name.as_str()) .unwrap() @@ -92,9 +85,11 @@ pub fn init(f: Box>) -> TauriPlugin { _ => {} } + app.manage(DBusName(dbus_name)); + Ok(()) }) - .on_event(|app, event| { + .on_event(move |app, event| { if let RunEvent::Exit = event { destroy(app); } @@ -104,15 +99,11 @@ pub fn init(f: Box>) -> TauriPlugin { pub fn destroy>(manager: &M) { if let Some(connection) = manager.try_state::() { - #[cfg(feature = "semver")] - let id = dbus_id( - manager.config(), - manager.app_handle().package_info().version.clone(), - ); - #[cfg(not(feature = "semver"))] - let id = dbus_id(manager.config()); - - let dbus_name = format!("org.{id}.SingleInstance",); - let _ = connection.0.release_name(dbus_name); + if let Some(dbus_name) = manager + .try_state::() + .and_then(|name| WellKnownName::try_from(name.0.clone()).ok()) + { + let _ = connection.0.release_name(dbus_name); + } } } diff --git a/plugins/single-instance/src/platform_impl/macos.rs b/plugins/single-instance/src/platform_impl/macos.rs index 639915136..a2df9092e 100644 --- a/plugins/single-instance/src/platform_impl/macos.rs +++ b/plugins/single-instance/src/platform_impl/macos.rs @@ -63,7 +63,7 @@ fn socket_path(config: &Config, _package_info: &tauri::PackageInfo) -> PathBuf { #[cfg(feature = "semver")] let identifier = format!( "{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. diff --git a/plugins/single-instance/src/platform_impl/windows.rs b/plugins/single-instance/src/platform_impl/windows.rs index 832d08034..d55ec079c 100644 --- a/plugins/single-instance/src/platform_impl/windows.rs +++ b/plugins/single-instance/src/platform_impl/windows.rs @@ -59,7 +59,7 @@ pub fn init(callback: Box>) -> TauriPlugin #[cfg(feature = "semver")] { 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")); diff --git a/plugins/single-instance/src/semver_compat.rs b/plugins/single-instance/src/semver_compat.rs index 80487cae6..2920f1b79 100644 --- a/plugins/single-instance/src/semver_compat.rs +++ b/plugins/single-instance/src/semver_compat.rs @@ -4,7 +4,7 @@ /// Takes a version and spits out a String with trailing _x, thus only considering the digits /// 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 if !version.pre.is_empty() { return version.to_string().replace(['.', '-'], "_");