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 (#3584)
* 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>
This commit is contained in:
1 parent
6a263bec1c
commit
f83c5d3b0f
6 files changed
+237
-13
No files matched your search
@@ -0,0 +1,192 @@
|
||||
// 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(())
|
||||
}
|
||||
Reference in new issue
Block a user