diff --git a/.changes/single-instance.macos.md b/.changes/single-instance.macos.md new file mode 100644 index 00000000..7b8b33b4 --- /dev/null +++ b/.changes/single-instance.macos.md @@ -0,0 +1,5 @@ +--- +"single-instance": patch +--- + +Added implementation for MacOS. diff --git a/Cargo.lock b/Cargo.lock index 2f1aeeea..cf627b57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5641,6 +5641,7 @@ dependencies = [ "serde_json", "tauri", "tauri-build", + "tauri-plugin-cli", "tauri-plugin-single-instance", ] diff --git a/plugins/single-instance/examples/vanilla/package-lock.json b/plugins/single-instance/examples/vanilla/package-lock.json new file mode 100644 index 00000000..14980fb0 --- /dev/null +++ b/plugins/single-instance/examples/vanilla/package-lock.json @@ -0,0 +1,204 @@ +{ + "name": "app", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "app", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "@tauri-apps/cli": "2.0.0-beta.3" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.0.0-beta.3.tgz", + "integrity": "sha512-xLAL2DNNUJWqHBKvanc3V9bG9kkwtFwc40X/DrfgEKnkajEm79wqnkaT8LUnmbe0WZ8bzBRO1fLIgKlOH6GiCA==", + "dev": true, + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.0.0-beta.3", + "@tauri-apps/cli-darwin-x64": "2.0.0-beta.3", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.0.0-beta.3", + "@tauri-apps/cli-linux-arm64-gnu": "2.0.0-beta.3", + "@tauri-apps/cli-linux-arm64-musl": "2.0.0-beta.3", + "@tauri-apps/cli-linux-x64-gnu": "2.0.0-beta.3", + "@tauri-apps/cli-linux-x64-musl": "2.0.0-beta.3", + "@tauri-apps/cli-win32-arm64-msvc": "2.0.0-beta.3", + "@tauri-apps/cli-win32-ia32-msvc": "2.0.0-beta.3", + "@tauri-apps/cli-win32-x64-msvc": "2.0.0-beta.3" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.0.0-beta.3.tgz", + "integrity": "sha512-gHcn3jI/4MDXDIlK/4Zz0ftTosgN3OimWlKxEz777QrA1hldrQweYIhdZXkqE9KgoE+u6w80vWIcr0InHAf7Iw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.0.0-beta.3.tgz", + "integrity": "sha512-kRCaukT2IAGMmNuAOUBhdZRlKujTy2lSsdNKmgGEMnzQLKJwWO9Gpq1NmPY7ZVqyXK/X8QnGHuasDEQsSO6B4w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.0.0-beta.3.tgz", + "integrity": "sha512-cpNZOQDotNSdjoZT16s1JtZvnkM0wgLwU39AhKhRCco4KEH3/8G1ngKF9JKalWUN8zDTcuCigEAr37gEv4mLAA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.0.0-beta.3.tgz", + "integrity": "sha512-8q86V6P9bkeoFcnvSsnvOwmKY6ijIN4ueRVXCj5cVpsw392VF9vud1Nq7/l+QDgn9OWbZNNVDl30iyoSuaykBA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.0.0-beta.3.tgz", + "integrity": "sha512-L7fokh4aqyV6yDPoeKwFN3Yt0pCAuZMWeP5tOeSBiom1pU7ppKH+4KHeTekNEIecZG+Ah250DkVCdmWS+aRFTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.0.0-beta.3.tgz", + "integrity": "sha512-/crp3K6PathqicVWPj8Kh1120NNVV7nagJ7oZW9OFch7nBS1tmDnSB5k5LgA4yYu+lDKNUREnATMWHL6i0gNeg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.0.0-beta.3.tgz", + "integrity": "sha512-jX1ZT0UQwdBGbpCwlpv2bsLDO7KFMeDJQ/ZZVMfWyjuYrGBG5zhJ2NXwTMkHVnxfvE6BVmnybWcykeSqTATeOw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.0.0-beta.3.tgz", + "integrity": "sha512-UCEZNKocENLX3HYKid4FEbrCMjCX9e58klBIvJKxT8HTjvpgFYDoKccswDNfszLhmineKMlkUvm7j7U0sMh8MQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.0.0-beta.3.tgz", + "integrity": "sha512-O8syGXDHyKN/cv1ktD76dTcbkQ1nNEPhnT1Z+r0GKxNsw4/MyIVglzEcou3aPq0/1MQ0PEGVyG1x0JMaPw7oHQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.0.0-beta.3.tgz", + "integrity": "sha512-YDdF3XWaptjKtKz33sZhC+uNAZwp6QtAmZSRCQQlC1W7uJwLD00/3QF4vO/c6Qm+BGFsazVh1+YmBF1p0kV0rg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + } + } +} diff --git a/plugins/single-instance/examples/vanilla/package.json b/plugins/single-instance/examples/vanilla/package.json index c44bd9c7..1b7ea034 100644 --- a/plugins/single-instance/examples/vanilla/package.json +++ b/plugins/single-instance/examples/vanilla/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "tauri": "tauri" }, "author": "", "license": "MIT", diff --git a/plugins/single-instance/examples/vanilla/src-tauri/Cargo.toml b/plugins/single-instance/examples/vanilla/src-tauri/Cargo.toml index badede36..ae789acd 100644 --- a/plugins/single-instance/examples/vanilla/src-tauri/Cargo.toml +++ b/plugins/single-instance/examples/vanilla/src-tauri/Cargo.toml @@ -12,6 +12,7 @@ serde_json = { workspace = true } serde = { workspace = true } tauri = { workspace = true } tauri-plugin-single-instance = { path = "../../../" } +tauri-plugin-cli = { path = "../../../../cli" } [build-dependencies] tauri-build = { workspace = true } diff --git a/plugins/single-instance/examples/vanilla/src-tauri/src/main.rs b/plugins/single-instance/examples/vanilla/src-tauri/src/main.rs index 0b93460d..49b1a5a6 100644 --- a/plugins/single-instance/examples/vanilla/src-tauri/src/main.rs +++ b/plugins/single-instance/examples/vanilla/src-tauri/src/main.rs @@ -9,6 +9,7 @@ fn main() { tauri::Builder::default() + .plugin(tauri_plugin_cli::init()) .plugin(tauri_plugin_single_instance::init(|app, argv, cwd| { println!("{}, {argv:?}, {cwd}", app.package_info().name); })) diff --git a/plugins/single-instance/examples/vanilla/src-tauri/tauri.conf.json b/plugins/single-instance/examples/vanilla/src-tauri/tauri.conf.json index 8f9a852d..41623cc5 100644 --- a/plugins/single-instance/examples/vanilla/src-tauri/tauri.conf.json +++ b/plugins/single-instance/examples/vanilla/src-tauri/tauri.conf.json @@ -29,5 +29,17 @@ "icons/icon.icns", "icons/icon.ico" ] + }, + "plugins": { + "cli": { + "description": "Testing single-instance on MacOS", + "args": [ + { + "name": "somearg", + "index": 1, + "takesValue": true + } + ] + } } } diff --git a/plugins/single-instance/src/platform_impl/macos.rs b/plugins/single-instance/src/platform_impl/macos.rs index 170ff0e0..680eca09 100644 --- a/plugins/single-instance/src/platform_impl/macos.rs +++ b/plugins/single-instance/src/platform_impl/macos.rs @@ -4,13 +4,119 @@ #![cfg(target_os = "macos")] +use std::{ + io::{BufWriter, Error, ErrorKind, Read, Write}, + os::unix::net::{UnixListener, UnixStream}, + path::PathBuf, +}; + use crate::SingleInstanceCallback; use tauri::{ plugin::{self, TauriPlugin}, - Manager, Runtime, + AppHandle, Config, Manager, RunEvent, Runtime, }; -pub fn init(_f: Box>) -> TauriPlugin { - plugin::Builder::new("single-instance").build() + +pub fn init(cb: Box>) -> TauriPlugin { + plugin::Builder::new("single-instance") + .setup(|app, _api| { + let socket = socket_path(app.config()); + + // 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); + } + _ => { + log::debug!( + "single_instance failed to notify - launching normally: {}", + e + ); + } + } + } + } + Ok(()) + }) + .on_event(|app, event| { + if let RunEvent::Exit = event { + destroy(app); + } + }) + .build() +} + +pub fn destroy>(manager: &M) { + let socket = socket_path(manager.config()); + socket_cleanup(&socket); +} + +fn socket_path(config: &Config) -> PathBuf { + let identifier = config.identifier.replace(['.', '-'].as_ref(), "_"); + // Use /tmp as socket path must be shorter than 100 chars. + PathBuf::from(format!("/tmp/{}_si.sock", identifier)) +} + +fn socket_cleanup(socket: &PathBuf) { + let _ = std::fs::remove_file(socket); } -pub fn destroy>(_manager: &M) {} +fn notify_singleton(socket: &PathBuf) -> Result<(), Error> { + let stream = UnixStream::connect(&socket)?; + let mut bf = BufWriter::new(&stream); + let args_joined = std::env::args().collect::>().join("\0"); + bf.write_all(args_joined.as_bytes())?; + bf.flush()?; + drop(bf); + Ok(()) +} + +fn listen_for_other_instances( + socket: &PathBuf, + app: AppHandle, + mut cb: Box>, +) { + match UnixListener::bind(&socket) { + Ok(listener) => { + let cwd = std::env::current_dir() + .unwrap_or_default() + .to_str() + .unwrap_or_default() + .to_string(); + + tauri::async_runtime::spawn(async move { + for stream in listener.incoming() { + match stream { + Ok(mut stream) => { + let mut s = String::new(); + match stream.read_to_string(&mut s) { + Ok(_) => { + let args: Vec = + s.split('\0').map(String::from).collect(); + cb(&app.clone().app_handle(), args, cwd.clone()); + } + Err(e) => log::debug!("single_instance failed to be notified: {e}"), + } + } + Err(err) => { + log::debug!("single_instance failed to be notified: {}", err); + continue; + } + } + } + }); + } + Err(err) => { + log::error!( + "single_instance failed to listen to other processes - launching normally: {}", + err + ); + } + } +}