Skip to content

Commit 98e2c11

Browse files
mrquantumoffFabianLarsamrbashir
authored
fix(single-instance): unconventional dbus names (fixes #3184) (#3194)
Co-authored-by: FabianLars <github@fabianlars.de> Co-authored-by: Amr Bashir <github@amrbashir.me>
1 parent 50b159f commit 98e2c11

9 files changed

Lines changed: 114 additions & 51 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"single-instance": minor:fix
3+
---
4+
5+
**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.
6+
7+
This will break the single-instance mechanism across different app versions if the app was installed multiple times.
8+
9+
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")`.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ target/
2121
package-lock.json
2222
yarn.lock
2323
bun.lockb
24+
bun.lock
2425

2526
# rust compiled folders
2627
target/

plugins/single-instance/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ fn main() {
5959

6060
Note that currently, plugins run in the order they were added in to the builder, so make sure that this plugin is registered first.
6161

62+
## Usage with Flatpak/Snap
63+
64+
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.
65+
6266
## Contributing
6367

6468
PRs accepted. Please make sure to read the Contributing Guide before making a pull request.

plugins/single-instance/examples/vanilla/src-tauri/src/main.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@
99

1010
fn main() {
1111
tauri::Builder::default()
12-
.plugin(tauri_plugin_single_instance::init(|app, argv, cwd| {
13-
println!("{}, {argv:?}, {cwd}", app.package_info().name);
14-
}))
12+
.plugin(
13+
tauri_plugin_single_instance::Builder::new()
14+
.callback(move |app, argv, cwd| {
15+
println!("{}, {argv:?}, {cwd}", app.package_info().name);
16+
})
17+
.dbus_id("org.Tauri.SIExampleApp".to_owned())
18+
.build(),
19+
)
1520
.run(tauri::generate_context!())
1621
.expect("error while running tauri application");
1722
}

plugins/single-instance/src/lib.rs

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,70 @@ pub(crate) type SingleInstanceCallback<R> =
2929
dyn FnMut(&AppHandle<R>, Vec<String>, String) + Send + Sync + 'static;
3030

3131
pub fn init<R: Runtime, F: FnMut(&AppHandle<R>, Vec<String>, String) + Send + Sync + 'static>(
32-
mut f: F,
32+
f: F,
3333
) -> TauriPlugin<R> {
34-
platform_impl::init(Box::new(move |app, args, cwd| {
35-
#[cfg(feature = "deep-link")]
36-
if let Some(deep_link) = app.try_state::<tauri_plugin_deep_link::DeepLink<R>>() {
37-
deep_link.handle_cli_arguments(args.iter());
38-
}
39-
f(app, args, cwd)
40-
}))
34+
Builder::new().callback(f).build()
4135
}
4236

4337
pub fn destroy<R: Runtime, M: Manager<R>>(manager: &M) {
4438
platform_impl::destroy(manager)
4539
}
40+
41+
pub struct Builder<R: Runtime> {
42+
callback: Box<SingleInstanceCallback<R>>,
43+
dbus_id: Option<String>,
44+
}
45+
46+
impl<R: Runtime> Default for Builder<R> {
47+
fn default() -> Self {
48+
Self {
49+
callback: Box::new(move |_app, _args, _| {
50+
#[cfg(feature = "deep-link")]
51+
if let Some(deep_link) = _app.try_state::<tauri_plugin_deep_link::DeepLink<R>>() {
52+
deep_link.handle_cli_arguments(_args.iter());
53+
}
54+
}),
55+
dbus_id: None,
56+
}
57+
}
58+
}
59+
60+
impl<R: Runtime> Builder<R> {
61+
pub fn new() -> Self {
62+
Default::default()
63+
}
64+
65+
/// Function to call when a secondary instance was opened by the user and killed by the plugin.
66+
/// If the `deep-link` feature is enabled, the plugin triggers the deep-link plugin before executing the callback.
67+
pub fn callback<F: FnMut(&AppHandle<R>, Vec<String>, String) + Send + Sync + 'static>(
68+
mut self,
69+
mut f: F,
70+
) -> Self {
71+
self.callback = Box::new(move |app, args, cwd| {
72+
#[cfg(feature = "deep-link")]
73+
if let Some(deep_link) = app.try_state::<tauri_plugin_deep_link::DeepLink<R>>() {
74+
deep_link.handle_cli_arguments(args.iter());
75+
}
76+
f(app, args, cwd)
77+
});
78+
self
79+
}
80+
81+
/// Set a custom D-Bus ID, used on Linux. The plugin will append a `.SingleInstance` subname.
82+
/// For example `com.mycompany.myapp` will result in the plugin registering its D-Bus service on `com.mycompany.myapp.SingleInstance`.
83+
/// Usually you want the same base ID across all components in your app.
84+
///
85+
/// Defaults to the app's bundle identifier set in tauri.conf.json.
86+
pub fn dbus_id(mut self, dbus_id: impl Into<String>) -> Self {
87+
self.dbus_id = Some(dbus_id.into());
88+
self
89+
}
90+
91+
pub fn build(self) -> TauriPlugin<R> {
92+
platform_impl::init(
93+
self.callback,
94+
#[cfg(target_os = "linux")]
95+
self.dbus_id,
96+
)
97+
}
98+
}

plugins/single-instance/src/platform_impl/linux.rs

Lines changed: 28 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,9 @@ use crate::semver_compat::semver_compat_string;
88
use crate::SingleInstanceCallback;
99
use tauri::{
1010
plugin::{self, TauriPlugin},
11-
AppHandle, Config, Manager, RunEvent, Runtime,
12-
};
13-
use zbus::{
14-
blocking::{connection::Builder, Connection},
15-
interface,
11+
AppHandle, Manager, RunEvent, Runtime,
1612
};
13+
use zbus::{blocking::Connection, interface, names::WellKnownName};
1714

1815
struct ConnectionHandle(Connection);
1916

@@ -29,35 +26,31 @@ impl<R: Runtime> SingleInstanceDBus<R> {
2926
}
3027
}
3128

32-
#[cfg(feature = "semver")]
33-
fn dbus_id(config: &Config, version: semver::Version) -> String {
34-
let mut id = config.identifier.replace(['.', '-'], "_");
35-
id.push('_');
36-
id.push_str(semver_compat_string(version).as_str());
37-
id
38-
}
39-
40-
#[cfg(not(feature = "semver"))]
41-
fn dbus_id(config: &Config) -> String {
42-
config.identifier.replace(['.', '-'], "_")
43-
}
29+
struct DBusName(String);
4430

45-
pub fn init<R: Runtime>(f: Box<SingleInstanceCallback<R>>) -> TauriPlugin<R> {
31+
pub fn init<R: Runtime>(
32+
callback: Box<SingleInstanceCallback<R>>,
33+
dbus_id: Option<String>,
34+
) -> TauriPlugin<R> {
4635
plugin::Builder::new("single-instance")
47-
.setup(|app, _api| {
36+
.setup(move |app, _api| {
37+
let mut dbus_name = dbus_id.unwrap_or_else(|| app.config().identifier.clone());
38+
dbus_name.push_str(".SingleInstance");
39+
4840
#[cfg(feature = "semver")]
49-
let id = dbus_id(app.config(), app.package_info().version.clone());
50-
#[cfg(not(feature = "semver"))]
51-
let id = dbus_id(app.config());
41+
{
42+
dbus_name.push('_');
43+
dbus_name.push_str(semver_compat_string(&app.package_info().version).as_str());
44+
}
45+
46+
let dbus_path = dbus_name.replace('.', "/");
5247

5348
let single_instance_dbus = SingleInstanceDBus {
54-
callback: f,
49+
callback,
5550
app_handle: app.clone(),
5651
};
57-
let dbus_name = format!("org.{id}.SingleInstance");
58-
let dbus_path = format!("/org/{id}/SingleInstance");
5952

60-
match Builder::session()
53+
match zbus::blocking::connection::Builder::session()
6154
.unwrap()
6255
.name(dbus_name.as_str())
6356
.unwrap()
@@ -92,9 +85,11 @@ pub fn init<R: Runtime>(f: Box<SingleInstanceCallback<R>>) -> TauriPlugin<R> {
9285
_ => {}
9386
}
9487

88+
app.manage(DBusName(dbus_name));
89+
9590
Ok(())
9691
})
97-
.on_event(|app, event| {
92+
.on_event(move |app, event| {
9893
if let RunEvent::Exit = event {
9994
destroy(app);
10095
}
@@ -104,15 +99,11 @@ pub fn init<R: Runtime>(f: Box<SingleInstanceCallback<R>>) -> TauriPlugin<R> {
10499

105100
pub fn destroy<R: Runtime, M: Manager<R>>(manager: &M) {
106101
if let Some(connection) = manager.try_state::<ConnectionHandle>() {
107-
#[cfg(feature = "semver")]
108-
let id = dbus_id(
109-
manager.config(),
110-
manager.app_handle().package_info().version.clone(),
111-
);
112-
#[cfg(not(feature = "semver"))]
113-
let id = dbus_id(manager.config());
114-
115-
let dbus_name = format!("org.{id}.SingleInstance",);
116-
let _ = connection.0.release_name(dbus_name);
102+
if let Some(dbus_name) = manager
103+
.try_state::<DBusName>()
104+
.and_then(|name| WellKnownName::try_from(name.0.clone()).ok())
105+
{
106+
let _ = connection.0.release_name(dbus_name);
107+
}
117108
}
118109
}

plugins/single-instance/src/platform_impl/macos.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ fn socket_path(config: &Config, _package_info: &tauri::PackageInfo) -> PathBuf {
6363
#[cfg(feature = "semver")]
6464
let identifier = format!(
6565
"{identifier}_{}",
66-
semver_compat_string(_package_info.version.clone()),
66+
semver_compat_string(&_package_info.version),
6767
);
6868

6969
// Use /tmp as socket path must be shorter than 100 chars.

plugins/single-instance/src/platform_impl/windows.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ pub fn init<R: Runtime>(callback: Box<SingleInstanceCallback<R>>) -> TauriPlugin
5959
#[cfg(feature = "semver")]
6060
{
6161
id.push('_');
62-
id.push_str(semver_compat_string(app.package_info().version.clone()).as_str());
62+
id.push_str(semver_compat_string(&app.package_info().version).as_str());
6363
}
6464

6565
let class_name = encode_wide(format!("{id}-sic"));

plugins/single-instance/src/semver_compat.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
/// Takes a version and spits out a String with trailing _x, thus only considering the digits
66
/// relevant regarding semver compatibility
7-
pub fn semver_compat_string(version: semver::Version) -> String {
7+
pub fn semver_compat_string(version: &semver::Version) -> String {
88
// for pre-release always treat each version separately
99
if !version.pre.is_empty() {
1010
return version.to_string().replace(['.', '-'], "_");

0 commit comments

Comments
 (0)