diff --git a/Cargo.lock b/Cargo.lock index 5260a56..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", @@ -635,15 +635,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.174" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +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]] @@ -1026,6 +1013,7 @@ dependencies = [ "clap", "eyre", "futures", + "libc", "rand", "rustix", "sidebus-common", @@ -1321,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", ] @@ -1338,7 +1328,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e8b4d00e672f147fc86a09738fadb1445bd1c0a40542378dfb82909deeee688" dependencies = [ "libc", - "nix 0.29.0", + "nix", ] [[package]] @@ -1436,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" @@ -1463,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" @@ -1651,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", @@ -1662,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", @@ -1680,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", @@ -1694,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", @@ -1778,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", @@ -1791,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", @@ -1803,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/README.md b/README.md index df3aac1..c8ebf21 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ (it.. runs as a "sidecar" to a VM.. but manages D-*Bus*.. get it?) + + A cross-domain smart D-Bus proxying system that makes (some) [XDG Desktop Portals] work across virtual machines. [XDG Desktop Portals]: https://flatpak.github.io/xdg-desktop-portal/docs/index.html @@ -10,9 +14,8 @@ A cross-domain smart D-Bus proxying system that makes (some) [XDG Desktop Portal - `sidebus-broker` host process: - to be launched alongside the VMM - - hosts D-Bus servers in-process, based on [busd](https://github.com/dbus2/busd): - - a "private" bus for VM-instance-specific daemons such as permission-store and document-portal - - a "VM" bus, the one actually exposed to the guest over vsock + - hosts a "private" bus for VM-instance-specific daemons such as permission-store and document-portal + - listens on vsock (or on a unix socket that muvm would proxy as vsock) and connects to the VM bus as a client when the agent connects - orchestrates the lifecycle of the aforementioned daemons + virtiofsd - (we are sharing the directory *provided by* the document-portal FUSE filesystem!) - provides portal front-end interfaces like `org.freedesktop.portal.FileChooser` on the VM bus @@ -20,11 +23,10 @@ A cross-domain smart D-Bus proxying system that makes (some) [XDG Desktop Portal - (not talking directly to impls: don't want to reimplement per-DE portal selection; also 1:1 mapping is nicer to code) - but with extra hooks like exposing files to the guest using our private (per-VM) document-portal! - `sidebus-agent` guest process: - - listens on a guest unix socket, proxies D-Bus messages to a vsock - - spawned on-demand by systemd via socket activation + - connects to the broker over vsock and splices the connection into the VM (session) bus + - can be spawned spawned on-demand by D-Bus - uses systemd credentials for config args like vsock port - (very convenient to pass via the VMM, e.g. qemu: `-smbios type=11,value=io.systemd.credential:sidebus.port=1337`) - - guest NixOS configuration exposed via the flake ## Development Notes diff --git a/flake.lock b/flake.lock index e8bb457..8445fa1 100644 --- a/flake.lock +++ b/flake.lock @@ -1,30 +1,32 @@ { "nodes": { - "flake-utils": { + "flake-parts": { "inputs": { - "systems": "systems" + "nixpkgs-lib": [ + "nixpkgs" + ] }, "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "lastModified": 1768135262, + "narHash": "sha256-PVvu7OqHBGWN16zSi6tEmPwwHQ4rLPU9Plvs8/1TUBY=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "80daad04eddbbf5a4d883996a73f3f542fa437ac", "type": "github" }, "original": { - "owner": "numtide", - "repo": "flake-utils", + "owner": "hercules-ci", + "repo": "flake-parts", "type": "github" } }, "nixpkgs": { "locked": { - "lastModified": 1751271578, - "narHash": "sha256-P/SQmKDu06x8yv7i0s8bvnnuJYkxVGBWLWHaU+tt4YY=", + "lastModified": 1762977756, + "narHash": "sha256-4PqRErxfe+2toFJFgcRKZ0UI9NSIOJa+7RXVtBhy4KE=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "3016b4b15d13f3089db8a41ef937b13a9e33a8df", + "rev": "c5ae371f1a6a7fd27823bc500d9390b38c05fa55", "type": "github" }, "original": { @@ -36,44 +38,8 @@ }, "root": { "inputs": { - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs", - "rust-overlay": "rust-overlay" - } - }, - "rust-overlay": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1751510438, - "narHash": "sha256-m8PjOoyyCR4nhqtHEBP1tB/jF+gJYYguSZmUmVTEAQE=", - "owner": "oxalica", - "repo": "rust-overlay", - "rev": "7f415261f298656f8164bd636c0dc05af4e95b6b", - "type": "github" - }, - "original": { - "owner": "oxalica", - "repo": "rust-overlay", - "type": "github" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs" } } }, diff --git a/flake.nix b/flake.nix index 1aeb30c..c2050d0 100644 --- a/flake.nix +++ b/flake.nix @@ -1,80 +1,60 @@ { inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - flake-utils.url = "github:numtide/flake-utils"; - rust-overlay = { - url = "github:oxalica/rust-overlay"; - inputs.nixpkgs.follows = "nixpkgs"; - }; + flake-parts.url = "github:hercules-ci/flake-parts"; + flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs"; }; - outputs = {self, nixpkgs, flake-utils, rust-overlay}: - flake-utils.lib.eachDefaultSystem (system: - let - overlays = [ (import rust-overlay) ]; - pkgs = import nixpkgs { - inherit system overlays; - }; + outputs = + inputs@{ flake-parts, ... }: + flake-parts.lib.mkFlake { inherit inputs; } { + systems = [ + "x86_64-linux" + "aarch64-linux" + ]; - buildEnvVars = { - BIN_XDG_PERMISSION_STORE = "${pkgs.xdg-desktop-portal}/libexec/xdg-permission-store"; - BIN_XDG_DOCUMENT_PORTAL = "${pkgs.xdg-desktop-portal}/libexec/xdg-document-portal"; - BIN_VIRTIOFSD = "${pkgs.virtiofsd}/bin/virtiofsd"; - }; + perSystem = + { pkgs, ... }: + let + buildEnvVars = { + BIN_XDG_PERMISSION_STORE = "${pkgs.xdg-desktop-portal}/libexec/xdg-permission-store"; + BIN_XDG_DOCUMENT_PORTAL = "${pkgs.xdg-desktop-portal}/libexec/xdg-document-portal"; + BIN_VIRTIOFSD = "${pkgs.virtiofsd}/bin/virtiofsd"; + }; - rustToolchain = pkgs.pkgsBuildHost.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; - rustPlatform = pkgs.makeRustPlatform { - cargo = rustToolchain; - rustc = rustToolchain; - }; - rustPackage = crate: - let cargoToml = builtins.fromTOML (builtins.readFile ./${crate}/Cargo.toml); - in rustPlatform.buildRustPackage { - inherit (cargoToml.package) name version; - src = ./.; - cargoLock.lockFile = ./Cargo.lock; - cargoLock.outputHashes = { - "zbus-5.9.0" = "sha256-3xaKbf+JmO5yVwPbvA3z9dHvqICh7yCeKk1SIX8zhJA="; - "busd-0.4.0" = "sha256-UzTclEJ8lRMmiuLJJi+gsm7vkx+MLfnDdi5s9OVT1HE="; + rustPackage = + crate: + let + cargoToml = builtins.fromTOML (builtins.readFile ./${crate}/Cargo.toml); + in + pkgs.rustPlatform.buildRustPackage { + inherit (cargoToml.package) name version; + src = ./.; + cargoLock.lockFile = ./Cargo.lock; + cargoLock.outputHashes = { + "busd-0.5.0" = "sha256-IZZ2MeEmUbzRrH6SUz0pnecMH4f8Mh54WdhI4q44YfI="; + }; + buildAndTestSubdir = crate; + env = buildEnvVars; }; - buildAndTestSubdir = crate; + in + { + devShells.default = pkgs.mkShell { + buildInputs = with pkgs; [ + cargo + rustc + rust-analyzer + clippy + ]; env = buildEnvVars; }; - in - { - devShells.default = pkgs.mkShell { - buildInputs = [ rustToolchain ]; - env = buildEnvVars; + + packages.sidebus-agent = rustPackage "sidebus-agent"; + packages.sidebus-broker = rustPackage "sidebus-broker"; }; - packages.sidebus-agent = rustPackage "sidebus-agent"; - packages.sidebus-broker = rustPackage "sidebus-broker"; - - nixosModules.sidebus-vm = { ... }: { - environment.sessionVariables.DBUS_SESSION_BUS_ADDRESS = "unix:path=/run/sidebus.sock"; - systemd.sockets.sidebus-agent = { - # SocketMode= is 0666 by default - listenStreams = [ "/run/sidebus.sock" ]; - wantedBy = [ "sockets.target" ]; - documentation = [ "https://git.clan.lol/valpackett/sidebus" ]; - }; - systemd.services.sidebus-agent = { - # TODO: confinement (can do a lot) - serviceConfig = { - ExecStart = "${rustPackage "sidebus-agent"}/bin/sidebus-agent"; - ImportCredential = "sidebus.*"; - }; - documentation = [ "https://git.clan.lol/valpackett/sidebus" ]; - }; - systemd.mounts = [ - { - type = "virtiofs"; - what = "vm-doc-portal"; - where = "/run/vm-doc-portal"; - wantedBy = [ "multi-user.target" ]; - } - ]; - }; - } - ); + flake = { + nixosModules.sidebus-vm = ./nixosModules/sidebus-vm.nix; + }; + }; } diff --git a/nixosModules/sidebus-vm.nix b/nixosModules/sidebus-vm.nix new file mode 100644 index 0000000..215d2b1 --- /dev/null +++ b/nixosModules/sidebus-vm.nix @@ -0,0 +1,25 @@ +{ + environment.sessionVariables.DBUS_SESSION_BUS_ADDRESS = "unix:path=/run/sidebus.sock"; + systemd.sockets.sidebus-agent = { + # SocketMode= is 0666 by default + listenStreams = [ "/run/sidebus.sock" ]; + wantedBy = [ "sockets.target" ]; + documentation = [ "https://git.clan.lol/valpackett/sidebus" ]; + }; + systemd.services.sidebus-agent = { + # TODO: confinement (can do a lot) + serviceConfig = { + ExecStart = throw "sidebus-vm module requires setting systemd.services.sidebus-agent.serviceConfig.ExecStart to a sidebus-agent package"; + ImportCredential = "sidebus.*"; + }; + documentation = [ "https://git.clan.lol/valpackett/sidebus" ]; + }; + systemd.mounts = [ + { + type = "virtiofs"; + what = "vm-doc-portal"; + where = "/run/vm-doc-portal"; + wantedBy = [ "multi-user.target" ]; + } + ]; +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml deleted file mode 100644 index a1de405..0000000 --- a/rust-toolchain.toml +++ /dev/null @@ -1,3 +0,0 @@ -[toolchain] -channel = "1.88.0" -components = ["rust-analyzer", "rust-src", "clippy"] diff --git a/sidebus-agent/src/main.rs b/sidebus-agent/src/main.rs index 984a59f..650cde3 100644 --- a/sidebus-agent/src/main.rs +++ b/sidebus-agent/src/main.rs @@ -1,5 +1,3 @@ -use eyre::OptionExt; -use tokio::net::UnixListener; use tracing::info; #[tokio::main] @@ -10,38 +8,23 @@ async fn main() -> eyre::Result<()> { let vsock_port = std::fs::read_to_string(creds_dir.join("sidebus.port"))? .trim() .parse::()?; + let vsock_addr = zbus::Address::from(zbus::address::Transport::Vsock( + zbus::address::transport::Vsock::new(2, vsock_port), + )); - let unix_listener = UnixListener::from_std( - listenfd::ListenFd::from_env() - .take_unix_listener(0)? - .ok_or_eyre("no unix listener provided")?, - )?; - info!("listening for unix clients"); + info!("connecting to session bus"); + let session_conn = zbus::connection::Builder::session()?.p2p().build().await?; + + let vsock_conn = zbus::connection::Builder::address(vsock_addr)? + .server(session_conn.server_guid()) + .unwrap() + .p2p() + .auth_mechanism(zbus::AuthMechanism::Anonymous) + .build() + .await + .unwrap(); + info!(guid = %vsock_conn.server_guid(), "connected to vsock bus"); + sidebus_common::raw::splice_conns(vsock_conn, session_conn).await; - while let Ok((unix_client, client_addr)) = unix_listener.accept().await { - info!(?client_addr, "new unix client"); - tokio::spawn(async move { - let vsock_addr = zbus::Address::from(zbus::address::Transport::Vsock( - zbus::address::transport::Vsock::new(2, vsock_port), - )); - let vsock_conn = zbus::connection::Builder::address(vsock_addr) - .unwrap() - .p2p() - .auth_mechanism(zbus::AuthMechanism::Anonymous) - .build() - .await - .unwrap(); - info!(guid = %vsock_conn.server_guid(), "connected to vsock bus"); - let client_conn = zbus::connection::Builder::unix_stream(unix_client) - .server(vsock_conn.server_guid()) - .unwrap() - .p2p() - .auth_mechanism(zbus::AuthMechanism::External) - .build() - .await - .unwrap(); - sidebus_common::raw::splice_conns(client_conn, vsock_conn).await; - }); - } Ok(()) } diff --git a/sidebus-broker/Cargo.toml b/sidebus-broker/Cargo.toml index 3be80d9..1cbde4d 100644 --- a/sidebus-broker/Cargo.toml +++ b/sidebus-broker/Cargo.toml @@ -18,3 +18,4 @@ rustix = { version = "1.0.8", features = ["fs"] } url = "2.5.4" rand = "0.9.2" futures = "0.3.31" +libc = "0.2.178" diff --git a/sidebus-broker/src/bus.rs b/sidebus-broker/src/bus.rs index 4607379..b2fd94f 100644 --- a/sidebus-broker/src/bus.rs +++ b/sidebus-broker/src/bus.rs @@ -1,6 +1,6 @@ -use std::sync::Arc; +use std::{hash::DefaultHasher, sync::Arc}; use tokio_stream::StreamExt as _; -use tracing::{debug, trace}; +use tracing::{debug, error, trace}; pub struct HostedBus { peers: Arc, @@ -57,10 +57,14 @@ impl HostedBus { .map_err(|err| eyre::eyre!(Box::new(err))) // https://github.com/eyre-rs/eyre/issues/31 XXX: busd should not use anyhow! } - pub async fn connect_unix(&mut self, socket: tokio::net::UnixStream) -> eyre::Result<()> { + pub async fn connect_unix( + &mut self, + socket: tokio::net::UnixStream, + auth: zbus::AuthMechanism, + ) -> eyre::Result<()> { let id = self.next_id(); self.peers - .add(&self.guid, id, socket.into(), zbus::AuthMechanism::External) + .add(&self.guid, id, socket.into(), auth) .await .map_err(|err| eyre::eyre!(Box::new(err))) } @@ -77,7 +81,7 @@ impl HostedBus { } pub trait SharedHostedBus { - async fn run_unix_listener(self, listener: tokio::net::UnixListener); + async fn run_unix_listener(self, listener: tokio::net::UnixListener, auth: zbus::AuthMechanism); async fn spawn_external_client( self, command: &mut tokio::process::Command, @@ -85,9 +89,15 @@ pub trait SharedHostedBus { } impl SharedHostedBus for Arc> { - async fn run_unix_listener(self, listener: tokio::net::UnixListener) { + async fn run_unix_listener( + self, + listener: tokio::net::UnixListener, + auth: zbus::AuthMechanism, + ) { while let Ok((socket, _remote_addr)) = listener.accept().await { - self.lock().await.connect_unix(socket).await.unwrap() + if let Err(e) = self.lock().await.connect_unix(socket, auth).await { + error!("unix connection: {:?}", e); + } } } @@ -100,7 +110,7 @@ impl SharedHostedBus for Arc> { let abstract_path = format!("/run/sidebus-broker/{}", zbus::Guid::generate()); let listener = tokio::net::UnixListener::bind(format!("\0{abstract_path}"))?; debug!(%abstract_path, "opened listener for external client"); - tokio::spawn(self.run_unix_listener(listener)); + tokio::spawn(self.run_unix_listener(listener, zbus::AuthMechanism::External)); Ok(command .env( "DBUS_SESSION_BUS_ADDRESS", diff --git a/sidebus-broker/src/main.rs b/sidebus-broker/src/main.rs index 7a2247d..aad976b 100644 --- a/sidebus-broker/src/main.rs +++ b/sidebus-broker/src/main.rs @@ -4,9 +4,12 @@ mod vsock; use bus::SharedHostedBus; use clap::Parser; -use std::{path::PathBuf, sync::Arc}; +use eyre::OptionExt; +use futures::{TryFutureExt, stream::FuturesUnordered}; +use std::{path::PathBuf, sync::Arc, time::Duration}; use tokio::{net::UnixListener, process::Command, sync::Mutex}; -use tracing::error; +use tokio_stream::StreamExt as _; +use tracing::{Instrument, debug, error, info_span}; use zbus::names::WellKnownName; // https://github.com/rust-lang/rfcs/issues/2407#issuecomment-385291238 @@ -22,7 +25,7 @@ macro_rules! enclose { #[derive(Parser)] #[command(version, about, long_about = None)] struct BrokerCli { - /// Create unix socket listeners for all internal busses in the provided directory + /// Create unix socket listeners for internal busses in the provided directory #[clap(long)] debug_access: Option, @@ -34,9 +37,29 @@ struct BrokerCli { #[clap(long, default_value = "/run/vm-doc-portal")] guest_mountpoint: PathBuf, - /// Vsock port number to listen on + /// Mappings from guest paths to host paths for passthrough file systems (for file transfer), in guest=host format #[clap(long)] - vsock_port: u32, + path_mapping: Vec, + + /// Vsock port number to listen on for the VM bus + #[clap(long)] + vsock_port: Option, + + /// Unix socket path to listen on for the VM bus + #[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, + + /// The group ID for the appvm group inside of the guest + #[clap(long, default_value = "1337")] + guest_gid: u32, } async fn new_hosted_bus() -> eyre::Result<( @@ -51,110 +74,272 @@ 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 (vm_bus, vm_bus_guid, _) = new_hosted_bus().await?; let (priv_bus, _, mut priv_lst) = new_hosted_bus().await?; + let mut server_tasks = tokio::task::JoinSet::new(); + let mut child_procs = Vec::new(); + if let Some(dir_path) = cli.debug_access { if !dir_path.is_dir() { error!(path = %dir_path.display(), "--debug-access path is not an existing directory"); std::process::exit(1); } - let vm_dbg_listener = UnixListener::bind(dir_path.join("vm.sock"))?; - let _vm_dbg_task = tokio::spawn(vm_bus.clone().run_unix_listener(vm_dbg_listener)); let priv_dbg_listener = UnixListener::bind(dir_path.join("priv.sock"))?; - let _priv_dbg_task = tokio::spawn(priv_bus.clone().run_unix_listener(priv_dbg_listener)); + server_tasks.spawn( + priv_bus + .clone() + .run_unix_listener(priv_dbg_listener, zbus::AuthMechanism::External), + ); // TODO: unlink sockets on exit } std::fs::create_dir_all(&cli.runtime_dir)?; - let _xps = priv_bus - .clone() - .spawn_external_client( - Command::new(env!("BIN_XDG_PERMISSION_STORE")) - .env("XDG_RUNTIME_DIR", cli.runtime_dir.as_os_str()) - .kill_on_drop(true), - ) - .await?; + child_procs.push( + priv_bus + .clone() + .spawn_external_client( + Command::new(env!("BIN_XDG_PERMISSION_STORE")) + .env("XDG_RUNTIME_DIR", cli.runtime_dir.as_os_str()) + .kill_on_drop(true), + ) + .await?, + ); let impl_permission_store = WellKnownName::from_static_str("org.freedesktop.impl.portal.PermissionStore")?.into(); priv_lst.wait_for_acquisition(impl_permission_store).await?; - let _xdp = priv_bus - .clone() - .spawn_external_client( - Command::new(env!("BIN_XDG_DOCUMENT_PORTAL")) - .env("XDG_RUNTIME_DIR", cli.runtime_dir.as_os_str()) - .kill_on_drop(true), - ) - .await?; + child_procs.push( + priv_bus + .clone() + .spawn_external_client( + Command::new(env!("BIN_XDG_DOCUMENT_PORTAL")) + .env("XDG_RUNTIME_DIR", cli.runtime_dir.as_os_str()) + .kill_on_drop(true), + ) + .await?, + ); let portal_documents = WellKnownName::from_static_str("org.freedesktop.portal.Documents")?.into(); priv_lst.wait_for_acquisition(portal_documents).await?; - let _vfs = Command::new(env!("BIN_VIRTIOFSD")) - .args(&[ - "--shared-dir", - cli.runtime_dir.join("doc").to_str().unwrap(), - "--socket-path", - cli.runtime_dir.join("fs.sock").to_str().unwrap(), - "--uid-map", - ":1000:1001:1:", - "--gid-map", - ":100:100:1:", - "--log-level", - "debug", - ]) - .env("XDG_RUNTIME_DIR", cli.runtime_dir.as_os_str()) - .kill_on_drop(true) - .spawn(); - // TODO: die when it exits + child_procs.push( + Command::new(env!("BIN_VIRTIOFSD")) + .args(&[ + "--shared-dir", + cli.runtime_dir.join("doc").to_str().unwrap(), + "--socket-path", + cli.runtime_dir.join("fs.sock").to_str().unwrap(), + "--uid-map", + &format!(":{}:{}:1:", cli.guest_uid, unsafe { libc::getuid() }), + "--gid-map", + &format!(":{}:{}:1:", cli.guest_gid, unsafe { libc::getgid() }), + "--log-level", + "debug", + ]) + .env("XDG_RUNTIME_DIR", cli.runtime_dir.as_os_str()) + .kill_on_drop(true) + .spawn()?, + ); - let vm_bus_conn = vm_bus.lock().await.connect_channel(false).await?; 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, + cli.guest_mountpoint.clone(), ) .await?; - vm_bus_conn - .request_name("org.freedesktop.portal.Desktop") - .await?; - let true = vm_bus_conn - .object_server() - .at("/org/freedesktop/portal/desktop", file_chooser_imp) - .await? - else { - unreachable!("our own fresh bus can't have interfaces already provided"); - }; - - // TODO: modprobe vhost_vsock first! - // NOTE: Every individual D-Bus client inside of the VM is a new client here! - vsock::ListenerBuilder::new(vsock::VsockAddr::new( - vsock::VMADDR_CID_HOST, - cli.vsock_port, - )) - .with_label("VM Bus") - .listen(move |client| { - enclose! { (vm_bus, vm_bus_guid) async move { - // TODO: Not necessary to go through the channel, add vsock support to the Peer too - let client_conn = client.build((&vm_bus_guid).into()).await?; - let vmbus_conn = vm_bus.lock().await.connect_channel(true).await?; - sidebus_common::raw::splice_conns(client_conn, vmbus_conn).await; - Ok(()) - } } - }) + let file_transfer_imp = portal::file_transfer::FileTransfer::new( + &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?; + 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( + vm_bus_conn: zbus::Connection, + 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 + .object_server() + .at("/org/freedesktop/portal/desktop", file_chooser) + .await? + { + 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", 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", print) + .await? + { + error!("org.freedesktop.portal.Print already provided"); + }; + + if !vm_bus_conn + .object_server() + .at("/org/freedesktop/portal/desktop", settings) + .await? + { + error!("org.freedesktop.portal.Settings already provided"); + }; + let settings_ref = vm_bus_conn + .object_server() + .interface::<_, portal::settings::Settings>("/org/freedesktop/portal/desktop") + .await?; + tokio::spawn(async move { + let settings = settings_ref.get().await; + let emitter = settings_ref.signal_emitter(); + if let Err(err) = settings.forward_changes(emitter).await { + 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, 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 = 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() + .await?; + on_vm_bus_connected(client_conn, file_chooser_imp, file_transfer_imp, notification_imp, print_imp, settings_imp).await + }); + tokio::spawn( + async { + match f.await { + Ok(()) => debug!("done with server"), + Err(err) => error!(%err, "error dealing with server"), + } + } + .instrument(info_span!("serve", ?remote_addr)), + ); + } + })); + } + + if let Some(port) = cli.vsock_port { + // TODO: modprobe vhost_vsock first! + server_tasks.spawn( + 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, 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, print_imp, settings_imp).await + }) + }) + .map_ok_or_else( + |e| { + error!("vsock listener: {:?}", e); + }, + |()| (), + ), + ); + } + + let mut waiter = child_procs + .iter_mut() + .map(|child| child.wait()) + .collect::>(); + tokio::select! { + _ = server_tasks.join_all() => debug!("server tasks ended"), + res = waiter.next() => debug!(?res, "child process terminated"), + _ = tokio::signal::ctrl_c() => debug!("interrupt signal"), + }; + drop(waiter); + for mut child in child_procs { + if let Err(e) = child.kill().await { + error!(?e, "could not kill process"); + } + } Ok(()) } diff --git a/sidebus-broker/src/portal/file_chooser.rs b/sidebus-broker/src/portal/file_chooser.rs index b3a0ff9..6121da8 100644 --- a/sidebus-broker/src/portal/file_chooser.rs +++ b/sidebus-broker/src/portal/file_chooser.rs @@ -8,6 +8,7 @@ use zbus::{Connection, ObjectServer, fdo::Result, zvariant}; use super::documents::DocumentsProxy; use super::request::{RESPONSE_SUCCESS, ReqHandler, ResultTransformer}; +#[derive(Clone)] pub struct FileChooser { host: FileChooserProxy<'static>, docs: DocumentsProxy<'static>, @@ -52,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) @@ -72,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) @@ -92,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) @@ -105,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 @@ -133,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()); @@ -149,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( @@ -165,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.. @@ -211,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..8c175f6 --- /dev/null +++ b/sidebus-broker/src/portal/file_transfer.rs @@ -0,0 +1,205 @@ +use std::{ + collections::HashMap, + os::fd::{AsFd, AsRawFd}, + os::unix::ffi::OsStrExt, + 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, + path_prefix_to_host: Vec<(PathBuf, PathBuf)>, +} + +#[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<()> { + 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( + &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!(%host_path, "could not add path as doc to retrieve file"); + } + } + 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!(%key, %sender, "started transfer"); + if let Err(err) = self + .tx + .send(ForwarderCommand::Add(key.clone(), sender.into())) + { + 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!(%key, "stopping transfer"); + 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, + path_prefix_to_host: Vec<(PathBuf, 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, + path_prefix_to_host, + }) + } + + 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!(?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!(?err, %key, "could not forward signal"); + } + } else { + error!(%key, "got a signal for unknown 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 69aa6be..d43de89 100644 --- a/sidebus-broker/src/portal/mod.rs +++ b/sidebus-broker/src/portal/mod.rs @@ -1,3 +1,7 @@ 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/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(()) + } +} 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 }) + } +} diff --git a/sidebus-broker/src/portal/settings.rs b/sidebus-broker/src/portal/settings.rs new file mode 100644 index 0000000..de03b8b --- /dev/null +++ b/sidebus-broker/src/portal/settings.rs @@ -0,0 +1,66 @@ +use std::collections::HashMap; + +use tokio_stream::StreamExt; +use tracing::warn; +use zbus::{Connection, fdo::Result, object_server::SignalEmitter, zvariant}; + +#[derive(Clone)] +pub struct Settings { + host: SettingsProxy<'static>, +} + +#[zbus::interface( + name = "org.freedesktop.portal.Settings", + proxy( + default_service = "org.freedesktop.portal.Desktop", + default_path = "/org/freedesktop/portal/desktop" + ) +)] +impl Settings { + async fn read(&self, namespace: &str, key: &str) -> Result { + self.host.read(namespace, key).await + } + + async fn read_all( + &self, + namespaces: Vec<&str>, + ) -> Result>> { + self.host.read_all(namespaces).await + } + + async fn read_one(&self, namespace: &str, key: &str) -> Result { + self.host.read_one(namespace, key).await + } + + /// SettingChanged signal + #[zbus(signal)] + async fn setting_changed( + signal_emitter: &SignalEmitter<'_>, + namespace: &str, + key: &str, + value: zvariant::Value<'_>, + ) -> zbus::Result<()>; + + #[zbus(property, name = "version")] + async fn version(&self) -> Result { + Ok(2) + } +} + +impl Settings { + pub(crate) async fn new(host_session_conn: &Connection) -> Result { + let host = SettingsProxy::builder(host_session_conn).build().await?; + Ok(Self { host }) + } + + pub async fn forward_changes(&self, signal_emitter: &SignalEmitter<'static>) -> Result<()> { + let mut stream = self.host.receive_setting_changed().await?; + while let Some(x) = stream.next().await { + let args = x.args()?; + Settings::setting_changed(signal_emitter, args.namespace, args.key, args.value).await?; + () + } + warn!("settings change stream end"); + Ok(()) + } +} diff --git a/sidebus-broker/src/vsock.rs b/sidebus-broker/src/vsock.rs index f3524a0..23d6c54 100644 --- a/sidebus-broker/src/vsock.rs +++ b/sidebus-broker/src/vsock.rs @@ -13,12 +13,11 @@ impl ConnectionBuilder { &self.remote_addr } - pub async fn build<'a>(self, guid: zbus::Guid<'a>) -> eyre::Result { + pub async fn build<'a>(self) -> eyre::Result { zbus::connection::Builder::vsock_stream(self.socket) - .server(guid) - .unwrap() - .p2p() .auth_mechanism(zbus::AuthMechanism::Anonymous) + .name("org.freedesktop.portal.Desktop")? + .name("org.freedesktop.portal.Documents")? .build() .await .map_err(|e| e.into())