mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-10-20 23:11:24 +02:00
* fix(single-instance): propagate D-Bus setup and handoff errors * chore: satisfy release metadata and test header checks * fix(single-instance): launch normally without a D-Bus session bus Log a warning instead of failing setup when the session bus is unreachable, and add context to handoff errors. * ci: run the single-instance D-Bus test under dbus-run-session Run it in test-rust on Linux and keep it out of the updater integration job, which has no session bus. --------- Co-authored-by: Lucas Nogueira <lucas@tauri.app>
193 lines
6.4 KiB
Rust
193 lines
6.4 KiB
Rust
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
#![cfg(all(
|
|
any(target_os = "linux", target_os = "freebsd"),
|
|
not(feature = "semver")
|
|
))]
|
|
|
|
use std::{
|
|
env, fs,
|
|
path::PathBuf,
|
|
process::{Child, Command, Output, Stdio},
|
|
sync::mpsc,
|
|
thread,
|
|
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
|
|
};
|
|
use tauri::test::{mock_builder, mock_context, noop_assets};
|
|
|
|
const TEST_NAME: &str = "dbus_setup_errors";
|
|
const ID_ENV: &str = "TAURI_DBUS_ERROR_ID";
|
|
const SETUP_ERROR: &str = "SETUP_ERROR:";
|
|
const STARTED: &str = "STARTED";
|
|
|
|
struct RejectCallback(mpsc::Sender<()>);
|
|
|
|
#[zbus::interface(name = "org.SingleInstance.DBus")]
|
|
impl RejectCallback {
|
|
fn execute_callback(&self, _argv: Vec<String>, _cwd: String) -> zbus::fdo::Result<()> {
|
|
self.0.send(()).unwrap();
|
|
Err(zbus::fdo::Error::Failed(
|
|
"test primary rejected delivery".into(),
|
|
))
|
|
}
|
|
}
|
|
|
|
struct Sandbox(PathBuf);
|
|
impl Drop for Sandbox {
|
|
fn drop(&mut self) {
|
|
let _ = fs::remove_dir_all(&self.0);
|
|
}
|
|
}
|
|
|
|
struct TestChild(Option<Child>);
|
|
impl TestChild {
|
|
fn spawn(id: &str, address: &str) -> Self {
|
|
Self(Some(
|
|
Command::new(env::current_exe().unwrap())
|
|
.args(["--ignored", "--exact", TEST_NAME, "--nocapture"])
|
|
.env(ID_ENV, id)
|
|
.env("DBUS_SESSION_BUS_ADDRESS", address)
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.spawn()
|
|
.unwrap(),
|
|
))
|
|
}
|
|
|
|
fn wait(mut self) -> Output {
|
|
let deadline = Instant::now() + Duration::from_secs(30);
|
|
loop {
|
|
if self.0.as_mut().unwrap().try_wait().unwrap().is_some() {
|
|
return self.0.take().unwrap().wait_with_output().unwrap();
|
|
}
|
|
assert!(Instant::now() < deadline, "D-Bus test child timed out");
|
|
thread::sleep(Duration::from_millis(10));
|
|
}
|
|
}
|
|
}
|
|
impl Drop for TestChild {
|
|
fn drop(&mut self) {
|
|
if let Some(child) = self.0.as_mut() {
|
|
let _ = child.kill();
|
|
let _ = child.wait();
|
|
}
|
|
}
|
|
}
|
|
|
|
fn assert_setup_error(output: Output) -> String {
|
|
assert_eq!(output.status.code(), Some(101));
|
|
let stdout = String::from_utf8(output.stdout).unwrap();
|
|
assert!(stdout.contains(SETUP_ERROR), "{stdout}");
|
|
let stderr = String::from_utf8(output.stderr).unwrap();
|
|
assert!(
|
|
stderr.contains("PluginInitialization(\"single-instance\""),
|
|
"{stderr}"
|
|
);
|
|
stdout
|
|
}
|
|
|
|
fn assert_started(output: Output) {
|
|
assert!(output.status.success(), "{output:?}");
|
|
let stdout = String::from_utf8(output.stdout).unwrap();
|
|
assert!(stdout.contains(STARTED), "{stdout}");
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires an explicit dbus-run-session invocation"]
|
|
fn dbus_setup_errors() -> Result<(), tauri::Error> {
|
|
if let Ok(id) = env::var(ID_ENV) {
|
|
let result = mock_builder()
|
|
.plugin(
|
|
tauri_plugin_single_instance::Builder::new()
|
|
.dbus_id(id)
|
|
.build(),
|
|
)
|
|
.build(mock_context(noop_assets()));
|
|
return match result {
|
|
// Without a usable session bus the app must still launch.
|
|
Ok(app) => {
|
|
println!("{STARTED}");
|
|
tauri_plugin_single_instance::destroy(&app);
|
|
Ok(())
|
|
}
|
|
Err(tauri::Error::PluginInitialization(plugin, message)) => {
|
|
assert_eq!(plugin, "single-instance");
|
|
println!("{SETUP_ERROR} {message}");
|
|
Err(tauri::Error::PluginInitialization(plugin, message))
|
|
}
|
|
Err(error) => panic!("unexpected error: {error}"),
|
|
};
|
|
}
|
|
|
|
let address =
|
|
env::var("DBUS_SESSION_BUS_ADDRESS").expect("run this test inside dbus-run-session");
|
|
let id = format!("org.tauri.DBusErrorTest.p{}", std::process::id());
|
|
let root = env::temp_dir().join(format!(
|
|
"tauri-dbus-error-{}-{}",
|
|
std::process::id(),
|
|
SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_nanos()
|
|
));
|
|
fs::create_dir(&root).unwrap();
|
|
let sandbox = Sandbox(root);
|
|
let missing_address = format!("unix:path={}", sandbox.0.join("missing.sock").display());
|
|
assert_started(TestChild::spawn(&id, &missing_address).wait());
|
|
assert_started(TestChild::spawn(&id, "invalid-address").wait());
|
|
assert_setup_error(TestChild::spawn("invalid dbus id", &address).wait());
|
|
|
|
// A real endpoint rejects delivery immediately, without bus timeouts or startup races.
|
|
let name = format!("{id}.SingleInstance");
|
|
let path = format!("/{}", name.replace('.', "/"));
|
|
let (tx, rx) = mpsc::channel();
|
|
let owner = zbus::blocking::connection::Builder::session()
|
|
.unwrap()
|
|
.name(name.as_str())
|
|
.unwrap()
|
|
.serve_at(path.as_str(), RejectCallback(tx))
|
|
.unwrap()
|
|
.build()
|
|
.unwrap();
|
|
let failure = assert_setup_error(TestChild::spawn(&id, &address).wait());
|
|
assert!(failure.contains("test primary rejected delivery"));
|
|
rx.recv_timeout(Duration::from_secs(1)).unwrap();
|
|
assert!(matches!(rx.try_recv(), Err(mpsc::TryRecvError::Empty)));
|
|
drop(owner);
|
|
|
|
// The successful primary and secondary paths still use the real plugin and bus.
|
|
let (tx, rx) = mpsc::channel();
|
|
let app = mock_builder()
|
|
.plugin(
|
|
tauri_plugin_single_instance::Builder::new()
|
|
.dbus_id(format!("{id}.Success"))
|
|
.callback(move |_, args, cwd| tx.send((args, cwd)).unwrap())
|
|
.build(),
|
|
)
|
|
.build(mock_context(noop_assets()))?;
|
|
let secondary = TestChild::spawn(&format!("{id}.Success"), &address).wait();
|
|
assert!(secondary.status.success());
|
|
assert!(
|
|
!String::from_utf8(secondary.stdout)
|
|
.unwrap()
|
|
.contains(STARTED)
|
|
);
|
|
let (args, cwd) = rx.recv_timeout(Duration::from_secs(1)).unwrap();
|
|
assert_eq!(
|
|
args,
|
|
[
|
|
env::current_exe().unwrap().to_str().unwrap(),
|
|
"--ignored",
|
|
"--exact",
|
|
TEST_NAME,
|
|
"--nocapture"
|
|
]
|
|
);
|
|
assert_eq!(cwd, env::current_dir().unwrap().to_str().unwrap());
|
|
assert!(matches!(rx.try_recv(), Err(mpsc::TryRecvError::Empty)));
|
|
tauri_plugin_single_instance::destroy(&app);
|
|
Ok(())
|
|
}
|