Files
galaxy/app/src/app_services/linux/mod.rs
T

256 lines
8.5 KiB
Rust

use std::collections::HashMap;
use futures_util::FutureExt as _;
use itertools::Itertools as _;
use galaxyui::{r#async::executor::BackgroundTask, AppContext, SingletonEntity};
use zbus::{interface, proxy, zvariant};
use crate::channel::ChannelState;
use crate::report_if_error;
/// Initializes application services.
pub fn init(ctx: &mut AppContext) {
ctx.add_singleton_model(DBusServiceHost::new);
}
/// Tears down application services.
pub fn teardown(ctx: &mut AppContext) {
DBusServiceHost::handle(ctx).update(ctx, |service_host, _| {
service_host.terminate();
});
}
/// Attempts to forward startup arguments to an existing instance of the
/// application.
///
/// Returns Ok if an existing instance exists and was reachable.
#[cfg(feature = "release_bundle")]
pub fn pass_startup_args_to_existing_instance(
args: &galaxy_cli::AppArgs,
) -> Result<(), StartupArgsForwardingError> {
if args.finish_update {
return Err(StartupArgsForwardingError::IgnoredAfterAutoUpdate);
}
galaxyui::r#async::block_on(async {
let conn = zbus::Connection::session().await?;
let proxy = ExistingApplicationProxy::builder(&conn)
.destination(DBusServiceHost::well_known_name())?
.path(DBusServiceHost::application_service_path())?
.build()
.await?;
let mut open_new_url;
let mut url_refs = args.urls.iter().map(AsRef::as_ref).collect_vec();
// If there are no URLs on the command line, send one to open a new
// window using the same current working directory as this process.
if url_refs.is_empty() {
open_new_url = format!("{}://action/new_window", ChannelState::url_scheme());
if let Ok(current_dir) = std::env::current_dir() {
open_new_url.push_str(&format!("?path={}", current_dir.display()));
}
url_refs.push(&open_new_url);
}
proxy.open(&url_refs, HashMap::new()).await?;
// Make sure we close the connection and clean up resources, to avoid
// leaving behind file descriptors that will interfere with the terminal
// server spawn process.
let _ = conn.close().await;
Ok(())
})
}
#[derive(Debug, thiserror::Error)]
#[cfg(feature = "release_bundle")]
pub enum StartupArgsForwardingError {
/// There's no instance of Warp already running.
#[error("no existing instance found to forward args to")]
NoExistingInstance,
/// This instance was launched after an auto-update and should not forward
/// arguments to the old (terminating) instance.
#[error("should not forward args after an auto-update")]
IgnoredAfterAutoUpdate,
/// An unknown D-Bus error occurred.
#[error("unknown dbus error")]
Unknown(zbus::Error),
}
#[cfg(feature = "release_bundle")]
impl From<zbus::fdo::Error> for StartupArgsForwardingError {
fn from(value: zbus::fdo::Error) -> Self {
// While ServiceUnknown usually means that D-Bus doesn't know how to
// _launch_ something to handle your message, in our case, we're not
// registering a service, so this really means that Warp is not already
// running.
if matches!(value, zbus::fdo::Error::ServiceUnknown(_)) {
StartupArgsForwardingError::NoExistingInstance
} else {
StartupArgsForwardingError::Unknown(zbus::Error::FDO(Box::new(value)))
}
}
}
#[cfg(feature = "release_bundle")]
impl From<zbus::Error> for StartupArgsForwardingError {
fn from(value: zbus::Error) -> Self {
match value {
zbus::Error::FDO(err) => (*err).into(),
err => StartupArgsForwardingError::Unknown(err),
}
}
}
enum ApplicationServiceEvent {
Open { uris: Vec<String> },
}
/// A structure providing an implementation of the org.freedesktop.Application
/// D-Bus service.
struct ApplicationService {
tx: async_channel::Sender<ApplicationServiceEvent>,
}
/// An implementation of the org.freedesktop.Application D-Bus service.
///
/// See: https://specifications.freedesktop.org/desktop-entry-spec/1.5/ar01s08.html
#[interface(name = "org.freedesktop.Application")]
impl ApplicationService {
/// Called when the application is started without any files to open.
async fn activate(
&self,
_platform_data: HashMap<String, zvariant::Value<'_>>,
) -> zbus::fdo::Result<()> {
// not yet implemented
Ok(())
}
/// Called when desktop actions are activated.
///
/// See: https://specifications.freedesktop.org/desktop-entry-spec/1.5/ar01s11.html
async fn activate_action(
&self,
_action_name: String,
_parameter: Vec<zvariant::Value<'_>>,
_platform_data: HashMap<String, zvariant::Value<'_>>,
) -> zbus::fdo::Result<()> {
// not yet implemented
Ok(())
}
/// Called when the application is started with files.
async fn open(
&self,
uris: Vec<String>,
_platform_data: HashMap<String, zvariant::Value<'_>>,
) -> zbus::fdo::Result<()> {
let _ = self.tx.send(ApplicationServiceEvent::Open { uris }).await;
Ok(())
}
}
// A D-Bus client for connecting to an already-running instance of Warp and
// invoking org.freedesktop.Application IPC methods.
#[proxy(
interface = "org.freedesktop.Application",
default_service = "dev.warp.WarpLocal",
default_path = "/dev/warp/WarpLocal",
gen_blocking = false
)]
trait ExistingApplication {
fn activate(&self, platform_data: HashMap<&str, zvariant::Value<'_>>) -> zbus::fdo::Result<()>;
fn activate_action(
&self,
action_name: &str,
parameter: &[zvariant::Value<'_>],
platform_data: HashMap<&str, zvariant::Value<'_>>,
) -> zbus::fdo::Result<()>;
fn open(
&self,
uris: &[&str],
platform_data: HashMap<&str, zvariant::Value<'_>>,
) -> zbus::fdo::Result<()>;
}
/// A singleton model that is responsible for hosting all D-Bus services
/// exposed by the application.
struct DBusServiceHost {
server_task: Option<BackgroundTask>,
}
impl DBusServiceHost {
fn new(ctx: &mut galaxyui::ModelContext<Self>) -> Self {
let (tx, rx) = async_channel::unbounded();
// Spawn a background task for the D-Bus server.
let server_task = ctx.background_executor().spawn(
async {
let conn = zbus::connection::Builder::session()?
.name(Self::well_known_name())?
.serve_at(Self::application_service_path(), ApplicationService { tx })?
// Instead of having zbus spawn a thread to poll for new
// messages, we'll poll on our own executor.
.internal_executor(false)
.build()
.await?;
loop {
conn.executor().tick().await;
}
}
.map(|result: anyhow::Result<()>| {
if let Err(err) = result {
log::error!(
"Failed to initialize org.freedesktop.Application D-Bus service: {err:#}"
);
}
}),
);
// Process any events that we receive over D-Bus.
ctx.spawn_stream_local(rx, |_, event, ctx| {
match event {
ApplicationServiceEvent::Open { uris } => {
for uri in uris {
match url::Url::parse(&uri) {
Ok(uri) => crate::uri::handle_incoming_uri(&uri, ctx),
Err(err) => log::warn!("Failed to parse URI when handling org.freedesktop.Application/open: {err:#}"),
}
}
},
}
}, |_, _| {});
Self {
server_task: Some(server_task),
}
}
fn terminate(&mut self) {
if let Some(server_task) = self.server_task.take() {
server_task.abort();
// Wait until we've torn down the dbus service.
report_if_error!(galaxyui::r#async::block_on(server_task));
}
}
/// Returns the D-Bus well-known name that should be used.
fn well_known_name() -> String {
ChannelState::app_id().to_string()
}
/// Returns the path under which the org.freedesktop.Application interface
/// will be hosted.
fn application_service_path() -> String {
format!("/{}", Self::well_known_name().split('.').join("/"))
}
}
impl galaxyui::Entity for DBusServiceHost {
type Event = ();
}
impl SingletonEntity for DBusServiceHost {}