Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/single-instance-destroy-on-exit-opt-out.md
Original file line number Diff line number Diff line change
@@ -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.
32 changes: 32 additions & 0 deletions plugins/single-instance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
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.
Expand Down
15 changes: 15 additions & 0 deletions plugins/single-instance/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ pub fn destroy<R: Runtime, M: Manager<R>>(manager: &M) {

pub struct Builder<R: Runtime> {
callback: Box<SingleInstanceCallback<R>>,
destroy_on_exit: bool,
dbus_id: Option<String>,
}

Expand All @@ -52,6 +53,7 @@ impl<R: Runtime> Default for Builder<R> {
deep_link.handle_cli_arguments(_args.iter());
}
}),
destroy_on_exit: true,
dbus_id: None,
}
}
Expand All @@ -78,6 +80,18 @@ impl<R: Runtime> Builder<R> {
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
}

/// 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.
Expand All @@ -91,6 +105,7 @@ impl<R: Runtime> Builder<R> {
pub fn build(self) -> TauriPlugin<R> {
platform_impl::init(
self.callback,
self.destroy_on_exit,
#[cfg(target_os = "linux")]
self.dbus_id,
)
Expand Down
118 changes: 61 additions & 57 deletions plugins/single-instance/src/platform_impl/linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,74 +30,78 @@ struct DBusName(String);

pub fn init<R: Runtime>(
callback: Box<SingleInstanceCallback<R>>,
destroy_on_exit: bool,
dbus_id: Option<String>,
) -> TauriPlugin<R> {
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::<Vec<String>>(),
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::<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));
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<R: Runtime, M: Manager<R>>(manager: &M) {
Expand Down
58 changes: 32 additions & 26 deletions plugins/single-instance/src/platform_impl/macos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,40 +17,46 @@ use tauri::{
};
use tokio::io::AsyncReadExt;

pub fn init<R: Runtime>(cb: Box<SingleInstanceCallback<R>>) -> TauriPlugin<R> {
plugin::Builder::new("single-instance")
.setup(|app, _api| {
let socket = socket_path(app.config(), app.package_info());
pub fn init<R: Runtime>(
cb: Box<SingleInstanceCallback<R>>,
destroy_on_exit: bool,
) -> TauriPlugin<R> {
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<R: Runtime, M: Manager<R>>(manager: &M) {
Expand Down
Loading
Loading