Skip to content

Commit 4d73d82

Browse files
authored
Create mod.rs
1 parent 85da1f0 commit 4d73d82

1 file changed

Lines changed: 205 additions & 0 deletions

File tree

src/portal/mod.rs

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
use std::{
2+
collections::HashMap,
3+
os::unix::net::UnixStream,
4+
path::PathBuf,
5+
sync::Mutex,
6+
};
7+
8+
use zbus::{
9+
fdo,
10+
interface,
11+
object_server::SignalContext,
12+
zvariant::{ObjectPath, OwnedFd, OwnedObjectPath, OwnedValue},
13+
Connection, ObjectServer,
14+
};
15+
16+
/// Per-session bookkeeping — currently just the device-type bitmask
17+
/// `SelectDevices` recorded, kept around for when a real consent UI
18+
/// needs to show "this app wants keyboard+pointer access" or similar.
19+
/// Bitmask values match the real portal spec's own convention:
20+
/// 1 = keyboard, 2 = pointer, 4 = touchscreen.
21+
#[derive(Default, Clone, Copy)]
22+
struct Session {
23+
devices: u32,
24+
}
25+
26+
struct PortalState {
27+
eis_socket_path: PathBuf,
28+
sessions: Mutex<HashMap<String, Session>>,
29+
}
30+
31+
struct RemoteDesktopPortal {
32+
state: std::sync::Arc<PortalState>,
33+
}
34+
35+
/// Exported dynamically, once per `handle` object path, by each of
36+
/// `create_session`/`select_devices`/`start` — real
37+
/// `org.freedesktop.impl.portal.Request` shape (an object a caller can
38+
/// `Close()` to cancel a still-pending request, and which emits exactly
39+
/// one `Response` signal when the request concludes, positively or
40+
/// negatively). Stateless itself; the response value is provided by
41+
/// whichever RemoteDesktopPortal method created it, immediately after
42+
/// export, not stored on this struct.
43+
struct RequestObject;
44+
45+
#[interface(name = "org.freedesktop.impl.portal.Request")]
46+
impl RequestObject {
47+
/// Real requests would stop whatever's pending and emit a
48+
/// cancelled `Response` here — since every request in this
49+
/// implementation has already been auto-approved and resolved by
50+
/// the time a caller could possibly call `Close`, this is a no-op
51+
/// stub that exists for interface-shape completeness.
52+
async fn close(&self) {}
53+
54+
#[zbus(signal)]
55+
async fn response(
56+
signal_ctxt: &SignalContext<'_>,
57+
response: u32,
58+
results: HashMap<String, OwnedValue>,
59+
) -> zbus::Result<()>;
60+
}
61+
62+
/// Exports a `RequestObject` at `handle` and immediately emits a
63+
/// success `Response(0, results)` signal on it — the "auto-approve"
64+
/// simplification this module's doc is explicit about, factored out
65+
/// since all three of `create_session`/`select_devices`/`start` do
66+
/// exactly this same sequence.
67+
async fn respond_success(
68+
object_server: &ObjectServer,
69+
handle: &ObjectPath<'_>,
70+
results: HashMap<String, OwnedValue>,
71+
) -> fdo::Result<()> {
72+
let owned_handle = OwnedObjectPath::from(handle.to_owned());
73+
object_server
74+
.at(&owned_handle, RequestObject)
75+
.await
76+
.map_err(|e| fdo::Error::Failed(format!("failed to export Request object: {e}")))?;
77+
let iface_ref = object_server
78+
.interface::<_, RequestObject>(&owned_handle)
79+
.await
80+
.map_err(|e| fdo::Error::Failed(format!("failed to look up just-exported Request object: {e}")))?;
81+
RequestObject::response(iface_ref.signal_context(), 0, results)
82+
.await
83+
.map_err(|e| fdo::Error::Failed(format!("failed to emit Response signal: {e}")))?;
84+
Ok(())
85+
}
86+
87+
#[interface(name = "org.freedesktop.impl.portal.RemoteDesktop")]
88+
impl RemoteDesktopPortal {
89+
async fn create_session(
90+
&self,
91+
handle: ObjectPath<'_>,
92+
session_handle: ObjectPath<'_>,
93+
_app_id: String,
94+
_options: HashMap<String, OwnedValue>,
95+
#[zbus(object_server)] object_server: &ObjectServer,
96+
) -> fdo::Result<()> {
97+
self.state
98+
.sessions
99+
.lock()
100+
.unwrap()
101+
.insert(session_handle.to_string(), Session::default());
102+
respond_success(object_server, &handle, HashMap::new()).await
103+
}
104+
105+
async fn select_devices(
106+
&self,
107+
handle: ObjectPath<'_>,
108+
session_handle: ObjectPath<'_>,
109+
_app_id: String,
110+
options: HashMap<String, OwnedValue>,
111+
#[zbus(object_server)] object_server: &ObjectServer,
112+
) -> fdo::Result<()> {
113+
// Real callers pass the requested device bitmask under the
114+
// "types" option key (u32) — this compositor doesn't currently
115+
// *restrict* what ConnectToEIS hands back based on it (the one
116+
// EIS socket serves pointer+keyboard+touch uniformly regardless
117+
// — see input_emulation::init/render/mod.rs's add_touch calls),
118+
// just records it for whenever a real consent UI wants to show
119+
// what was asked for.
120+
let devices = options
121+
.get("types")
122+
.and_then(|v| u32::try_from(v.clone()).ok())
123+
.unwrap_or(0);
124+
if let Some(session) = self.state.sessions.lock().unwrap().get_mut(&session_handle.to_string()) {
125+
session.devices = devices;
126+
}
127+
respond_success(object_server, &handle, HashMap::new()).await
128+
}
129+
130+
async fn start(
131+
&self,
132+
handle: ObjectPath<'_>,
133+
_session_handle: ObjectPath<'_>,
134+
_app_id: String,
135+
_parent_window: String,
136+
_options: HashMap<String, OwnedValue>,
137+
#[zbus(object_server)] object_server: &ObjectServer,
138+
) -> fdo::Result<()> {
139+
respond_success(object_server, &handle, HashMap::new()).await
140+
}
141+
142+
/// The one method with a genuinely immediate, synchronous result —
143+
/// real portal spec has this one return the fd directly rather than
144+
/// through the Request/Response pattern the other three use (there's
145+
/// nothing left to negotiate by this point; the session was already
146+
/// approved in `start`).
147+
async fn connect_to_eis(
148+
&self,
149+
_session_handle: ObjectPath<'_>,
150+
_app_id: String,
151+
_options: HashMap<String, OwnedValue>,
152+
) -> fdo::Result<OwnedFd> {
153+
let stream = UnixStream::connect(&self.state.eis_socket_path).map_err(|e| {
154+
fdo::Error::Failed(format!(
155+
"failed to connect to internal EIS socket at {}: {e}",
156+
self.state.eis_socket_path.display()
157+
))
158+
})?;
159+
Ok(OwnedFd::from(std::os::fd::OwnedFd::from(stream)))
160+
}
161+
}
162+
163+
pub fn init(eis_socket_path: PathBuf) {
164+
std::thread::spawn(move || {
165+
let rt = match tokio::runtime::Builder::new_current_thread().enable_all().build() {
166+
Ok(rt) => rt,
167+
Err(e) => {
168+
tracing::warn!("portal: failed to start tokio runtime, RemoteDesktop portal disabled: {e}");
169+
return;
170+
}
171+
};
172+
rt.block_on(async move {
173+
let state = std::sync::Arc::new(PortalState {
174+
eis_socket_path,
175+
sessions: Mutex::new(HashMap::new()),
176+
});
177+
let portal = RemoteDesktopPortal { state };
178+
179+
let connection: Connection = match Connection::session().await {
180+
Ok(c) => c,
181+
Err(e) => {
182+
tracing::warn!("portal: no D-Bus session bus available, RemoteDesktop portal disabled: {e}");
183+
return;
184+
}
185+
};
186+
if let Err(e) = connection.object_server().at("/org/freedesktop/portal/desktop", portal).await {
187+
tracing::warn!("portal: failed to export RemoteDesktop interface: {e}");
188+
return;
189+
}
190+
if let Err(e) = connection.request_name("org.freedesktop.impl.portal.desktop.blue").await {
191+
tracing::warn!("portal: failed to claim D-Bus service name (another portal backend already running?): {e}");
192+
return;
193+
}
194+
195+
tracing::info!(
196+
"RemoteDesktop D-Bus portal backend registered at \
197+
org.freedesktop.impl.portal.desktop.blue — see portal/mod.rs's \
198+
module doc for the real, load-bearing security caveat (no \
199+
consent UI, auto-approves every request) before relying on \
200+
this for anything real"
201+
);
202+
std::future::pending::<()>().await;
203+
});
204+
});
205+
}

0 commit comments

Comments
 (0)