From 2561342e0c66dd45951d8f2f4effba16fcdf0107 Mon Sep 17 00:00:00 2001 From: Val Packett Date: Fri, 6 Feb 2026 00:31:15 -0300 Subject: [PATCH 1/6] Add FileTransfer (drag & drop / copy & paste files) Host->Guest is easy; Guest->Host is not there yet because we have to do something for the (O_PATH) fd passing such as converting to inode numbers and getting an O_PATH fd back through the VMM, which requires modifying libkrun/muvm to have a connection channel.. Or we should switch this from vsock to a virtgpu channel where it would be easier to handle. --- sidebus-broker/src/main.rs | 41 +++++- sidebus-broker/src/portal/file_chooser.rs | 36 +++-- sidebus-broker/src/portal/file_transfer.rs | 157 +++++++++++++++++++++ sidebus-broker/src/portal/mod.rs | 1 + sidebus-broker/src/vsock.rs | 1 + 5 files changed, 216 insertions(+), 20 deletions(-) create mode 100644 sidebus-broker/src/portal/file_transfer.rs diff --git a/sidebus-broker/src/main.rs b/sidebus-broker/src/main.rs index 7385eff..194f2af 100644 --- a/sidebus-broker/src/main.rs +++ b/sidebus-broker/src/main.rs @@ -144,6 +144,12 @@ async fn main() -> eyre::Result<()> { let priv_bus_conn = priv_bus.lock().await.connect_channel(false).await?; let host_session_conn = zbus::connection::Builder::session()?.build().await?; let file_chooser_imp = portal::file_chooser::FileChooser::new( + &host_session_conn, + &priv_bus_conn, + cli.guest_mountpoint.clone(), + ) + .await?; + let file_transfer_imp = portal::file_transfer::FileTransfer::new( &host_session_conn, &priv_bus_conn, cli.guest_mountpoint, @@ -154,6 +160,7 @@ async fn main() -> eyre::Result<()> { async fn on_vm_bus_connected( vm_bus_conn: zbus::Connection, file_chooser: portal::file_chooser::FileChooser, + file_transfer: portal::file_transfer::FileTransfer, settings: portal::settings::Settings, ) -> Result<(), eyre::Report> { if !vm_bus_conn @@ -163,6 +170,28 @@ async fn main() -> eyre::Result<()> { { error!("org.freedesktop.portal.FileChooser already provided"); }; + + if !vm_bus_conn + .object_server() + .at("/org/freedesktop/portal/documents", file_transfer) + .await? + { + error!("org.freedesktop.portal.FileTransfer already provided"); + }; + let file_transfer_ref = vm_bus_conn + .object_server() + .interface::<_, portal::file_transfer::FileTransfer>( + "/org/freedesktop/portal/documents", + ) + .await?; + tokio::spawn(async move { + let file_transfer = file_transfer_ref.get().await; + let emitter = file_transfer_ref.signal_emitter(); + if let Err(err) = file_transfer.forward_transfer_closed(emitter.clone()).await { + error!(%err, "forwarding forward_transfer_closed changes ended"); + } + }); + if !vm_bus_conn .object_server() .at("/org/freedesktop/portal/desktop", settings) @@ -181,21 +210,23 @@ async fn main() -> eyre::Result<()> { error!(%err, "forwarding settings changes ended"); } }); + // XXX: no method for "wait until the conn dies"? Ok(std::future::pending::<()>().await) } if let Some(path) = cli.unix_path { let vm_unix_listener = UnixListener::bind(path)?; - server_tasks.spawn(enclose!((file_chooser_imp, settings_imp) async move { + server_tasks.spawn(enclose!((file_chooser_imp, file_transfer_imp, settings_imp) async move { while let Ok((socket, remote_addr)) = vm_unix_listener.accept().await { - let f = enclose!((file_chooser_imp, settings_imp) async move { + let f = enclose!((file_chooser_imp, file_transfer_imp, settings_imp) async move { let client_conn = zbus::connection::Builder::unix_stream(socket) .auth_mechanism(zbus::AuthMechanism::Anonymous) .name("org.freedesktop.portal.Desktop")? + .name("org.freedesktop.portal.Documents")? .build() .await?; - on_vm_bus_connected(client_conn, file_chooser_imp, settings_imp).await + on_vm_bus_connected(client_conn, file_chooser_imp, file_transfer_imp, settings_imp).await }); tokio::spawn( async { @@ -216,10 +247,10 @@ async fn main() -> eyre::Result<()> { vsock::ListenerBuilder::new(vsock::VsockAddr::new(vsock::VMADDR_CID_HOST, port)) .with_label("VM Bus") .listen(move |client| { - enclose!((file_chooser_imp, settings_imp) async move { + enclose!((file_chooser_imp, file_transfer_imp, settings_imp) async move { // TODO: Not necessary to go through the channel, add vsock support to the Peer too? let client_conn = client.build().await?; - on_vm_bus_connected(client_conn, file_chooser_imp, settings_imp).await + on_vm_bus_connected(client_conn, file_chooser_imp, file_transfer_imp, settings_imp).await }) }) .map_ok_or_else( diff --git a/sidebus-broker/src/portal/file_chooser.rs b/sidebus-broker/src/portal/file_chooser.rs index 09662cc..6121da8 100644 --- a/sidebus-broker/src/portal/file_chooser.rs +++ b/sidebus-broker/src/portal/file_chooser.rs @@ -53,6 +53,7 @@ impl FileChooser { docs: self.docs.clone(), guest_root: self.guest_root.clone(), for_save: false, + persistent: true, directory: options.get_as("directory")?.unwrap_or(false), }) .perform(async || self.host.open_file(parent_window, title, options).await) @@ -73,6 +74,7 @@ impl FileChooser { docs: self.docs.clone(), guest_root: self.guest_root.clone(), for_save: true, + persistent: true, directory: false, }) .perform(async || self.host.save_file(parent_window, title, options).await) @@ -93,6 +95,7 @@ impl FileChooser { docs: self.docs.clone(), guest_root: self.guest_root.clone(), for_save: true, + persistent: true, directory: false, }) .perform(async || self.host.save_files(parent_window, title, options).await) @@ -106,11 +109,13 @@ impl FileChooser { } } -struct FileTransformer { - docs: DocumentsProxy<'static>, - guest_root: PathBuf, - for_save: bool, - directory: bool, +#[derive(Clone)] +pub struct FileTransformer { + pub docs: DocumentsProxy<'static>, + pub guest_root: PathBuf, + pub for_save: bool, + pub persistent: bool, + pub directory: bool, } // ref: send_response_in_thread_func @@ -134,6 +139,14 @@ impl ResultTransformer for FileTransformer { .async_map(|u| self.add_path_as_doc(u)) .await .flatten() + .map(|path| match url::Url::from_file_path(&path) { + Ok(url) => Some(url.to_string()), + Err(err) => { + warn!(?err, ?path, "could not make url from returned path"); + None + } + }) + .flatten() .collect::>(); results.insert("uris", guest_uris.into()); @@ -150,7 +163,7 @@ const DIRECTORY: u32 = 1 << 3; // https://github.com/flatpak/xdg-desktop-portal/blob/10e712e06aa8eb9cd0e59c73c5be62ba53e981a4/src/xdp-documents.c#L71 impl FileTransformer { - async fn add_path_as_doc(&self, path: PathBuf) -> Option { + pub async fn add_path_as_doc(&self, path: PathBuf) -> Option { use rustix::fs::{Mode, OFlags}; let o_path_fd = match rustix::fs::open( @@ -166,8 +179,8 @@ impl FileTransformer { }; let flags = REUSE_EXISTING - | PERSISTENT | AS_NEEDED_BY_APP + | if self.persistent { PERSISTENT } else { 0 } | if self.directory { DIRECTORY } else { 0 }; // XXX: portal impl can return writable=false but host frontend does not pass that back.. @@ -212,14 +225,7 @@ impl FileTransformer { return None; } }; - let path = self.guest_root.join(doc_id).join(filename); - match url::Url::from_file_path(&path) { - Ok(url) => Some(url.to_string()), - Err(err) => { - warn!(?err, ?path, "could not make url from returned path"); - None - } - } + Some(self.guest_root.join(doc_id).join(filename)) } } diff --git a/sidebus-broker/src/portal/file_transfer.rs b/sidebus-broker/src/portal/file_transfer.rs new file mode 100644 index 0000000..9695b6d --- /dev/null +++ b/sidebus-broker/src/portal/file_transfer.rs @@ -0,0 +1,157 @@ +use std::{collections::HashMap, path::PathBuf}; + +use tokio::sync::broadcast; +use tokio_stream::StreamExt; +use tracing::{debug, error}; +use zbus::{ + Connection, fdo::Result, names::OwnedUniqueName, object_server::SignalEmitter, zvariant, +}; + +use super::{documents::DocumentsProxy, file_chooser::FileTransformer}; + +#[derive(Clone)] +pub struct FileTransfer { + host: FileTransferProxy<'static>, + file_transformer: FileTransformer, + tx: broadcast::Sender, +} + +#[derive(Clone, Debug)] +enum ForwarderCommand { + Add(String, OwnedUniqueName), + // Remove(String), +} + +#[zbus::interface( + name = "org.freedesktop.portal.FileTransfer", + proxy( + default_service = "org.freedesktop.portal.Documents", + default_path = "/org/freedesktop/portal/documents" + ) +)] +impl FileTransfer { + async fn add_files( + &self, + _key: &str, + _fds: Vec>, + _options: HashMap<&str, zvariant::Value<'_>>, + ) -> Result<()> { + Err(zbus::fdo::Error::NotSupported( + "Adding files to transfer out is not yet implemented".to_owned(), + )) + } + + async fn retrieve_files( + &self, + key: &str, + options: HashMap<&str, zvariant::Value<'_>>, + ) -> Result> { + let host_paths = self.host.retrieve_files(key, options).await?; + let mut result = Vec::with_capacity(host_paths.len()); + for host_path in host_paths { + if let Some(guest_path) = self + .file_transformer + .add_path_as_doc(PathBuf::from(&host_path)) + .await + { + result.push(guest_path.to_string_lossy().into_owned()); + } else { + debug!("could not expose path {host_path}"); + } + } + Ok(result) + } + + async fn start_transfer( + &self, + #[zbus(header)] hdr: zbus::message::Header<'_>, + options: HashMap<&str, zvariant::Value<'_>>, + ) -> Result { + let sender = hdr + .sender() + .ok_or_else(|| zbus::Error::MissingField)? + .to_owned(); + let key = self.host.start_transfer(options).await?; + debug!("start_transfer: {key}"); + if let Err(err) = self + .tx + .send(ForwarderCommand::Add(key.clone(), sender.into())) + { + error!("file_transfer internal channel error: {err:?}"); + return Err(zbus::fdo::Error::IOError("channel error".to_owned())); + } + Ok(key) + } + + async fn stop_transfer(&self, key: &str) -> Result<()> { + debug!("stop_transfer: {key}"); + self.host.stop_transfer(key).await + } + + #[zbus(signal)] + async fn transfer_closed(signal_emitter: &SignalEmitter<'_>, key: &str) -> zbus::Result<()>; + + #[zbus(property, name = "version")] + fn version(&self) -> Result { + Ok(1) + } +} + +impl FileTransfer { + pub async fn new( + host_session_conn: &Connection, + priv_conn: &Connection, + guest_root: PathBuf, + ) -> Result { + let host = FileTransferProxy::builder(host_session_conn) + .build() + .await?; + let docs = DocumentsProxy::builder(priv_conn).build().await?; + let file_transformer = FileTransformer { + docs, + guest_root, + for_save: false, + persistent: false, + directory: false, + }; + let (tx, _) = broadcast::channel(8); + Ok(FileTransfer { + host, + file_transformer, + tx, + }) + } + + pub async fn forward_transfer_closed( + &self, + mut signal_emitter: SignalEmitter<'static>, + ) -> Result<()> { + let mut stream = self.host.receive_transfer_closed().await?; + let mut cmds = self.tx.subscribe(); + let mut receivers = HashMap::new(); + + loop { + tokio::select! { + Ok(cmd) = cmds.recv() => match cmd { + ForwarderCommand::Add(key, receiver) => { receivers.insert(key, receiver); }, + // ForwarderCommand::Remove(key) => { receivers.remove(&key); }, + }, + Some(signal) = stream.next() => { + debug!("transfer closed {signal:?}"); + if let Ok((key,)) = signal.0.deserialize::<(&str,)>() { + if let Some(bus_name) = receivers.remove(key) { + signal_emitter = signal_emitter.set_destination(zbus::names::BusName::Unique(bus_name.clone().into())); + if let Err(err) = FileTransfer::transfer_closed(&signal_emitter, key).await { + error!("could not forward signal for key {key}: {err:?}"); + } + } else { + error!("got a signal for unknown key {key}"); + } + } else { + error!("could not deserialize transfer closed signal"); + }; + } + } + } + } +} diff --git a/sidebus-broker/src/portal/mod.rs b/sidebus-broker/src/portal/mod.rs index 2345ef0..be14b9f 100644 --- a/sidebus-broker/src/portal/mod.rs +++ b/sidebus-broker/src/portal/mod.rs @@ -1,4 +1,5 @@ pub mod documents; pub mod file_chooser; +pub mod file_transfer; pub mod request; pub mod settings; diff --git a/sidebus-broker/src/vsock.rs b/sidebus-broker/src/vsock.rs index 65532c7..23d6c54 100644 --- a/sidebus-broker/src/vsock.rs +++ b/sidebus-broker/src/vsock.rs @@ -17,6 +17,7 @@ impl ConnectionBuilder { zbus::connection::Builder::vsock_stream(self.socket) .auth_mechanism(zbus::AuthMechanism::Anonymous) .name("org.freedesktop.portal.Desktop")? + .name("org.freedesktop.portal.Documents")? .build() .await .map_err(|e| e.into()) From 26261306592f6173fb844083b396bebf8140cb2b Mon Sep 17 00:00:00 2001 From: Val Packett Date: Fri, 6 Feb 2026 05:05:03 -0300 Subject: [PATCH 2/6] Add Notification support --- sidebus-broker/src/main.rs | 31 ++++++-- sidebus-broker/src/portal/mod.rs | 1 + sidebus-broker/src/portal/notification.rs | 95 +++++++++++++++++++++++ 3 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 sidebus-broker/src/portal/notification.rs diff --git a/sidebus-broker/src/main.rs b/sidebus-broker/src/main.rs index 194f2af..fc79898 100644 --- a/sidebus-broker/src/main.rs +++ b/sidebus-broker/src/main.rs @@ -155,12 +155,14 @@ async fn main() -> eyre::Result<()> { cli.guest_mountpoint, ) .await?; + let notification_imp = portal::notification::Notification::new(&host_session_conn).await?; let settings_imp = portal::settings::Settings::new(&host_session_conn).await?; async fn on_vm_bus_connected( vm_bus_conn: zbus::Connection, file_chooser: portal::file_chooser::FileChooser, file_transfer: portal::file_transfer::FileTransfer, + notification: portal::notification::Notification, settings: portal::settings::Settings, ) -> Result<(), eyre::Report> { if !vm_bus_conn @@ -192,6 +194,25 @@ async fn main() -> eyre::Result<()> { } }); + if !vm_bus_conn + .object_server() + .at("/org/freedesktop/portal/desktop", notification) + .await? + { + error!("org.freedesktop.portal.Notification already provided"); + }; + let notification_ref = vm_bus_conn + .object_server() + .interface::<_, portal::notification::Notification>("/org/freedesktop/portal/desktop") + .await?; + tokio::spawn(async move { + let notification = notification_ref.get().await; + let emitter = notification_ref.signal_emitter(); + if let Err(err) = notification.forward_actions(emitter.clone()).await { + error!(%err, "forwarding notification changes ended"); + } + }); + if !vm_bus_conn .object_server() .at("/org/freedesktop/portal/desktop", settings) @@ -217,16 +238,16 @@ async fn main() -> eyre::Result<()> { if let Some(path) = cli.unix_path { let vm_unix_listener = UnixListener::bind(path)?; - server_tasks.spawn(enclose!((file_chooser_imp, file_transfer_imp, settings_imp) async move { + server_tasks.spawn(enclose!((file_chooser_imp, file_transfer_imp, notification_imp, settings_imp) async move { while let Ok((socket, remote_addr)) = vm_unix_listener.accept().await { - let f = enclose!((file_chooser_imp, file_transfer_imp, settings_imp) async move { + let f = enclose!((file_chooser_imp, file_transfer_imp, notification_imp, settings_imp) async move { let client_conn = zbus::connection::Builder::unix_stream(socket) .auth_mechanism(zbus::AuthMechanism::Anonymous) .name("org.freedesktop.portal.Desktop")? .name("org.freedesktop.portal.Documents")? .build() .await?; - on_vm_bus_connected(client_conn, file_chooser_imp, file_transfer_imp, settings_imp).await + on_vm_bus_connected(client_conn, file_chooser_imp, file_transfer_imp, notification_imp, settings_imp).await }); tokio::spawn( async { @@ -247,10 +268,10 @@ async fn main() -> eyre::Result<()> { vsock::ListenerBuilder::new(vsock::VsockAddr::new(vsock::VMADDR_CID_HOST, port)) .with_label("VM Bus") .listen(move |client| { - enclose!((file_chooser_imp, file_transfer_imp, settings_imp) async move { + enclose!((file_chooser_imp, file_transfer_imp, notification_imp, settings_imp) async move { // TODO: Not necessary to go through the channel, add vsock support to the Peer too? let client_conn = client.build().await?; - on_vm_bus_connected(client_conn, file_chooser_imp, file_transfer_imp, settings_imp).await + on_vm_bus_connected(client_conn, file_chooser_imp, file_transfer_imp, notification_imp, settings_imp).await }) }) .map_ok_or_else( diff --git a/sidebus-broker/src/portal/mod.rs b/sidebus-broker/src/portal/mod.rs index be14b9f..0f097ac 100644 --- a/sidebus-broker/src/portal/mod.rs +++ b/sidebus-broker/src/portal/mod.rs @@ -1,5 +1,6 @@ pub mod documents; pub mod file_chooser; pub mod file_transfer; +pub mod notification; pub mod request; pub mod settings; diff --git a/sidebus-broker/src/portal/notification.rs b/sidebus-broker/src/portal/notification.rs new file mode 100644 index 0000000..0d41850 --- /dev/null +++ b/sidebus-broker/src/portal/notification.rs @@ -0,0 +1,95 @@ +use std::collections::HashMap; + +use tokio_stream::StreamExt; +use tracing::warn; +use zbus::{Connection, fdo::Result, names::UniqueName, object_server::SignalEmitter, zvariant}; + +#[derive(Clone)] +pub struct Notification { + host: NotificationProxy<'static>, +} + +#[zbus::interface( + name = "org.freedesktop.portal.Notification", + proxy( + default_service = "org.freedesktop.portal.Desktop", + default_path = "/org/freedesktop/portal/desktop" + ) +)] +impl Notification { + async fn add_notification( + &self, + #[zbus(header)] hdr: zbus::message::Header<'_>, + id: &str, + notification: HashMap<&str, zvariant::Value<'_>>, + ) -> Result<()> { + let sender = hdr.sender().ok_or_else(|| zbus::Error::MissingField)?; + self.host + .add_notification( + &format!("{sender}\x0C\x0CSIDEBUS\x0C\x0C{id}"), + notification, + ) + .await + } + + async fn remove_notification( + &self, + #[zbus(header)] hdr: zbus::message::Header<'_>, + id: &str, + ) -> Result<()> { + let sender = hdr.sender().ok_or_else(|| zbus::Error::MissingField)?; + self.host + .remove_notification(&format!("{sender}\x0C\x0CSIDEBUS\x0C\x0C{id}")) + .await + } + + #[zbus(signal)] + async fn action_invoked( + signal_emitter: &SignalEmitter<'_>, + id: &str, + action: &str, + parameter: Vec>, + ) -> zbus::Result<()>; + + #[zbus(property)] + async fn supported_options(&self) -> Result> { + self.host + .supported_options() + .await + .map_err(|err| err.into()) + } + + #[zbus(property, name = "version")] + fn version(&self) -> Result { + Ok(2) + } +} + +impl Notification { + pub async fn new(host_session_conn: &Connection) -> Result { + let host = NotificationProxy::builder(host_session_conn) + .build() + .await?; + Ok(Self { host }) + } + + pub async fn forward_actions(&self, mut signal_emitter: SignalEmitter<'static>) -> Result<()> { + let mut stream = self.host.receive_action_invoked().await?; + while let Some(x) = stream.next().await { + let args = x.args()?; + let mut split = args.id.split("\x0C\x0CSIDEBUS\x0C\x0C"); + let sender = split + .next() + .and_then(|x| UniqueName::try_from(x).ok().map(|x| x.to_owned())) + .ok_or_else(|| zbus::fdo::Error::Failed("bad ID".to_owned()))?; + let id = split + .next() + .ok_or_else(|| zbus::fdo::Error::Failed("bad ID".to_owned()))?; + signal_emitter = signal_emitter.set_destination(sender.into()); + Notification::action_invoked(&signal_emitter, id, args.action, args.parameter).await?; + () + } + warn!("actions stream end"); + Ok(()) + } +} From 95bc64076d775efc5844557a469a4cde49c68063 Mon Sep 17 00:00:00 2001 From: Val Packett Date: Thu, 12 Feb 2026 22:15:40 -0300 Subject: [PATCH 3/6] Add file path mapping for FileTransfer Match by longest prefix to get host paths from guest ones --- sidebus-broker/src/main.rs | 26 ++++++++ sidebus-broker/src/portal/file_transfer.rs | 76 ++++++++++++++++++---- 2 files changed, 88 insertions(+), 14 deletions(-) diff --git a/sidebus-broker/src/main.rs b/sidebus-broker/src/main.rs index fc79898..5946f48 100644 --- a/sidebus-broker/src/main.rs +++ b/sidebus-broker/src/main.rs @@ -4,6 +4,7 @@ mod vsock; use bus::SharedHostedBus; use clap::Parser; +use eyre::OptionExt; use futures::{TryFutureExt, stream::FuturesUnordered}; use std::{path::PathBuf, sync::Arc, time::Duration}; use tokio::{net::UnixListener, process::Command, sync::Mutex}; @@ -36,6 +37,10 @@ struct BrokerCli { #[clap(long, default_value = "/run/vm-doc-portal")] guest_mountpoint: PathBuf, + /// Mappings from guest paths to host paths for passthrough file systems (for file transfer), in guest=host format + #[clap(long)] + path_mapping: Vec, + /// Vsock port number to listen on for the VM bus #[clap(long)] vsock_port: Option, @@ -65,11 +70,31 @@ async fn new_hosted_bus() -> eyre::Result<( Ok((Arc::new(Mutex::new(bus)), guid, owner_stream)) } +fn parse_path_mapping(s: &str) -> eyre::Result<(PathBuf, PathBuf)> { + let mut split = s.split('='); + let guest_path = PathBuf::from(split.next().ok_or_eyre("failed to split mapping")?); + let host_path = PathBuf::from(split.next().ok_or_eyre("failed to split mapping")?); + Ok((guest_path, host_path)) +} + #[tokio::main] async fn main() -> eyre::Result<()> { tracing_subscriber::fmt::init(); let cli = BrokerCli::parse(); + let mut path_prefix_to_host: Vec<(PathBuf, PathBuf)> = cli + .path_mapping + .iter() + .flat_map(|arg| match parse_path_mapping(arg) { + Ok(mapping) => Some(mapping), + Err(err) => { + error!(?err, %arg, "could not parse path mapping"); + None + } + }) + .collect(); + path_prefix_to_host.sort_unstable_by_key(|(prefix, _)| -(prefix.as_os_str().len() as isize)); + debug!(?path_prefix_to_host, "parsed path mappings"); let (priv_bus, _, mut priv_lst) = new_hosted_bus().await?; @@ -153,6 +178,7 @@ async fn main() -> eyre::Result<()> { &host_session_conn, &priv_bus_conn, cli.guest_mountpoint, + path_prefix_to_host, ) .await?; let notification_imp = portal::notification::Notification::new(&host_session_conn).await?; diff --git a/sidebus-broker/src/portal/file_transfer.rs b/sidebus-broker/src/portal/file_transfer.rs index 9695b6d..8c175f6 100644 --- a/sidebus-broker/src/portal/file_transfer.rs +++ b/sidebus-broker/src/portal/file_transfer.rs @@ -1,4 +1,9 @@ -use std::{collections::HashMap, path::PathBuf}; +use std::{ + collections::HashMap, + os::fd::{AsFd, AsRawFd}, + os::unix::ffi::OsStrExt, + path::PathBuf, +}; use tokio::sync::broadcast; use tokio_stream::StreamExt; @@ -14,6 +19,7 @@ pub struct FileTransfer { host: FileTransferProxy<'static>, file_transformer: FileTransformer, tx: broadcast::Sender, + path_prefix_to_host: Vec<(PathBuf, PathBuf)>, } #[derive(Clone, Debug)] @@ -32,13 +38,53 @@ enum ForwarderCommand { impl FileTransfer { async fn add_files( &self, - _key: &str, - _fds: Vec>, - _options: HashMap<&str, zvariant::Value<'_>>, + key: &str, + fds: Vec>, + options: HashMap<&str, zvariant::Value<'_>>, ) -> Result<()> { - Err(zbus::fdo::Error::NotSupported( - "Adding files to transfer out is not yet implemented".to_owned(), - )) + let mut host_paths = Vec::with_capacity(fds.len()); + for fd in fds.iter() { + let link = rustix::fs::readlink( + format!("/proc/self/fd/{}", fd.as_fd().as_raw_fd()), + Vec::new(), + ) + .map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?; + let guest_path = std::path::PathBuf::from(std::ffi::OsStr::from_bytes( + &link.to_string_lossy().as_bytes(), + )); + let (prefix, host_prefix) = self + .path_prefix_to_host + .iter() + .find(|(prefix, _)| guest_path.starts_with(prefix)) + .ok_or_else(|| { + zbus::fdo::Error::Failed("Could not find host mapping for path".to_owned()) + })?; + let guest_suffix = guest_path + .strip_prefix(prefix) + .map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?; + let host_path = if guest_suffix.as_os_str().is_empty() { + // Edge case: a bind-mounted file exposed at the same path would get an extra '/' after its path + host_prefix.to_path_buf() + } else { + host_prefix.join(guest_suffix) + }; + debug!( + ?guest_path, + ?prefix, + ?guest_suffix, + ?host_prefix, + ?host_path, + "mapped path" + ); + let path_fd = rustix::fs::open( + host_path, + rustix::fs::OFlags::PATH, + rustix::fs::Mode::empty(), + ) + .map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?; + host_paths.push(path_fd.into()); // OwnedFd variant of zbus's Fd enum, so still owned by the Vec + } + self.host.add_files(key, host_paths, options).await } async fn retrieve_files( @@ -56,7 +102,7 @@ impl FileTransfer { { result.push(guest_path.to_string_lossy().into_owned()); } else { - debug!("could not expose path {host_path}"); + debug!(%host_path, "could not add path as doc to retrieve file"); } } Ok(result) @@ -72,19 +118,19 @@ impl FileTransfer { .ok_or_else(|| zbus::Error::MissingField)? .to_owned(); let key = self.host.start_transfer(options).await?; - debug!("start_transfer: {key}"); + debug!(%key, %sender, "started transfer"); if let Err(err) = self .tx .send(ForwarderCommand::Add(key.clone(), sender.into())) { - error!("file_transfer internal channel error: {err:?}"); + error!(?err, "file_transfer internal channel error"); return Err(zbus::fdo::Error::IOError("channel error".to_owned())); } Ok(key) } async fn stop_transfer(&self, key: &str) -> Result<()> { - debug!("stop_transfer: {key}"); + debug!(%key, "stopping transfer"); self.host.stop_transfer(key).await } @@ -102,6 +148,7 @@ impl FileTransfer { host_session_conn: &Connection, priv_conn: &Connection, guest_root: PathBuf, + path_prefix_to_host: Vec<(PathBuf, PathBuf)>, ) -> Result { let host = FileTransferProxy::builder(host_session_conn) .build() @@ -119,6 +166,7 @@ impl FileTransfer { host, file_transformer, tx, + path_prefix_to_host, }) } @@ -137,15 +185,15 @@ impl FileTransfer { // ForwarderCommand::Remove(key) => { receivers.remove(&key); }, }, Some(signal) = stream.next() => { - debug!("transfer closed {signal:?}"); + debug!(?signal, "transfer closed"); if let Ok((key,)) = signal.0.deserialize::<(&str,)>() { if let Some(bus_name) = receivers.remove(key) { signal_emitter = signal_emitter.set_destination(zbus::names::BusName::Unique(bus_name.clone().into())); if let Err(err) = FileTransfer::transfer_closed(&signal_emitter, key).await { - error!("could not forward signal for key {key}: {err:?}"); + error!(?err, %key, "could not forward signal"); } } else { - error!("got a signal for unknown key {key}"); + error!(%key, "got a signal for unknown key"); } } else { error!("could not deserialize transfer closed signal"); From 52a0ccee0d6fec65da7e42742525ef508cb5dd28 Mon Sep 17 00:00:00 2001 From: Val Packett Date: Fri, 27 Feb 2026 05:27:53 -0300 Subject: [PATCH 4/6] Add Print support --- sidebus-broker/src/main.rs | 20 ++++++--- sidebus-broker/src/portal/mod.rs | 1 + sidebus-broker/src/portal/print.rs | 66 ++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 5 deletions(-) create mode 100644 sidebus-broker/src/portal/print.rs diff --git a/sidebus-broker/src/main.rs b/sidebus-broker/src/main.rs index 5946f48..875f480 100644 --- a/sidebus-broker/src/main.rs +++ b/sidebus-broker/src/main.rs @@ -182,6 +182,7 @@ async fn main() -> eyre::Result<()> { ) .await?; let notification_imp = portal::notification::Notification::new(&host_session_conn).await?; + let print_imp = portal::print::Print::new(&host_session_conn).await?; let settings_imp = portal::settings::Settings::new(&host_session_conn).await?; async fn on_vm_bus_connected( @@ -189,6 +190,7 @@ async fn main() -> eyre::Result<()> { file_chooser: portal::file_chooser::FileChooser, file_transfer: portal::file_transfer::FileTransfer, notification: portal::notification::Notification, + print: portal::print::Print, settings: portal::settings::Settings, ) -> Result<(), eyre::Report> { if !vm_bus_conn @@ -239,6 +241,14 @@ async fn main() -> eyre::Result<()> { } }); + if !vm_bus_conn + .object_server() + .at("/org/freedesktop/portal/desktop", print) + .await? + { + error!("org.freedesktop.portal.Print already provided"); + }; + if !vm_bus_conn .object_server() .at("/org/freedesktop/portal/desktop", settings) @@ -264,16 +274,16 @@ async fn main() -> eyre::Result<()> { if let Some(path) = cli.unix_path { let vm_unix_listener = UnixListener::bind(path)?; - server_tasks.spawn(enclose!((file_chooser_imp, file_transfer_imp, notification_imp, settings_imp) async move { + server_tasks.spawn(enclose!((file_chooser_imp, file_transfer_imp, notification_imp, print_imp, settings_imp) async move { while let Ok((socket, remote_addr)) = vm_unix_listener.accept().await { - let f = enclose!((file_chooser_imp, file_transfer_imp, notification_imp, settings_imp) async move { + let f = enclose!((file_chooser_imp, file_transfer_imp, notification_imp, print_imp, settings_imp) async move { let client_conn = zbus::connection::Builder::unix_stream(socket) .auth_mechanism(zbus::AuthMechanism::Anonymous) .name("org.freedesktop.portal.Desktop")? .name("org.freedesktop.portal.Documents")? .build() .await?; - on_vm_bus_connected(client_conn, file_chooser_imp, file_transfer_imp, notification_imp, settings_imp).await + on_vm_bus_connected(client_conn, file_chooser_imp, file_transfer_imp, notification_imp, print_imp, settings_imp).await }); tokio::spawn( async { @@ -294,10 +304,10 @@ async fn main() -> eyre::Result<()> { vsock::ListenerBuilder::new(vsock::VsockAddr::new(vsock::VMADDR_CID_HOST, port)) .with_label("VM Bus") .listen(move |client| { - enclose!((file_chooser_imp, file_transfer_imp, notification_imp, settings_imp) async move { + enclose!((file_chooser_imp, file_transfer_imp, notification_imp, print_imp, settings_imp) async move { // TODO: Not necessary to go through the channel, add vsock support to the Peer too? let client_conn = client.build().await?; - on_vm_bus_connected(client_conn, file_chooser_imp, file_transfer_imp, notification_imp, settings_imp).await + on_vm_bus_connected(client_conn, file_chooser_imp, file_transfer_imp, notification_imp, print_imp, settings_imp).await }) }) .map_ok_or_else( diff --git a/sidebus-broker/src/portal/mod.rs b/sidebus-broker/src/portal/mod.rs index 0f097ac..d43de89 100644 --- a/sidebus-broker/src/portal/mod.rs +++ b/sidebus-broker/src/portal/mod.rs @@ -2,5 +2,6 @@ pub mod documents; pub mod file_chooser; pub mod file_transfer; pub mod notification; +pub mod print; pub mod request; pub mod settings; diff --git a/sidebus-broker/src/portal/print.rs b/sidebus-broker/src/portal/print.rs new file mode 100644 index 0000000..7c77881 --- /dev/null +++ b/sidebus-broker/src/portal/print.rs @@ -0,0 +1,66 @@ +use std::collections::HashMap; + +use zbus::{Connection, ObjectServer, fdo::Result, zvariant}; + +use super::request::ReqHandler; + +#[derive(Clone)] +pub struct Print { + host: PrintProxy<'static>, +} + +#[zbus::interface( + name = "org.freedesktop.portal.Print", + proxy( + default_service = "org.freedesktop.portal.Desktop", + default_path = "/org/freedesktop/portal/desktop" + ) +)] +impl Print { + async fn prepare_print( + &self, + #[zbus(header)] hdr: zbus::message::Header<'_>, + #[zbus(object_server)] server: &ObjectServer, + #[zbus(connection)] conn: &Connection, + parent_window: &str, + title: &str, + settings: HashMap<&str, zvariant::Value<'_>>, + page_setup: HashMap<&str, zvariant::Value<'_>>, + options: HashMap<&str, zvariant::Value<'_>>, + ) -> Result { + ReqHandler::prepare(&self.host, hdr, server, conn, &options) + .perform(async || { + self.host + .prepare_print(parent_window, title, settings, page_setup, options) + .await + }) + .await + } + + async fn print( + &self, + #[zbus(header)] hdr: zbus::message::Header<'_>, + #[zbus(object_server)] server: &ObjectServer, + #[zbus(connection)] conn: &Connection, + parent_window: &str, + title: &str, + fd: zvariant::Fd<'_>, + options: HashMap<&str, zvariant::Value<'_>>, + ) -> Result { + ReqHandler::prepare(&self.host, hdr, server, conn, &options) + .perform(async || self.host.print(parent_window, title, fd, options).await) + .await + } + + #[zbus(property, name = "version")] + fn version(&self) -> Result { + Ok(3) + } +} + +impl Print { + pub async fn new(host_session_conn: &Connection) -> Result { + let host = PrintProxy::builder(host_session_conn).build().await?; + Ok(Self { host }) + } +} From eedf1f889da4ab81d5e0bd798fb3b7786741f895 Mon Sep 17 00:00:00 2001 From: Val Packett Date: Fri, 27 Feb 2026 05:29:35 -0300 Subject: [PATCH 5/6] Update zbus to 5.0 --- Cargo.lock | 89 ++++++++++++++++++++++++++++++------------------------ Cargo.toml | 4 +-- flake.nix | 3 +- 3 files changed, 52 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7231309..3d963c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -142,8 +142,8 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "busd" -version = "0.4.0" -source = "git+https://github.com/valpackett/busd?branch=val%2Fmsksqvsqqrxm#25736c855284b13371f0be2ef38f16af8f73bda1" +version = "0.5.0" +source = "git+https://github.com/valpackett/busd?branch=val%2Fmsksqvsqqrxm#7084025107e02600043856c56fd178730526daad" dependencies = [ "anyhow", "clap", @@ -151,8 +151,8 @@ dependencies = [ "event-listener", "fastrand", "futures-util", - "nix 0.30.1", "quick-xml", + "rustix", "serde", "tokio", "tracing", @@ -641,9 +641,9 @@ checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "listenfd" @@ -726,19 +726,6 @@ dependencies = [ "memoffset", ] -[[package]] -name = "nix" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset", -] - [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -871,9 +858,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.38.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8927b0664f5c5a98265138b7e3f90aa19a6b21353182469ace36d4ac527b7b1b" +checksum = "f2e3bf4aa9d243beeb01a7b3bc30b77cfe2c44e24ec02d751a7104a53c2c49a1" dependencies = [ "memchr", "serde", @@ -940,15 +927,15 @@ checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1322,7 +1309,9 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" dependencies = [ + "getrandom", "js-sys", + "serde", "wasm-bindgen", ] @@ -1339,7 +1328,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e8b4d00e672f147fc86a09738fadb1445bd1c0a40542378dfb82909deeee688" dependencies = [ "libc", - "nix 0.29.0", + "nix", ] [[package]] @@ -1437,6 +1426,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-sys" version = "0.52.0" @@ -1464,6 +1459,15 @@ dependencies = [ "windows-targets 0.53.2", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -1652,8 +1656,9 @@ dependencies = [ [[package]] name = "zbus" -version = "5.9.0" -source = "git+https://github.com/dbus2/zbus#6da6b1b5f528fe2d14b0f25ae1dca1a9fd31575c" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" dependencies = [ "async-broadcast", "async-recursion", @@ -1663,16 +1668,17 @@ dependencies = [ "futures-core", "futures-lite", "hex", - "nix 0.30.1", + "libc", "ordered-stream", - "rand", + "rustix", "serde", "serde_repr", "tokio", "tokio-vsock", "tracing", "uds_windows", - "windows-sys 0.60.2", + "uuid", + "windows-sys 0.61.2", "winnow", "zbus_macros", "zbus_names", @@ -1681,8 +1687,9 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.9.0" -source = "git+https://github.com/dbus2/zbus#6da6b1b5f528fe2d14b0f25ae1dca1a9fd31575c" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -1695,8 +1702,9 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.2.0" -source = "git+https://github.com/dbus2/zbus#6da6b1b5f528fe2d14b0f25ae1dca1a9fd31575c" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" dependencies = [ "serde", "winnow", @@ -1779,8 +1787,9 @@ dependencies = [ [[package]] name = "zvariant" -version = "5.6.0" -source = "git+https://github.com/dbus2/zbus#6da6b1b5f528fe2d14b0f25ae1dca1a9fd31575c" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" dependencies = [ "endi", "enumflags2", @@ -1792,8 +1801,9 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "5.6.0" -source = "git+https://github.com/dbus2/zbus#6da6b1b5f528fe2d14b0f25ae1dca1a9fd31575c" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -1804,8 +1814,9 @@ dependencies = [ [[package]] name = "zvariant_utils" -version = "3.2.0" -source = "git+https://github.com/dbus2/zbus#6da6b1b5f528fe2d14b0f25ae1dca1a9fd31575c" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 3d815b6..0f66a83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,4 @@ members = [ [workspace.dependencies] sidebus-common = { path = "sidebus-common" } busd = { git = "https://github.com/valpackett/busd", branch = "val/msksqvsqqrxm", default-features = false } -zbus = { git = "https://github.com/dbus2/zbus", default-features = false, features = ["tokio", "tokio-vsock", "bus-impl", "p2p"] } -# zbus git to match busd git - +zbus = { version = "5.0", default-features = false, features = ["tokio", "tokio-vsock", "bus-impl", "p2p"] } diff --git a/flake.nix b/flake.nix index 7326859..c2050d0 100644 --- a/flake.nix +++ b/flake.nix @@ -32,8 +32,7 @@ src = ./.; cargoLock.lockFile = ./Cargo.lock; cargoLock.outputHashes = { - "zbus-5.9.0" = "sha256-uaHPHdmDWYy0jeKPd0/eCUupID2tswGHmEmscp6fCII="; - "busd-0.4.0" = "sha256-hIvjt3v6AYc7URLFknXTmSc+NdxOlN/2RGXVsuoNgA4="; + "busd-0.5.0" = "sha256-IZZ2MeEmUbzRrH6SUz0pnecMH4f8Mh54WdhI4q44YfI="; }; buildAndTestSubdir = crate; env = buildEnvVars; From c42eaef55440e2594677ede5279bd8c3eaf128f2 Mon Sep 17 00:00:00 2001 From: Val Packett Date: Fri, 27 Feb 2026 05:33:02 -0300 Subject: [PATCH 6/6] Support EXTERNAL auth to the client bus with given UID With the virtgpu channel, the proxy is part of muvm-guest, so it does not do the protocol-aware splicing we do here, so it can't do different auth between the sides like sidebus-agent does. But turns out EXTERNAL auth works fine, as long as we correct for the UID difference. --- sidebus-broker/src/main.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/sidebus-broker/src/main.rs b/sidebus-broker/src/main.rs index 875f480..aad976b 100644 --- a/sidebus-broker/src/main.rs +++ b/sidebus-broker/src/main.rs @@ -49,6 +49,10 @@ struct BrokerCli { #[clap(long)] unix_path: Option, + /// Use ANONYMOUS auth to connect to the guest bus instead of EXTERNAL with the provided --guest-uid + #[clap(long)] + guest_bus_anonymous_auth: bool, + /// The user ID for the appvm user inside of the guest #[clap(long, default_value = "1337")] guest_uid: u32, @@ -277,8 +281,11 @@ async fn main() -> eyre::Result<()> { server_tasks.spawn(enclose!((file_chooser_imp, file_transfer_imp, notification_imp, print_imp, settings_imp) async move { while let Ok((socket, remote_addr)) = vm_unix_listener.accept().await { let f = enclose!((file_chooser_imp, file_transfer_imp, notification_imp, print_imp, settings_imp) async move { - let client_conn = zbus::connection::Builder::unix_stream(socket) - .auth_mechanism(zbus::AuthMechanism::Anonymous) + let client_conn = if cli.guest_bus_anonymous_auth { + zbus::connection::Builder::unix_stream(socket).auth_mechanism(zbus::AuthMechanism::Anonymous) + } else { + zbus::connection::Builder::unix_stream(socket).user_id(cli.guest_uid) + } .name("org.freedesktop.portal.Desktop")? .name("org.freedesktop.portal.Documents")? .build()