Compare commits

...

2 Commits

Author SHA1 Message Date
Lucas Nogueira 57f6e45b27 Merge branch 'v2' into feat/setup-single-instance-manually 2026-02-05 10:29:41 -03:00
Lucas Nogueira b65a193e6d feat(single-instance): add setup() function to run flow separately 2026-02-05 10:28:15 -03:00
5 changed files with 160 additions and 163 deletions
@@ -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.
+25 -6
View File
@@ -10,7 +10,7 @@
)] )]
#![cfg(not(any(target_os = "android", target_os = "ios")))] #![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")] #[cfg(target_os = "windows")]
#[path = "platform_impl/windows.rs"] #[path = "platform_impl/windows.rs"]
@@ -34,6 +34,17 @@ pub fn init<R: Runtime, F: FnMut(&AppHandle<R>, Vec<String>, String) + Send + Sy
Builder::new().callback(f).build() 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<R: Runtime, F: FnMut(&AppHandle<R>, Vec<String>, String) + Send + Sync + 'static>(
app: &AppHandle<R>,
callback: F,
dbus_id: Option<String>,
) -> Result<(), ()> {
platform_impl::setup_single_instance(app, Box::new(callback), dbus_id)
}
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)
} }
@@ -89,10 +100,18 @@ impl<R: Runtime> Builder<R> {
} }
pub fn build(self) -> TauriPlugin<R> { pub fn build(self) -> TauriPlugin<R> {
platform_impl::init( let callback = self.callback;
self.callback, let dbus_id = self.dbus_id;
#[cfg(target_os = "linux")] plugin::Builder::new("single-instance")
self.dbus_id, .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()
} }
} }
@@ -6,10 +6,7 @@
use crate::semver_compat::semver_compat_string; use crate::semver_compat::semver_compat_string;
use crate::SingleInstanceCallback; use crate::SingleInstanceCallback;
use tauri::{ use tauri::{AppHandle, Manager, Runtime};
plugin::{self, TauriPlugin},
AppHandle, Manager, RunEvent, Runtime,
};
use zbus::{blocking::Connection, interface, names::WellKnownName}; use zbus::{blocking::Connection, interface, names::WellKnownName};
struct ConnectionHandle(Connection); struct ConnectionHandle(Connection);
@@ -28,76 +25,69 @@ impl<R: Runtime> SingleInstanceDBus<R> {
struct DBusName(String); struct DBusName(String);
pub fn init<R: Runtime>( pub fn setup_single_instance<R: Runtime>(
app: &AppHandle<R>,
callback: Box<SingleInstanceCallback<R>>, callback: Box<SingleInstanceCallback<R>>,
dbus_id: Option<String>, dbus_id: Option<String>,
) -> TauriPlugin<R> { ) -> Result<(), ()> {
plugin::Builder::new("single-instance") let mut dbus_name = dbus_id.unwrap_or_else(|| app.config().identifier.clone());
.setup(move |app, _api| {
let mut dbus_name = dbus_id.unwrap_or_else(|| app.config().identifier.clone());
#[cfg(feature = "semver")] #[cfg(feature = "semver")]
{ {
dbus_name.push('_'); dbus_name.push('_');
dbus_name.push_str(semver_compat_string(&app.package_info().version).as_str()); dbus_name.push_str(semver_compat_string(&app.package_info().version).as_str());
} }
dbus_name.push_str(".SingleInstance");
let mut dbus_path = dbus_name.replace('.', "/").replace('-', "_"); dbus_name.push_str(".SingleInstance");
if !dbus_path.starts_with('/') {
dbus_path = format!("/{dbus_path}");
}
let single_instance_dbus = SingleInstanceDBus { let mut dbus_path = dbus_name.replace('.', "/").replace('-', "_");
callback, if !dbus_path.starts_with('/') {
app_handle: app.clone(), dbus_path = format!("/{dbus_path}");
}; }
match zbus::blocking::connection::Builder::session() let single_instance_dbus = SingleInstanceDBus {
.unwrap() callback,
.name(dbus_name.as_str()) app_handle: app.clone(),
.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::<Vec<String>>(),
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)); match zbus::blocking::connection::Builder::session()
.unwrap()
Ok(()) .name(dbus_name.as_str())
}) .unwrap()
.on_event(move |app, event| { .replace_existing_names(false)
if let RunEvent::Exit = event { .allow_name_replacements(false)
destroy(app); .serve_at(dbus_path.as_str(), single_instance_dbus)
} .unwrap()
})
.build() .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::<Vec<String>>(),
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<R: Runtime, M: Manager<R>>(manager: &M) { pub fn destroy<R: Runtime, M: Manager<R>>(manager: &M) {
@@ -11,45 +11,37 @@ use std::{
#[cfg(feature = "semver")] #[cfg(feature = "semver")]
use crate::semver_compat::semver_compat_string; use crate::semver_compat::semver_compat_string;
use crate::SingleInstanceCallback; use crate::SingleInstanceCallback;
use tauri::{ use tauri::{AppHandle, Config, Manager, Runtime};
plugin::{self, TauriPlugin},
AppHandle, Config, Manager, RunEvent, Runtime,
};
pub fn init<R: Runtime>(cb: Box<SingleInstanceCallback<R>>) -> TauriPlugin<R> { pub fn setup_single_instance<R: Runtime>(
plugin::Builder::new("single-instance") app: &AppHandle<R>,
.setup(|app, _api| { cb: Box<SingleInstanceCallback<R>>,
let socket = socket_path(app.config(), app.package_info()); _dbus_id: Option<String>,
) -> Result<(), ()> {
let socket = socket_path(app.config(), app.package_info());
// Notify the singleton which may or may not exist. // Notify the singleton which may or may not exist.
match notify_singleton(&socket) { match notify_singleton(&socket) {
Ok(_) => { Ok(_) => {
std::process::exit(0); 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() { tracing::debug!(
ErrorKind::NotFound | ErrorKind::ConnectionRefused => { "single_instance failed to notify - launching normally: {}",
// This process claims itself as singleton as likely none exists e
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 let RunEvent::Exit = event {
destroy(app);
}
})
.build()
} }
pub fn destroy<R: Runtime, M: Manager<R>>(manager: &M) { pub fn destroy<R: Runtime, M: Manager<R>>(manager: &M) {
@@ -7,10 +7,7 @@ use crate::semver_compat::semver_compat_string;
use crate::SingleInstanceCallback; use crate::SingleInstanceCallback;
use std::ffi::CStr; use std::ffi::CStr;
use tauri::{ use tauri::{AppHandle, Manager, Runtime};
plugin::{self, TauriPlugin},
AppHandle, Manager, RunEvent, Runtime,
};
use windows_sys::Win32::{ use windows_sys::Win32::{
Foundation::{CloseHandle, GetLastError, ERROR_ALREADY_EXISTS, HWND, LPARAM, LRESULT, WPARAM}, Foundation::{CloseHandle, GetLastError, ERROR_ALREADY_EXISTS, HWND, LPARAM, LRESULT, WPARAM},
System::{ System::{
@@ -51,69 +48,63 @@ impl<R: Runtime> UserData<R> {
} }
} }
pub fn init<R: Runtime>(callback: Box<SingleInstanceCallback<R>>) -> TauriPlugin<R> { pub fn setup_single_instance<R: Runtime>(
plugin::Builder::new("single-instance") app: &AppHandle<R>,
.setup(|app, _api| { callback: Box<SingleInstanceCallback<R>>,
#[allow(unused_mut)] _dbus_id: Option<String>,
let mut id = app.config().identifier.clone(); ) -> Result<(), ()> {
#[cfg(feature = "semver")] #[allow(unused_mut)]
{ let mut id = app.config().identifier.clone();
id.push('_'); #[cfg(feature = "semver")]
id.push_str(semver_compat_string(&app.package_info().version).as_str()); {
} id.push('_');
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"));
let window_name = encode_wide(format!("{id}-siw")); let window_name = encode_wide(format!("{id}-siw"));
let mutex_name = encode_wide(format!("{id}-sim")); let mutex_name = encode_wide(format!("{id}-sim"));
let hmutex = let hmutex = unsafe { CreateMutexW(std::ptr::null(), true.into(), mutex_name.as_ptr()) };
unsafe { CreateMutexW(std::ptr::null(), true.into(), mutex_name.as_ptr()) };
if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS { if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS {
unsafe { unsafe {
let hwnd = FindWindowW(class_name.as_ptr(), window_name.as_ptr()); let hwnd = FindWindowW(class_name.as_ptr(), window_name.as_ptr());
if !hwnd.is_null() { if !hwnd.is_null() {
let cwd = std::env::current_dir().unwrap_or_default(); let cwd = std::env::current_dir().unwrap_or_default();
let cwd = cwd.to_str().unwrap_or_default(); let cwd = cwd.to_str().unwrap_or_default();
let args = std::env::args().collect::<Vec<String>>().join("|"); let args = std::env::args().collect::<Vec<String>>().join("|");
let data = format!("{cwd}|{args}\0",); let data = format!("{cwd}|{args}\0",);
let bytes = data.as_bytes(); let bytes = data.as_bytes();
let cds = COPYDATASTRUCT { let cds = COPYDATASTRUCT {
dwData: WMCOPYDATA_SINGLE_INSTANCE_DATA, dwData: WMCOPYDATA_SINGLE_INSTANCE_DATA,
cbData: bytes.len() as _, cbData: bytes.len() as _,
lpData: bytes.as_ptr() 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 userdata = Box::into_raw(Box::new(userdata));
let hwnd = create_event_target_window::<R>(&class_name, &window_name, userdata);
app.manage(TargetWindowHandle(hwnd as _));
}
Ok(()) SendMessageW(hwnd, WM_COPYDATA, 0, &cds as *const _ as _);
})
.on_event(|app, event| { app.cleanup_before_exit();
if let RunEvent::Exit = event { std::process::exit(0);
destroy(app);
} }
}) }
.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::<R>(&class_name, &window_name, userdata);
app.manage(TargetWindowHandle(hwnd as _));
}
Ok(())
} }
pub fn destroy<R: Runtime, M: Manager<R>>(manager: &M) { pub fn destroy<R: Runtime, M: Manager<R>>(manager: &M) {