From 42a21bd866ab14991783c8f92629980a4ac7ae44 Mon Sep 17 00:00:00 2001 From: JGuest121 Date: Mon, 13 Jul 2026 14:16:47 +0100 Subject: [PATCH 1/3] add configurable destroy_on_exit to single instance plugin --- plugins/single-instance/README.md | 32 +++++ plugins/single-instance/src/lib.rs | 8 ++ .../src/platform_impl/linux.rs | 118 +++++++++--------- .../src/platform_impl/macos.rs | 58 +++++---- .../src/platform_impl/windows.rs | 101 ++++++++------- 5 files changed, 186 insertions(+), 131 deletions(-) diff --git a/plugins/single-instance/README.md b/plugins/single-instance/README.md index e5f1fd8fbb..46b282a65d 100644 --- a/plugins/single-instance/README.md +++ b/plugins/single-instance/README.md @@ -59,6 +59,38 @@ 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. +By default the plugin will clean itself up on exit. If you need precise control of the cleanup process, you can disable the default behavior: + +```rust +use tauri::{Manager}; + +#[derive(Clone, serde::Serialize)] +struct Payload { + args: Vec, + cwd: String, +} + +fn main() { + tauri::Builder::default() + .plugin( + tauri_plugin_single_instance::Builder::new() + .callback(|app, argv, cwd| { + println!("{}, {argv:?}, {cwd}", app.package_info().name); + app.emit("single-instance", Payload { args: argv, cwd }).unwrap(); + }) + .destroy_on_exit(false) + .build(), + ) + .run(|app, event| { + if let tauri::RunEvent::Exit = event { + // Make sure to clean up when we're done + tauri_plugin_single_instance::destroy(app); + } + }) + .expect("error while running tauri application"); +} +``` + ## 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. diff --git a/plugins/single-instance/src/lib.rs b/plugins/single-instance/src/lib.rs index ad840caeda..de7b460c46 100644 --- a/plugins/single-instance/src/lib.rs +++ b/plugins/single-instance/src/lib.rs @@ -40,6 +40,7 @@ pub fn destroy>(manager: &M) { pub struct Builder { callback: Box>, + destroy_on_exit: bool, dbus_id: Option, } @@ -52,6 +53,7 @@ impl Default for Builder { deep_link.handle_cli_arguments(_args.iter()); } }), + destroy_on_exit: true, dbus_id: None, } } @@ -78,6 +80,11 @@ impl Builder { self } + pub fn destroy_on_exit(mut self, val: bool) -> Self { + self.destroy_on_exit = val; + 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. @@ -91,6 +98,7 @@ impl Builder { pub fn build(self) -> TauriPlugin { platform_impl::init( self.callback, + self.destroy_on_exit, #[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 892baee7fe..b75682e4bd 100644 --- a/plugins/single-instance/src/platform_impl/linux.rs +++ b/plugins/single-instance/src/platform_impl/linux.rs @@ -30,74 +30,78 @@ struct DBusName(String); pub fn init( callback: Box>, + destroy_on_exit: bool, dbus_id: Option, ) -> TauriPlugin { - plugin::Builder::new("single-instance") - .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")] - { - dbus_name.push('_'); - dbus_name.push_str(semver_compat_string(&app.package_info().version).as_str()); - } + let mut builder = plugin::Builder::new("single-instance").setup(move |app, _api| { + let mut dbus_name = dbus_id.unwrap_or_else(|| app.config().identifier.clone()); + dbus_name.push_str(".SingleInstance"); - let mut dbus_path = dbus_name.replace('.', "/").replace('-', "_"); - if !dbus_path.starts_with('/') { - dbus_path = format!("/{dbus_path}"); - } + #[cfg(feature = "semver")] + { + dbus_name.push('_'); + dbus_name.push_str(semver_compat_string(&app.package_info().version).as_str()); + } - let single_instance_dbus = SingleInstanceDBus { - callback, - app_handle: app.clone(), - }; - - match zbus::blocking::connection::Builder::session() - .unwrap() - .name(dbus_name.as_str()) - .unwrap() - .replace_existing_names(false) - .allow_name_replacements(false) - .serve_at(dbus_path.as_str(), single_instance_dbus) - .unwrap() - .build() - { - Ok(connection) => { - app.manage(ConnectionHandle(connection)); - } - Err(zbus::Error::NameTaken) => { - if let Ok(connection) = Connection::session() { - let _ = connection.call_method( - Some(dbus_name.as_str()), - dbus_path.as_str(), - Some("org.SingleInstance.DBus"), - "ExecuteCallback", - &( - std::env::args().collect::>(), - std::env::current_dir() - .unwrap_or_default() - .to_str() - .unwrap_or_default(), - ), - ); - } - app.cleanup_before_exit(); - std::process::exit(0); + let mut dbus_path = dbus_name.replace('.', "/").replace('-', "_"); + if !dbus_path.starts_with('/') { + dbus_path = format!("/{dbus_path}"); + } + + let single_instance_dbus = SingleInstanceDBus { + callback, + app_handle: app.clone(), + }; + + match zbus::blocking::connection::Builder::session() + .unwrap() + .name(dbus_name.as_str()) + .unwrap() + .replace_existing_names(false) + .allow_name_replacements(false) + .serve_at(dbus_path.as_str(), single_instance_dbus) + .unwrap() + .build() + { + Ok(connection) => { + app.manage(ConnectionHandle(connection)); + } + Err(zbus::Error::NameTaken) => { + if let Ok(connection) = Connection::session() { + let _ = connection.call_method( + Some(dbus_name.as_str()), + dbus_path.as_str(), + Some("org.SingleInstance.DBus"), + "ExecuteCallback", + &( + std::env::args().collect::>(), + std::env::current_dir() + .unwrap_or_default() + .to_str() + .unwrap_or_default(), + ), + ); } - _ => {} + app.cleanup_before_exit(); + std::process::exit(0); } + _ => {} + } - app.manage(DBusName(dbus_name)); + app.manage(DBusName(dbus_name)); - Ok(()) - }) - .on_event(move |app, event| { + Ok(()) + }); + + if destroy_on_exit { + builder = builder.on_event(move |app, event| { if let RunEvent::Exit = event { destroy(app); } - }) - .build() + }); + } + + builder.build() } pub fn destroy>(manager: &M) { diff --git a/plugins/single-instance/src/platform_impl/macos.rs b/plugins/single-instance/src/platform_impl/macos.rs index bd2c74a7e3..7a8b7c2ef3 100644 --- a/plugins/single-instance/src/platform_impl/macos.rs +++ b/plugins/single-instance/src/platform_impl/macos.rs @@ -17,40 +17,46 @@ use tauri::{ }; use tokio::io::AsyncReadExt; -pub fn init(cb: Box>) -> TauriPlugin { - plugin::Builder::new("single-instance") - .setup(|app, _api| { - let socket = socket_path(app.config(), app.package_info()); +pub fn init( + cb: Box>, + destroy_on_exit: bool, +) -> TauriPlugin { + let mut builder = plugin::Builder::new("single-instance").setup(|app, _api| { + let socket = socket_path(app.config(), app.package_info()); - // Notify the singleton which may or may not exist. - match notify_singleton(&socket) { - Ok(_) => { - std::process::exit(0); - } - Err(e) => { - match e.kind() { - ErrorKind::NotFound | ErrorKind::ConnectionRefused => { - // This process claims itself as singleton as likely none exists - socket_cleanup(&socket); - listen_for_other_instances(socket, app.clone(), cb); - } - _ => { - tracing::debug!( - "single_instance failed to notify - launching normally: {}", - e - ); - } + // Notify the singleton which may or may not exist. + match notify_singleton(&socket) { + Ok(_) => { + std::process::exit(0); + } + Err(e) => { + match e.kind() { + ErrorKind::NotFound | ErrorKind::ConnectionRefused => { + // This process claims itself as singleton as likely none exists + socket_cleanup(&socket); + listen_for_other_instances(socket, app.clone(), cb); + } + _ => { + tracing::debug!( + "single_instance failed to notify - launching normally: {}", + e + ); } } } - Ok(()) - }) - .on_event(|app, event| { + } + Ok(()) + }); + + if destroy_on_exit { + builder = builder.on_event(|app, event| { if let RunEvent::Exit = event { destroy(app); } }) - .build() + } + + builder.build() } pub fn destroy>(manager: &M) { diff --git a/plugins/single-instance/src/platform_impl/windows.rs b/plugins/single-instance/src/platform_impl/windows.rs index d55ec079c6..4b5e44efd6 100644 --- a/plugins/single-instance/src/platform_impl/windows.rs +++ b/plugins/single-instance/src/platform_impl/windows.rs @@ -51,69 +51,74 @@ impl UserData { } } -pub fn init(callback: Box>) -> TauriPlugin { - plugin::Builder::new("single-instance") - .setup(|app, _api| { - #[allow(unused_mut)] - let mut id = app.config().identifier.clone(); - #[cfg(feature = "semver")] - { - id.push('_'); - id.push_str(semver_compat_string(&app.package_info().version).as_str()); - } +pub fn init( + callback: Box>, + destroy_on_exit: bool, +) -> TauriPlugin { + let mut builder = plugin::Builder::new("single-instance").setup(|app, _api| { + #[allow(unused_mut)] + let mut id = app.config().identifier.clone(); + #[cfg(feature = "semver")] + { + id.push('_'); + id.push_str(semver_compat_string(&app.package_info().version).as_str()); + } - let class_name = encode_wide(format!("{id}-sic")); - let window_name = encode_wide(format!("{id}-siw")); - let mutex_name = encode_wide(format!("{id}-sim")); + let class_name = encode_wide(format!("{id}-sic")); + let window_name = encode_wide(format!("{id}-siw")); + let mutex_name = encode_wide(format!("{id}-sim")); - let hmutex = - unsafe { CreateMutexW(std::ptr::null(), true.into(), mutex_name.as_ptr()) }; + let hmutex = unsafe { CreateMutexW(std::ptr::null(), true.into(), mutex_name.as_ptr()) }; - if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS { - unsafe { - let hwnd = FindWindowW(class_name.as_ptr(), window_name.as_ptr()); + if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS { + unsafe { + let hwnd = FindWindowW(class_name.as_ptr(), window_name.as_ptr()); - if !hwnd.is_null() { - let cwd = std::env::current_dir().unwrap_or_default(); - let cwd = cwd.to_str().unwrap_or_default(); + if !hwnd.is_null() { + let cwd = std::env::current_dir().unwrap_or_default(); + let cwd = cwd.to_str().unwrap_or_default(); - let args = std::env::args().collect::>().join("|"); + let args = std::env::args().collect::>().join("|"); - let data = format!("{cwd}|{args}\0",); + let data = format!("{cwd}|{args}\0",); - let bytes = data.as_bytes(); - let cds = COPYDATASTRUCT { - dwData: WMCOPYDATA_SINGLE_INSTANCE_DATA, - cbData: bytes.len() as _, - lpData: bytes.as_ptr() as _, - }; + let bytes = data.as_bytes(); + let cds = COPYDATASTRUCT { + dwData: WMCOPYDATA_SINGLE_INSTANCE_DATA, + cbData: bytes.len() as _, + lpData: bytes.as_ptr() as _, + }; - SendMessageW(hwnd, WM_COPYDATA, 0, &cds as *const _ as _); + SendMessageW(hwnd, WM_COPYDATA, 0, &cds as *const _ as _); - app.cleanup_before_exit(); - std::process::exit(0); - } + app.cleanup_before_exit(); + std::process::exit(0); } - } else { - app.manage(MutexHandle(hmutex as _)); - - let userdata = UserData { - app: app.clone(), - callback, - }; - let userdata = Box::into_raw(Box::new(userdata)); - let hwnd = create_event_target_window::(&class_name, &window_name, userdata); - app.manage(TargetWindowHandle(hwnd as _)); } + } else { + app.manage(MutexHandle(hmutex as _)); + + let userdata = UserData { + app: app.clone(), + callback, + }; + let userdata = Box::into_raw(Box::new(userdata)); + let hwnd = create_event_target_window::(&class_name, &window_name, userdata); + app.manage(TargetWindowHandle(hwnd as _)); + } - Ok(()) - }) - .on_event(|app, event| { + Ok(()) + }); + + if destroy_on_exit { + builder = builder.on_event(|app, event| { if let RunEvent::Exit = event { destroy(app); } - }) - .build() + }); + } + + builder.build() } pub fn destroy>(manager: &M) { From 645d6fe84bc2dde2aea7497133b32626de9bab31 Mon Sep 17 00:00:00 2001 From: JGuest121 Date: Mon, 13 Jul 2026 16:34:06 +0100 Subject: [PATCH 2/3] add change file --- .changes/single-instance-destroy-on-exit-opt-out.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changes/single-instance-destroy-on-exit-opt-out.md diff --git a/.changes/single-instance-destroy-on-exit-opt-out.md b/.changes/single-instance-destroy-on-exit-opt-out.md new file mode 100644 index 0000000000..900c180775 --- /dev/null +++ b/.changes/single-instance-destroy-on-exit-opt-out.md @@ -0,0 +1,6 @@ +--- +"log": minor:feat +"log-js": minor +--- + +Extend single-instance plugin with `destroy_on_exit` flag to allow consumers to opt-out out of automatically destroying the plugin on `tauri::RunEvent::Exit` as some consumers may prefer to defer releasing the single-instance lock until after application-specific cleanup has been performed. From e68831aa6022f6af292be8a02db738a4951f3a20 Mon Sep 17 00:00:00 2001 From: JGuest121 Date: Mon, 13 Jul 2026 16:38:15 +0100 Subject: [PATCH 3/3] document destroy_on_exit --- plugins/single-instance/src/lib.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plugins/single-instance/src/lib.rs b/plugins/single-instance/src/lib.rs index de7b460c46..e1faecf80d 100644 --- a/plugins/single-instance/src/lib.rs +++ b/plugins/single-instance/src/lib.rs @@ -80,6 +80,13 @@ impl Builder { self } + /// Set whether the plugin should destroy the single instance lock on app exit. + /// Set to `false` if you want precise control over when the plugin is destroyed + /// and intend to call [`destroy`] manually. This is useful if you want to continue + /// to ensure only a single instance of your app is running while performing some + /// long-running cleanup tasks on app exit. + /// + /// Defaults to `true`. pub fn destroy_on_exit(mut self, val: bool) -> Self { self.destroy_on_exit = val; self