Skip to content

Commit dabac0e

Browse files
authored
feat(single-instance): add MacOs unix domain socket impl (#1035)
* feat(single-instance): add macos implementation * chore(single-instance): test MacOs by adding CLI to example * feat(single-instance): simplify macOS implementation * chore(single-instance): address remarks
1 parent a233919 commit dabac0e

8 files changed

Lines changed: 335 additions & 5 deletions

File tree

.changes/single-instance.macos.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"single-instance": patch
3+
---
4+
5+
Added implementation for MacOS.

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

plugins/single-instance/examples/vanilla/package-lock.json

Lines changed: 204 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

plugins/single-instance/examples/vanilla/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"description": "",
55
"main": "index.js",
66
"scripts": {
7-
"test": "echo \"Error: no test specified\" && exit 1"
7+
"tauri": "tauri"
88
},
99
"author": "",
1010
"license": "MIT",

plugins/single-instance/examples/vanilla/src-tauri/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ serde_json = { workspace = true }
1212
serde = { workspace = true }
1313
tauri = { workspace = true }
1414
tauri-plugin-single-instance = { path = "../../../" }
15+
tauri-plugin-cli = { path = "../../../../cli" }
1516

1617
[build-dependencies]
1718
tauri-build = { workspace = true }

plugins/single-instance/examples/vanilla/src-tauri/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
fn main() {
1111
tauri::Builder::default()
12+
.plugin(tauri_plugin_cli::init())
1213
.plugin(tauri_plugin_single_instance::init(|app, argv, cwd| {
1314
println!("{}, {argv:?}, {cwd}", app.package_info().name);
1415
}))

plugins/single-instance/examples/vanilla/src-tauri/tauri.conf.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,17 @@
2929
"icons/icon.icns",
3030
"icons/icon.ico"
3131
]
32+
},
33+
"plugins": {
34+
"cli": {
35+
"description": "Testing single-instance on MacOS",
36+
"args": [
37+
{
38+
"name": "somearg",
39+
"index": 1,
40+
"takesValue": true
41+
}
42+
]
43+
}
3244
}
3345
}

plugins/single-instance/src/platform_impl/macos.rs

Lines changed: 110 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,119 @@
44

55
#![cfg(target_os = "macos")]
66

7+
use std::{
8+
io::{BufWriter, Error, ErrorKind, Read, Write},
9+
os::unix::net::{UnixListener, UnixStream},
10+
path::PathBuf,
11+
};
12+
713
use crate::SingleInstanceCallback;
814
use tauri::{
915
plugin::{self, TauriPlugin},
10-
Manager, Runtime,
16+
AppHandle, Config, Manager, RunEvent, Runtime,
1117
};
12-
pub fn init<R: Runtime>(_f: Box<SingleInstanceCallback<R>>) -> TauriPlugin<R> {
13-
plugin::Builder::new("single-instance").build()
18+
19+
pub fn init<R: Runtime>(cb: Box<SingleInstanceCallback<R>>) -> TauriPlugin<R> {
20+
plugin::Builder::new("single-instance")
21+
.setup(|app, _api| {
22+
let socket = socket_path(app.config());
23+
24+
// Notify the singleton which may or may not exist.
25+
match notify_singleton(&socket) {
26+
Ok(_) => {
27+
std::process::exit(0);
28+
}
29+
Err(e) => {
30+
match e.kind() {
31+
ErrorKind::NotFound | ErrorKind::ConnectionRefused => {
32+
// This process claims itself as singleton as likely none exists
33+
socket_cleanup(&socket);
34+
listen_for_other_instances(&socket, app.clone(), cb);
35+
}
36+
_ => {
37+
log::debug!(
38+
"single_instance failed to notify - launching normally: {}",
39+
e
40+
);
41+
}
42+
}
43+
}
44+
}
45+
Ok(())
46+
})
47+
.on_event(|app, event| {
48+
if let RunEvent::Exit = event {
49+
destroy(app);
50+
}
51+
})
52+
.build()
53+
}
54+
55+
pub fn destroy<R: Runtime, M: Manager<R>>(manager: &M) {
56+
let socket = socket_path(manager.config());
57+
socket_cleanup(&socket);
58+
}
59+
60+
fn socket_path(config: &Config) -> PathBuf {
61+
let identifier = config.identifier.replace(['.', '-'].as_ref(), "_");
62+
// Use /tmp as socket path must be shorter than 100 chars.
63+
PathBuf::from(format!("/tmp/{}_si.sock", identifier))
64+
}
65+
66+
fn socket_cleanup(socket: &PathBuf) {
67+
let _ = std::fs::remove_file(socket);
1468
}
1569

16-
pub fn destroy<R: Runtime, M: Manager<R>>(_manager: &M) {}
70+
fn notify_singleton(socket: &PathBuf) -> Result<(), Error> {
71+
let stream = UnixStream::connect(&socket)?;
72+
let mut bf = BufWriter::new(&stream);
73+
let args_joined = std::env::args().collect::<Vec<String>>().join("\0");
74+
bf.write_all(args_joined.as_bytes())?;
75+
bf.flush()?;
76+
drop(bf);
77+
Ok(())
78+
}
79+
80+
fn listen_for_other_instances<A: Runtime>(
81+
socket: &PathBuf,
82+
app: AppHandle<A>,
83+
mut cb: Box<SingleInstanceCallback<A>>,
84+
) {
85+
match UnixListener::bind(&socket) {
86+
Ok(listener) => {
87+
let cwd = std::env::current_dir()
88+
.unwrap_or_default()
89+
.to_str()
90+
.unwrap_or_default()
91+
.to_string();
92+
93+
tauri::async_runtime::spawn(async move {
94+
for stream in listener.incoming() {
95+
match stream {
96+
Ok(mut stream) => {
97+
let mut s = String::new();
98+
match stream.read_to_string(&mut s) {
99+
Ok(_) => {
100+
let args: Vec<String> =
101+
s.split('\0').map(String::from).collect();
102+
cb(&app.clone().app_handle(), args, cwd.clone());
103+
}
104+
Err(e) => log::debug!("single_instance failed to be notified: {e}"),
105+
}
106+
}
107+
Err(err) => {
108+
log::debug!("single_instance failed to be notified: {}", err);
109+
continue;
110+
}
111+
}
112+
}
113+
});
114+
}
115+
Err(err) => {
116+
log::error!(
117+
"single_instance failed to listen to other processes - launching normally: {}",
118+
err
119+
);
120+
}
121+
}
122+
}

0 commit comments

Comments
 (0)