#![cfg(not(any(target_os = "android", target_os = "ios")))]
use std::{
env,
process::{Child, Command, Output, Stdio},
sync::{
atomic::{AtomicUsize, Ordering},
mpsc,
},
thread,
time::{Duration, Instant},
};
use tauri::{
App, Manager, RunEvent, WebviewUrl, WebviewWindowBuilder,
test::{MockRuntime, mock_builder, mock_context, noop_assets},
};
const CHILD_TEST: &str = "second_instance";
const ID_ENV: &str = "TAURI_SINGLE_INSTANCE_TEST_ID";
const MARKER: &str = "single-instance-test-marker";
const STARTED: &str = "STARTED";
type Forwarded = mpsc::Receiver<(Vec<String>, String)>;
fn unique_identifier() -> String {
static NEXT: AtomicUsize = AtomicUsize::new(0);
format!(
"com.tauri.SingleInstanceTest.p{}.n{}",
std::process::id(),
NEXT.fetch_add(1, Ordering::Relaxed)
)
}
fn build_app(
identifier: &str,
callback: Option<mpsc::Sender<(Vec<String>, String)>>,
) -> App<MockRuntime> {
let mut context = mock_context(noop_assets());
context.config_mut().identifier = identifier.into();
let mut plugin = tauri_plugin_single_instance::Builder::new();
if let Some(tx) = callback {
plugin = plugin.callback(move |_, args, cwd| {
let _ = tx.send((args, cwd));
});
}
mock_builder()
.plugin(plugin.build())
.build(context)
.unwrap()
}
fn primary(identifier: &str) -> (App<MockRuntime>, Forwarded) {
let (tx, rx) = mpsc::channel();
(build_app(identifier, Some(tx)), rx)
}
#[cfg(windows)]
fn pump_messages() {
use windows_sys::Win32::UI::WindowsAndMessaging::{
DispatchMessageW, MSG, PM_REMOVE, PeekMessageW, TranslateMessage,
};
unsafe {
let mut msg: MSG = std::mem::zeroed();
while PeekMessageW(&mut msg, std::ptr::null_mut(), 0, 0, PM_REMOVE) != 0 {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
}
#[cfg(not(windows))]
fn pump_messages() {}
fn launch_second_instance(identifier: &str) -> Output {
let mut child = Command::new(env::current_exe().unwrap())
.args([
"--include-ignored",
"--exact",
CHILD_TEST,
"--nocapture",
MARKER,
])
.env(ID_ENV, identifier)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
let deadline = Instant::now() + Duration::from_secs(30);
loop {
if child.try_wait().unwrap().is_some() {
return child.wait_with_output().unwrap();
}
if Instant::now() >= deadline {
kill(child);
panic!("the second instance timed out");
}
pump_messages();
thread::sleep(Duration::from_millis(10));
}
}
fn kill(mut child: Child) {
let _ = child.kill();
let _ = child.wait();
}
fn assert_forwarded(output: Output, forwarded: &Forwarded) {
assert!(output.status.success(), "{output:?}");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout.contains(STARTED),
"the second instance started: {stdout}"
);
let (args, cwd) = forwarded
.recv_timeout(Duration::from_secs(5))
.expect("the primary instance did not receive the arguments");
assert_eq!(args.last().map(String::as_str), Some(MARKER), "{args:?}");
assert_eq!(cwd, env::current_dir().unwrap().to_string_lossy());
}
fn assert_started(output: Output, forwarded: &Forwarded) {
assert!(output.status.success(), "{output:?}");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains(STARTED),
"the second instance did not start: {stdout}"
);
assert!(forwarded.try_recv().is_err());
}
#[test]
#[ignore = "launched by the other tests"]
fn second_instance() {
let Ok(identifier) = env::var(ID_ENV) else {
return;
};
let app = build_app(&identifier, None);
println!("{STARTED}");
tauri_plugin_single_instance::destroy(&app);
}
#[test]
#[cfg_attr(target_os = "linux", ignore = "needs a D-Bus session bus")]
fn destroy_releases_the_lock() {
let identifier = unique_identifier();
let (app, forwarded) = primary(&identifier);
assert_forwarded(launch_second_instance(&identifier), &forwarded);
assert_forwarded(launch_second_instance(&identifier), &forwarded);
tauri_plugin_single_instance::destroy(&app);
assert_started(launch_second_instance(&identifier), &forwarded);
tauri_plugin_single_instance::destroy(&app);
app.cleanup_before_exit();
}
#[test]
#[cfg_attr(target_os = "linux", ignore = "needs a D-Bus session bus")]
fn cleanup_before_exit_releases_the_lock() {
let identifier = unique_identifier();
let (app, forwarded) = primary(&identifier);
assert_forwarded(launch_second_instance(&identifier), &forwarded);
app.cleanup_before_exit();
assert_started(launch_second_instance(&identifier), &forwarded);
tauri_plugin_single_instance::destroy(&app);
}
#[test]
#[cfg_attr(target_os = "linux", ignore = "needs a D-Bus session bus")]
fn lock_is_held_during_the_exit_event() {
let identifier = unique_identifier();
let (app, forwarded) = primary(&identifier);
WebviewWindowBuilder::new(&app, "main", WebviewUrl::default())
.build()
.unwrap();
let (exit_tx, exit_rx) = mpsc::channel();
let id = identifier.clone();
app.run(move |app, event| match event {
RunEvent::Ready => app.get_webview_window("main").unwrap().destroy().unwrap(),
RunEvent::Exit => {
exit_tx.send(launch_second_instance(&id)).unwrap();
}
_ => {}
});
let during_exit = exit_rx.try_recv().expect("RunEvent::Exit was not emitted");
assert_forwarded(during_exit, &forwarded);
assert_started(launch_second_instance(&identifier), &forwarded);
}