diff --git a/.changes/setup-single-instance-manually.md b/.changes/setup-single-instance-manually.md new file mode 100644 index 000000000..3036a2a92 --- /dev/null +++ b/.changes/setup-single-instance-manually.md @@ -0,0 +1,5 @@ +--- +"single-instance": patch +--- + +Add `setup` function to run the single instance initialization manually, without waiting for the tauri setup hook to run. diff --git a/plugins/single-instance/src/lib.rs b/plugins/single-instance/src/lib.rs index ad840caed..26e7f33d9 100644 --- a/plugins/single-instance/src/lib.rs +++ b/plugins/single-instance/src/lib.rs @@ -10,7 +10,7 @@ )] #![cfg(not(any(target_os = "android", target_os = "ios")))] -use tauri::{plugin::TauriPlugin, AppHandle, Manager, Runtime}; +use tauri::{plugin, plugin::TauriPlugin, AppHandle, Manager, RunEvent, Runtime}; #[cfg(target_os = "windows")] #[path = "platform_impl/windows.rs"] @@ -34,6 +34,17 @@ pub fn init, Vec, String) + Send + Sy Builder::new().callback(f).build() } +/// Runs the single-instance setup flow with the given app and callback. +/// Use this when you need to run single-instance from your own plugin or app setup. +/// On Linux, pass `dbus_id` (e.g. `Some("com.mycompany.myapp".into())`); on other platforms it is ignored. +pub fn setup, Vec, String) + Send + Sync + 'static>( + app: &AppHandle, + callback: F, + dbus_id: Option, +) -> Result<(), ()> { + platform_impl::setup_single_instance(app, Box::new(callback), dbus_id) +} + pub fn destroy>(manager: &M) { platform_impl::destroy(manager) } @@ -89,10 +100,18 @@ impl Builder { } pub fn build(self) -> TauriPlugin { - platform_impl::init( - self.callback, - #[cfg(target_os = "linux")] - self.dbus_id, - ) + let callback = self.callback; + let dbus_id = self.dbus_id; + plugin::Builder::new("single-instance") + .setup(move |app, _api| { + let _ = platform_impl::setup_single_instance(app, callback, dbus_id); + Ok(()) + }) + .on_event(|app, event| { + if let RunEvent::Exit = event { + destroy(app); + } + }) + .build() } } diff --git a/plugins/single-instance/src/platform_impl/linux.rs b/plugins/single-instance/src/platform_impl/linux.rs index 593e34b50..a929fa170 100644 --- a/plugins/single-instance/src/platform_impl/linux.rs +++ b/plugins/single-instance/src/platform_impl/linux.rs @@ -6,10 +6,7 @@ use crate::semver_compat::semver_compat_string; use crate::SingleInstanceCallback; -use tauri::{ - plugin::{self, TauriPlugin}, - AppHandle, Manager, RunEvent, Runtime, -}; +use tauri::{AppHandle, Manager, Runtime}; use zbus::{blocking::Connection, interface, names::WellKnownName}; struct ConnectionHandle(Connection); @@ -28,73 +25,68 @@ impl SingleInstanceDBus { struct DBusName(String); -pub fn init( +pub fn setup_single_instance( + app: &AppHandle, callback: Box>, 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"); +) -> Result<(), ()> { + let mut dbus_name = dbus_id.unwrap_or_else(|| app.config().identifier.clone()); - #[cfg(feature = "semver")] - { - dbus_name.push('_'); - dbus_name.push_str(semver_compat_string(&app.package_info().version).as_str()); - } + #[cfg(feature = "semver")] + { + dbus_name.push('_'); + dbus_name.push_str(semver_compat_string(&app.package_info().version).as_str()); + } + dbus_name.push_str(".SingleInstance"); - let dbus_path = dbus_name.replace('.', "/"); + 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(), - }; + 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)); - - Ok(()) - }) - .on_event(move |app, event| { - if let RunEvent::Exit = event { - destroy(app); - } - }) + 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)); + + Ok(()) } 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 a2df9092e..e2ec2c93c 100644 --- a/plugins/single-instance/src/platform_impl/macos.rs +++ b/plugins/single-instance/src/platform_impl/macos.rs @@ -11,45 +11,37 @@ use std::{ #[cfg(feature = "semver")] use crate::semver_compat::semver_compat_string; use crate::SingleInstanceCallback; -use tauri::{ - plugin::{self, TauriPlugin}, - AppHandle, Config, Manager, RunEvent, Runtime, -}; +use tauri::{AppHandle, Config, Manager, Runtime}; -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 setup_single_instance( + app: &AppHandle, + cb: Box>, + _dbus_id: Option, +) -> Result<(), ()> { + 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); + // 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); } - 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 - ); - } - } + _ => { + tracing::debug!( + "single_instance failed to notify - launching normally: {}", + e + ); } } - Ok(()) - }) - .on_event(|app, event| { - if let RunEvent::Exit = event { - destroy(app); - } - }) - .build() + } + } + Ok(()) } 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 d55ec079c..8f293edc2 100644 --- a/plugins/single-instance/src/platform_impl/windows.rs +++ b/plugins/single-instance/src/platform_impl/windows.rs @@ -7,10 +7,7 @@ use crate::semver_compat::semver_compat_string; use crate::SingleInstanceCallback; use std::ffi::CStr; -use tauri::{ - plugin::{self, TauriPlugin}, - AppHandle, Manager, RunEvent, Runtime, -}; +use tauri::{AppHandle, Manager, Runtime}; use windows_sys::Win32::{ Foundation::{CloseHandle, GetLastError, ERROR_ALREADY_EXISTS, HWND, LPARAM, LRESULT, WPARAM}, System::{ @@ -51,69 +48,63 @@ 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 setup_single_instance( + app: &AppHandle, + callback: Box>, + _dbus_id: Option, +) -> Result<(), ()> { + #[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 _, - }; - - SendMessageW(hwnd, WM_COPYDATA, 0, &cds as *const _ as _); - - app.cleanup_before_exit(); - std::process::exit(0); - } - } - } else { - app.manage(MutexHandle(hmutex as _)); - - let userdata = UserData { - app: app.clone(), - callback, + let bytes = data.as_bytes(); + let cds = COPYDATASTRUCT { + dwData: WMCOPYDATA_SINGLE_INSTANCE_DATA, + cbData: bytes.len() as _, + lpData: bytes.as_ptr() as _, }; - 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| { - if let RunEvent::Exit = event { - destroy(app); + SendMessageW(hwnd, WM_COPYDATA, 0, &cds as *const _ as _); + + app.cleanup_before_exit(); + std::process::exit(0); } - }) - .build() + } + } 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(()) } pub fn destroy>(manager: &M) {