diff --git a/Cargo.lock b/Cargo.lock index cbcd922d..a42f7ca8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -503,6 +503,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-str" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4d34b8f066904ed7cfa4a6f9ee96c3214aa998cb44b69ca20bd2054f47402ed" + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -2132,6 +2138,7 @@ dependencies = [ "ashpd", "async-channel", "better_default", + "const-str", "dirs", "formatx", "fs-err", diff --git a/Cargo.toml b/Cargo.toml index 4e2ae37a..e0aa8e03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,3 +43,4 @@ ashpd = { version = "0.12", default-features = false, features = [ futures-timer = "3.0.3" tokio-util = "0.7.15" tracing-appender = "0.2.3" +const-str = "0.7.0" diff --git a/data/io.github.nozwock.Packet.Api.service.in b/data/io.github.nozwock.Packet.Api.service.in new file mode 100644 index 00000000..2343aee9 --- /dev/null +++ b/data/io.github.nozwock.Packet.Api.service.in @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=@app-id@ +Exec=@bindir@/packet --background diff --git a/data/meson.build b/data/meson.build index e1a3127f..35b3f1da 100644 --- a/data/meson.build +++ b/data/meson.build @@ -99,3 +99,14 @@ configure_file( install: true, install_dir: datadir / 'dbus-1' / 'services' ) + +service_conf = configuration_data() +service_conf.set('app-id', api_id) +service_conf.set('bindir', bindir) +configure_file( + input: '@0@.service.in'.format(base_id + '.Api'), + output: '@0@.service'.format(api_id), + configuration: service_conf, + install: true, + install_dir: datadir / 'dbus-1' / 'services' +) diff --git a/meson.build b/meson.build index f79439b2..60f7dbb2 100644 --- a/meson.build +++ b/meson.build @@ -48,6 +48,8 @@ else application_id = base_id endif +api_id = application_id + '.Api' + meson.add_dist_script( 'build-aux/dist-vendor.sh', meson.project_build_root() / 'meson-dist' / meson.project_name() @@ -70,4 +72,4 @@ gnome.post_install( gtk_update_icon_cache: true, glib_compile_schemas: true, update_desktop_database: true, -) +) \ No newline at end of file diff --git a/src/config.rs.in b/src/config.rs.in index f4bbb430..67d0832e 100644 --- a/src/config.rs.in +++ b/src/config.rs.in @@ -6,3 +6,6 @@ pub const PKGDATADIR: &str = @PKGDATADIR@; pub const PROFILE: &str = @PROFILE@; pub const RESOURCES_FILE: &str = concat!(@PKGDATADIR@, "/resources.gresource"); pub const VERSION: &str = @VERSION@; + +pub const DBUS_API_NAME: &str = const_str::concat!(APP_ID, ".Api"); +pub const DBUS_API_PATH: &str = const_str::concat!("/", const_str::replace!(APP_ID, ".", "/")); diff --git a/src/dbus.rs b/src/dbus.rs new file mode 100644 index 00000000..c0666183 --- /dev/null +++ b/src/dbus.rs @@ -0,0 +1,90 @@ +use std::sync::{Arc, LazyLock}; + +use tokio::sync::{ + Mutex, + mpsc::{Receiver, Sender, channel}, +}; +use zbus::{Connection, object_server::InterfaceRef}; + +use crate::config::{DBUS_API_NAME, DBUS_API_PATH}; + +#[derive(Debug)] +pub struct Packet { + pub device_name: String, + pub visibility: bool, + visibility_tx: Arc>>, + pub visibility_rx: Arc>>, +} + +#[zbus::interface(name = "io.github.nozwock.Packet1")] +impl Packet { + #[zbus(property)] + pub async fn device_name(&self) -> &str { + &self.device_name + } + + #[zbus(property)] + pub async fn device_visibility(&self) -> bool { + self.visibility + } + + /// Also sends the param to the channel associated with `visibility_tx`. + #[zbus(property)] + pub async fn set_device_visibility(&mut self, visibility: bool) { + self.visibility = visibility; + _ = self + .visibility_tx + .lock() + .await + .send(visibility) + .await + .inspect_err(|err| tracing::warn!(%err)); + } +} + +static CONNECTION: LazyLock>> = LazyLock::new(|| Mutex::new(None)); + +pub async fn get_connection() -> Option { + CONNECTION.lock().await.as_ref().cloned() +} + +pub async fn create_connection( + device_name: String, + visibility: bool, +) -> anyhow::Result { + let mut conn_guard = CONNECTION.lock().await; + + if let Some(conn) = conn_guard.as_ref() { + Ok(conn.clone()) + } else { + let (tx, rx) = channel::(1); + let conn = zbus::connection::Builder::session()? + .name(DBUS_API_NAME)? + .serve_at( + DBUS_API_PATH, + Packet { + device_name, + visibility, + visibility_tx: Arc::new(Mutex::new(tx)), + visibility_rx: Arc::new(Mutex::new(rx)), + }, + )? + .build() + .await?; + *conn_guard = Some(conn.clone()); + + Ok(conn) + } +} + +/// # Panics +/// Panics if `CONNECTION` is `None`. +pub async fn packet_iface() -> InterfaceRef { + get_connection() + .await + .expect("Session should be created before getting iface") + .object_server() + .interface::<_, Packet>(DBUS_API_PATH) + .await + .expect("Interface should be on the object path") +} diff --git a/src/main.rs b/src/main.rs index 3f9bec41..ecee691c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ mod application; #[rustfmt::skip] mod config; mod constants; +mod dbus; mod ext; mod monitors; mod objects; diff --git a/src/window.rs b/src/window.rs index 8f832a95..174d9b5f 100644 --- a/src/window.rs +++ b/src/window.rs @@ -19,6 +19,7 @@ use tokio_util::sync::CancellationToken; use crate::application::PacketApplication; use crate::config::{APP_ID, PROFILE}; use crate::constants::packet_log_path; +use crate::dbus; use crate::ext::MessageExt; use crate::objects::{self, SendRequestState}; use crate::objects::{TransferState, UserAction}; @@ -171,6 +172,7 @@ mod imp { pub is_mdns_discovery_on: Rc>, pub looping_async_tasks: RefCell>, + pub dbus_async_tasks: RefCell>, pub is_background_allowed: Cell, pub should_quit: Cell, @@ -216,6 +218,7 @@ mod imp { obj.setup_notification_actions_monitor(); obj.setup_rqs_service(); obj.request_background_at_start(); + obj.setup_dbus_api(); } } @@ -265,9 +268,16 @@ mod imp { // Abort all looping tasks before closing tracing::info!( - count = self.looping_async_tasks.borrow().len(), + rqs_tasks = self.looping_async_tasks.borrow().len(), + dbus_tasks = self.dbus_async_tasks.borrow().len(), "Cancelling looping tasks" ); + while let Some(join_handle) = self.dbus_async_tasks.borrow_mut().pop() { + match join_handle { + LoopingTaskHandle::Tokio(join_handle) => join_handle.abort(), + LoopingTaskHandle::Glib(join_handle) => join_handle.abort(), + } + } while let Some(join_handle) = self.looping_async_tasks.borrow_mut().pop() { match join_handle { LoopingTaskHandle::Tokio(join_handle) => join_handle.abort(), @@ -1495,6 +1505,24 @@ impl PacketApplicationWindow { )); } + async fn on_visibility_changed(&self, visibility: bool) { + let imp = self.imp(); + + self.bottom_bar_status_indicator_ui_update(visibility); + + if let Some(rqs) = imp.rqs.lock().await.as_mut() { + let visibility = if visibility { + rqs_lib::Visibility::Visible + } else { + rqs_lib::Visibility::Invisible + }; + + rqs.change_visibility(visibility); + } else { + tracing::warn!("Couldn't set device visibility due RQS not being set"); + } + } + fn bottom_bar_status_indicator_ui_update(&self, is_visible: bool) { let imp = self.imp(); @@ -1578,23 +1606,15 @@ impl PacketApplicationWindow { #[weak] imp, move |obj| { - imp.obj() - .bottom_bar_status_indicator_ui_update(obj.is_active()); - - let visibility = if obj.is_active() { - rqs_lib::Visibility::Visible - } else { - rqs_lib::Visibility::Invisible - }; - - glib::spawn_future_local(async move { - imp.rqs - .lock() - .await - .as_mut() - .unwrap() - .change_visibility(visibility); - }); + glib::spawn_future_local(clone!( + #[weak] + imp, + #[weak] + obj, + async move { + imp.obj().on_visibility_changed(obj.is_active()).await; + } + )); } )); } @@ -1849,6 +1869,90 @@ impl PacketApplicationWindow { handle } + fn setup_dbus_api(&self) { + let obj = self.clone(); + + let inner = async move || -> anyhow::Result<()> { + let imp = obj.imp(); + + _ = dbus::create_connection( + obj.get_device_name_state().as_str().to_string(), + imp.settings.boolean("device-visibility"), + ) + .await?; + + let handle = glib::spawn_future_local(clone!( + #[weak] + imp, + async move { + // IMPORTANT: Keep notice of get() and get_mut() so that it doesn't lead to deadlock + let mut visibility_rx = { + let iface_ref = dbus::packet_iface().await; + iface_ref + .get() + .await + .visibility_rx + .clone() + .lock_owned() + .await + }; + + while let Some(extern_visibility) = visibility_rx.recv().await { + _ = imp + .settings + .set_boolean("device-visibility", extern_visibility) + .inspect_err(|err| tracing::warn!(%err)); + } + } + )); + imp.dbus_async_tasks + .borrow_mut() + .push(LoopingTaskHandle::Glib(handle)); + + imp.settings + .connect_changed(None, move |settings, key| match key { + "device-visibility" | "device-name" => { + let key = key.to_string(); + glib::spawn_future_local(clone!( + #[weak] + settings, + async move { + let iface_ref = dbus::packet_iface().await; + let mut iface = iface_ref.get_mut().await; + match key.as_str() { + "device-name" => { + iface.device_name = + settings.string(&key).as_str().to_string(); + _ = iface + .device_name_changed(iface_ref.signal_emitter()) + .await + .inspect_err(|err| tracing::warn!(%err)); + } + "device-visibility" => { + iface.visibility = settings.boolean(&key); + _ = iface + .device_visibility_changed(iface_ref.signal_emitter()) + .await + .inspect_err(|err| tracing::warn!(%err)); + } + _ => {} + } + } + )); + } + _ => {} + }); + + Ok(()) + }; + + glib::spawn_future_local(async move { + _ = inner() + .await + .inspect_err(|err| tracing::error!(%err, "Failed to setup DBus API")); + }); + } + fn setup_connection_monitors(&self) { let imp = self.imp();