1
0
Fork 0
forked from wry/wry

portal: implement RemoteDesktop portal

This commit is contained in:
Julian Orth 2024-07-24 17:57:48 +02:00
parent 40e87f8f91
commit 665127e6c0
22 changed files with 994 additions and 35 deletions

View file

@ -1,3 +1,4 @@
[preferred]
default=gtk
org.freedesktop.impl.portal.ScreenCast=jay
org.freedesktop.impl.portal.RemoteDesktop=jay

View file

@ -1,3 +1,3 @@
[portal]
DBusName=org.freedesktop.impl.portal.desktop.jay
Interfaces=org.freedesktop.impl.portal.ScreenCast;
Interfaces=org.freedesktop.impl.portal.ScreenCast;org.freedesktop.impl.portal.RemoteDesktop;

View file

@ -5,6 +5,7 @@
- Add support for tearing.
- Add support for touch input.
- Add support for libei.
- Add support for RemoteDesktop portal.
# 1.4.0 (2024-07-07)

View file

@ -146,8 +146,7 @@ async fn accept(fd: Rc<OwnedFd>, state: Rc<State>) {
break;
}
};
let id = state.clients.id();
if let Err(e) = state.ei_clients.spawn(id, &state, fd) {
if let Err(e) = state.ei_clients.spawn(&state, fd) {
log::error!("Could not spawn a client: {}", ErrorFmt(e));
break;
}

View file

@ -16,6 +16,7 @@ use {
asyncevent::AsyncEvent,
buffd::{EiMsgFormatter, EiMsgParser, EiMsgParserError, OutBufferSwapchain},
clonecell::CloneCell,
debug_fn::debug_fn,
errorfmt::ErrorFmt,
numcell::NumCell,
pid_info::{get_pid_info, get_socket_creds, PidInfo},
@ -31,7 +32,7 @@ use {
ops::DerefMut,
rc::Rc,
},
uapi::{c, OwnedFd},
uapi::OwnedFd,
};
mod ei_error;
@ -64,26 +65,21 @@ impl EiClients {
mem::take(self.shutdown_clients.borrow_mut().deref_mut());
}
pub fn spawn(
&self,
id: ClientId,
global: &Rc<State>,
socket: Rc<OwnedFd>,
) -> Result<(), EiClientError> {
pub fn spawn(&self, global: &Rc<State>, socket: Rc<OwnedFd>) -> Result<(), EiClientError> {
let Some((uid, pid)) = get_socket_creds(&socket) else {
return Ok(());
};
self.spawn2(id, global, socket, uid, pid)?;
let pid_info = get_pid_info(uid, pid);
self.spawn2(global, socket, Some(pid_info), None)?;
Ok(())
}
pub fn spawn2(
&self,
id: ClientId,
global: &Rc<State>,
socket: Rc<OwnedFd>,
uid: c::uid_t,
pid: c::pid_t,
pid_info: Option<PidInfo>,
app_id: Option<String>,
) -> Result<Rc<EiClient>, EiClientError> {
let versions = EiInterfaceVersions {
ei_button: EiInterfaceVersion::new(1),
@ -100,7 +96,7 @@ impl EiClients {
ei_touchscreen: EiInterfaceVersion::new(1),
};
let data = Rc::new(EiClient {
id,
id: global.clients.id(),
state: global.clone(),
context: Cell::new(EiContext::Receiver),
connection: Default::default(),
@ -111,10 +107,11 @@ impl EiClients {
flush_request: Default::default(),
shutdown: Default::default(),
tracker: Default::default(),
pid_info: get_pid_info(uid, pid),
pid_info,
disconnect_announced: Cell::new(false),
versions,
name: Default::default(),
app_id,
last_serial: Default::default(),
});
track!(data, data);
@ -127,12 +124,17 @@ impl EiClients {
data: data.clone(),
};
log::info!(
"Client {} connected, pid: {}, uid: {}, fd: {}, comm: {:?}",
id,
pid,
uid,
client.data.socket.raw(),
data.pid_info.comm,
"Client {} connected{:?}",
data.id,
debug_fn(|fmt| {
if let Some(p) = &data.pid_info {
write!(fmt, ", pid: {}, uid: {}, comm: {:?}", p.pid, p.uid, p.comm)?;
}
if let Some(app_id) = &data.app_id {
write!(fmt, ", app-id: {app_id:?}")?;
}
Ok(())
}),
);
self.clients.borrow_mut().insert(client.data.id, client);
Ok(data)
@ -199,10 +201,11 @@ pub struct EiClient {
flush_request: AsyncEvent,
shutdown: AsyncEvent,
pub tracker: Tracker<EiClient>,
pub pid_info: PidInfo,
pub pid_info: Option<PidInfo>,
pub disconnect_announced: Cell<bool>,
pub versions: EiInterfaceVersions,
pub name: RefCell<Option<String>>,
pub app_id: Option<String>,
pub last_serial: NumCell<u64>,
}

View file

@ -7,6 +7,8 @@ pub mod ext_session_lock_v1;
pub mod ipc;
pub mod jay_compositor;
pub mod jay_damage_tracking;
pub mod jay_ei_session;
pub mod jay_ei_session_builder;
pub mod jay_idle;
pub mod jay_input;
pub mod jay_log_file;

View file

@ -4,6 +4,7 @@ use {
client::{Client, ClientCaps, ClientError, CAP_JAY_COMPOSITOR},
globals::{Global, GlobalName},
ifs::{
jay_ei_session_builder::JayEiSessionBuilder,
jay_idle::JayIdle,
jay_input::JayInput,
jay_log_file::JayLogFile,
@ -30,6 +31,8 @@ use {
thiserror::Error,
};
pub const CREATE_EI_SESSION_SINCE: Version = Version(5);
pub struct JayCompositorGlobal {
name: GlobalName,
}
@ -66,7 +69,7 @@ impl Global for JayCompositorGlobal {
}
fn version(&self) -> u32 {
4
5
}
fn required_caps(&self) -> ClientCaps {
@ -377,6 +380,19 @@ impl JayCompositorRequestHandler for JayCompositor {
seat.global.select_workspace(selector);
Ok(())
}
fn create_ei_session(&self, req: CreateEiSession, _slf: &Rc<Self>) -> Result<(), Self::Error> {
let obj = Rc::new(JayEiSessionBuilder {
id: req.id,
client: self.client.clone(),
tracker: Default::default(),
version: self.version,
app_id: Default::default(),
});
track!(self.client, obj);
self.client.add_client_obj(&obj)?;
Ok(())
}
}
object_base! {

81
src/ifs/jay_ei_session.rs Normal file
View file

@ -0,0 +1,81 @@
use {
crate::{
client::{Client, ClientError, ClientId},
leaks::Tracker,
object::{Object, Version},
wire::{
jay_ei_session::{Created, Destroyed, Failed, JayEiSessionRequestHandler, Release},
JayEiSessionId,
},
},
std::rc::Rc,
thiserror::Error,
uapi::OwnedFd,
};
pub struct JayEiSession {
pub id: JayEiSessionId,
pub client: Rc<Client>,
pub ei_client_id: Option<ClientId>,
pub tracker: Tracker<Self>,
pub version: Version,
}
impl JayEiSession {
pub fn send_created(&self, fd: &Rc<OwnedFd>) {
self.client.event(Created {
self_id: self.id,
fd: fd.clone(),
});
}
pub fn send_failed(&self, reason: &str) {
self.client.event(Failed {
self_id: self.id,
reason,
});
}
fn send_destroyed(&self) {
self.client.event(Destroyed { self_id: self.id });
}
fn kill(&self, send_destroyed: bool) {
if let Some(id) = self.ei_client_id {
self.client.state.ei_clients.shutdown(id);
}
if send_destroyed {
self.send_destroyed();
}
}
}
impl JayEiSessionRequestHandler for JayEiSession {
type Error = JayEiSessionError;
fn release(&self, _req: Release, _slf: &Rc<Self>) -> Result<(), Self::Error> {
self.kill(false);
self.client.remove_obj(self)?;
Ok(())
}
}
object_base! {
self = JayEiSession;
version = self.version;
}
impl Object for JayEiSession {
fn break_loops(&self) {
self.kill(false);
}
}
simple_add_obj!(JayEiSession);
#[derive(Debug, Error)]
pub enum JayEiSessionError {
#[error(transparent)]
ClientError(Box<ClientError>),
}
efrom!(JayEiSessionError, ClientError);

View file

@ -0,0 +1,97 @@
use {
crate::{
client::{Client, ClientError},
ei::ei_client::EiClientError,
ifs::jay_ei_session::JayEiSession,
leaks::Tracker,
object::{Object, Version},
utils::{errorfmt::ErrorFmt, oserror::OsError},
wire::{
jay_ei_session_builder::{Commit, JayEiSessionBuilderRequestHandler, SetAppId},
JayEiSessionBuilderId,
},
},
std::{cell::RefCell, rc::Rc},
thiserror::Error,
uapi::c,
};
pub struct JayEiSessionBuilder {
pub id: JayEiSessionBuilderId,
pub client: Rc<Client>,
pub tracker: Tracker<Self>,
pub version: Version,
pub app_id: RefCell<Option<String>>,
}
impl JayEiSessionBuilderRequestHandler for JayEiSessionBuilder {
type Error = JayEiSessionBuilderError;
fn commit(&self, req: Commit, _slf: &Rc<Self>) -> Result<(), Self::Error> {
self.client.remove_obj(self)?;
let app_id = self.app_id.borrow().clone();
if app_id.is_none() {
return Err(JayEiSessionBuilderError::NoAppId);
}
let res = (move || {
let con = uapi::socketpair(c::AF_UNIX, c::SOCK_STREAM | c::SOCK_CLOEXEC, 0);
let (server, client) = match con {
Ok(w) => w,
Err(e) => return Err(JayEiSessionBuilderError::SocketPair(e.into())),
};
let ei_client_id = self
.client
.state
.ei_clients
.spawn2(&self.client.state, Rc::new(server), None, app_id)
.map_err(JayEiSessionBuilderError::SpawnClient)?
.id;
Ok((ei_client_id, Rc::new(client)))
})();
let obj = Rc::new(JayEiSession {
id: req.id,
client: self.client.clone(),
ei_client_id: res.as_ref().ok().map(|v| v.0),
tracker: Default::default(),
version: self.version,
});
track!(self.client, obj);
self.client.add_client_obj(&obj)?;
match res {
Ok((_, fd)) => obj.send_created(&fd),
Err(e) => {
let e = format!("Could not spawn client: {}", ErrorFmt(e));
log::error!("{}", e);
obj.send_failed(&e);
}
}
Ok(())
}
fn set_app_id(&self, req: SetAppId<'_>, _slf: &Rc<Self>) -> Result<(), Self::Error> {
*self.app_id.borrow_mut() = Some(req.app_id.to_string());
Ok(())
}
}
object_base! {
self = JayEiSessionBuilder;
version = self.version;
}
impl Object for JayEiSessionBuilder {}
simple_add_obj!(JayEiSessionBuilder);
#[derive(Debug, Error)]
pub enum JayEiSessionBuilderError {
#[error(transparent)]
ClientError(Box<ClientError>),
#[error("Could not create a socketpair")]
SocketPair(#[source] OsError),
#[error("Could not spawn a new client")]
SpawnClient(#[source] EiClientError),
#[error("Commit called without app-id")]
NoAppId,
}
efrom!(JayEiSessionBuilderError, ClientError);

View file

@ -1,4 +1,5 @@
mod ptl_display;
mod ptl_remote_desktop;
mod ptl_render_ctx;
mod ptl_screencast;
mod ptr_gui;
@ -17,6 +18,7 @@ use {
pipewire::pw_con::{PwConHolder, PwConOwner},
portal::{
ptl_display::{watch_displays, PortalDisplay, PortalDisplayId},
ptl_remote_desktop::{add_remote_desktop_dbus_members, RemoteDesktopSession},
ptl_render_ctx::PortalRenderCtx,
ptl_screencast::{add_screencast_dbus_members, ScreencastSession},
},
@ -195,6 +197,7 @@ async fn run_async(
displays: Default::default(),
dbus,
screencasts: Default::default(),
remote_desktop_sessions: Default::default(),
next_id: NumCell::new(1),
render_ctxs: Default::default(),
dma_buf_ids: Default::default(),
@ -210,6 +213,7 @@ async fn run_async(
if let Some(pw_con) = &pw_con {
add_screencast_dbus_members(&state, &pw_con.con, &obj);
}
add_remote_desktop_dbus_members(&state, &obj);
obj
};
watch_displays(state.clone()).await;
@ -288,6 +292,7 @@ struct PortalState {
displays: CopyHashMap<PortalDisplayId, Rc<PortalDisplay>>,
dbus: Rc<DbusSocket>,
screencasts: CopyHashMap<String, Rc<ScreencastSession>>,
remote_desktop_sessions: CopyHashMap<String, Rc<RemoteDesktopSession>>,
next_id: NumCell<u32>,
render_ctxs: CopyHashMap<c::dev_t, Weak<PortalRenderCtx>>,
dma_buf_ids: Rc<DmaBufIds>,

View file

@ -4,8 +4,8 @@ use {
ifs::wl_seat::POINTER,
object::Version,
portal::{
ptl_render_ctx::PortalRenderCtx, ptl_screencast::ScreencastSession,
ptr_gui::WindowData, PortalState,
ptl_remote_desktop::RemoteDesktopSession, ptl_render_ctx::PortalRenderCtx,
ptl_screencast::ScreencastSession, ptr_gui::WindowData, PortalState,
},
utils::{
bitflags::BitflagsExt, clonecell::CloneCell, copyhashmap::CopyHashMap,
@ -74,6 +74,7 @@ pub struct PortalDisplay {
pub windows: CopyHashMap<WlSurfaceId, Rc<WindowData>>,
pub screencasts: CopyHashMap<String, Rc<ScreencastSession>>,
pub remote_desktop_sessions: CopyHashMap<String, Rc<RemoteDesktopSession>>,
}
pub struct PortalOutput {
@ -303,7 +304,7 @@ fn finish_display_connect(dpy: Rc<PortalDisplayPrelude>) {
con: dpy.con.clone(),
owner: Default::default(),
caps: Default::default(),
version: Version(version.min(4)),
version: Version(version.min(5)),
});
dpy.con.add_object(jc.clone());
dpy.registry.request_bind(name, version, jc.deref());
@ -395,6 +396,7 @@ fn finish_display_connect(dpy: Rc<PortalDisplayPrelude>) {
vp,
windows: Default::default(),
screencasts: Default::default(),
remote_desktop_sessions: Default::default(),
});
dpy.state.displays.set(dpy.id, dpy.clone());

View file

@ -0,0 +1,384 @@
mod remote_desktop_gui;
use {
crate::{
dbus::{prelude::Variant, DbusObject, DictEntry, DynamicType, PendingReply, FALSE},
ifs::jay_compositor::CREATE_EI_SESSION_SINCE,
portal::{
ptl_display::{PortalDisplay, PortalDisplayId},
ptl_remote_desktop::remote_desktop_gui::SelectionGui,
PortalState, PORTAL_SUCCESS,
},
utils::{
clonecell::{CloneCell, UnsafeCellCloneSafe},
copyhashmap::CopyHashMap,
hash_map_ext::HashMapExt,
},
wire_dbus::{
org,
org::freedesktop::impl_::portal::{
remote_desktop::{
ConnectToEIS, ConnectToEISReply, CreateSession, CreateSessionReply,
SelectDevices, SelectDevicesReply, Start, StartReply,
},
session::{CloseReply as SessionCloseReply, Closed},
},
},
wl_usr::usr_ifs::usr_jay_ei_session::{UsrJayEiSession, UsrJayEiSessionOwner},
},
std::{borrow::Cow, cell::Cell, ops::Deref, rc::Rc},
uapi::OwnedFd,
};
shared_ids!(ScreencastSessionId);
pub struct RemoteDesktopSession {
_id: ScreencastSessionId,
state: Rc<PortalState>,
pub app: String,
session_obj: DbusObject,
pub phase: CloneCell<RemoteDesktopPhase>,
}
#[derive(Clone)]
pub enum RemoteDesktopPhase {
Init,
DevicesSelected,
Selecting(Rc<SelectingDisplay>),
Starting(Rc<StartingRemoteDesktop>),
Started(Rc<StartedRemoteDesktop>),
Terminated,
}
unsafe impl UnsafeCellCloneSafe for RemoteDesktopPhase {}
pub struct SelectingDisplay {
pub session: Rc<RemoteDesktopSession>,
pub request_obj: Rc<DbusObject>,
pub reply: Rc<PendingReply<StartReply<'static>>>,
pub guis: CopyHashMap<PortalDisplayId, Rc<SelectionGui>>,
}
pub struct StartingRemoteDesktop {
pub session: Rc<RemoteDesktopSession>,
pub _request_obj: Rc<DbusObject>,
pub reply: Rc<PendingReply<StartReply<'static>>>,
pub dpy: Rc<PortalDisplay>,
pub ei_session: Rc<UsrJayEiSession>,
}
pub struct StartedRemoteDesktop {
session: Rc<RemoteDesktopSession>,
dpy: Rc<PortalDisplay>,
ei_session: Rc<UsrJayEiSession>,
ei_fd: Cell<Option<Rc<OwnedFd>>>,
}
bitflags! {
DeviceTypes: u32;
KEYBOARD = 1,
POINTER = 2,
TOUCHSCREEN = 4,
}
impl UsrJayEiSessionOwner for StartingRemoteDesktop {
fn created(&self, fd: &Rc<OwnedFd>) {
{
let inner_type = DynamicType::DictEntry(
Box::new(DynamicType::String),
Box::new(DynamicType::Variant),
);
let kt = DynamicType::Struct(vec![
DynamicType::U32,
DynamicType::Array(Box::new(inner_type.clone())),
]);
let variants = [
DictEntry {
key: "devices".into(),
value: Variant::U32(DeviceTypes::all().0),
},
DictEntry {
key: "clipboard_enabled".into(),
value: Variant::Bool(FALSE),
},
DictEntry {
key: "streams".into(),
value: Variant::Array(kt, vec![]),
},
];
self.reply.ok(&StartReply {
response: PORTAL_SUCCESS,
results: Cow::Borrowed(&variants[..]),
});
}
let started = Rc::new(StartedRemoteDesktop {
session: self.session.clone(),
dpy: self.dpy.clone(),
ei_session: self.ei_session.clone(),
ei_fd: Cell::new(Some(fd.clone())),
});
self.session
.phase
.set(RemoteDesktopPhase::Started(started.clone()));
started.ei_session.owner.set(Some(started.clone()));
}
fn failed(&self, reason: &str) {
log::error!("Could not create session: {}", reason);
self.reply.err(reason);
self.session.kill();
}
}
impl SelectingDisplay {
pub fn starting(&self, dpy: &Rc<PortalDisplay>) {
let builder = dpy.jc.create_ei_session();
builder.set_app_id(&self.session.app);
let ei_session = builder.commit();
let starting = Rc::new(StartingRemoteDesktop {
session: self.session.clone(),
_request_obj: self.request_obj.clone(),
reply: self.reply.clone(),
dpy: dpy.clone(),
ei_session,
});
self.session
.phase
.set(RemoteDesktopPhase::Starting(starting.clone()));
starting.ei_session.owner.set(Some(starting.clone()));
dpy.remote_desktop_sessions.set(
self.session.session_obj.path().to_owned(),
self.session.clone(),
);
}
}
impl RemoteDesktopSession {
pub(super) fn kill(&self) {
self.session_obj.emit_signal(&Closed);
self.state
.remote_desktop_sessions
.remove(self.session_obj.path());
match self.phase.set(RemoteDesktopPhase::Terminated) {
RemoteDesktopPhase::Init => {}
RemoteDesktopPhase::DevicesSelected => {}
RemoteDesktopPhase::Terminated => {}
RemoteDesktopPhase::Selecting(s) => {
s.reply.err("Session has been terminated");
for gui in s.guis.lock().drain_values() {
gui.kill(false);
}
}
RemoteDesktopPhase::Starting(s) => {
s.reply.err("Session has been terminated");
s.ei_session.con.remove_obj(s.ei_session.deref());
s.dpy
.remote_desktop_sessions
.remove(self.session_obj.path());
}
RemoteDesktopPhase::Started(s) => {
s.ei_session.con.remove_obj(s.ei_session.deref());
s.dpy
.remote_desktop_sessions
.remove(self.session_obj.path());
}
}
}
fn dbus_select_devices(
self: &Rc<Self>,
_req: SelectDevices,
reply: PendingReply<SelectDevicesReply<'static>>,
) {
match self.phase.get() {
RemoteDesktopPhase::Init => {}
_ => {
self.kill();
reply.err("Devices have already been selected");
return;
}
}
self.phase.set(RemoteDesktopPhase::DevicesSelected);
reply.ok(&SelectDevicesReply {
response: PORTAL_SUCCESS,
results: Default::default(),
});
}
fn dbus_start(self: &Rc<Self>, req: Start<'_>, reply: PendingReply<StartReply<'static>>) {
match self.phase.get() {
RemoteDesktopPhase::DevicesSelected => {}
_ => {
self.kill();
reply.err("Session is not in the correct phase for starting");
return;
}
}
let request_obj = match self.state.dbus.add_object(req.handle.to_string()) {
Ok(r) => r,
Err(_) => {
self.kill();
reply.err("Request handle is not unique");
return;
}
};
{
use org::freedesktop::impl_::portal::request::*;
request_obj.add_method::<Close, _>({
let slf = self.clone();
move |_, pr| {
slf.kill();
pr.ok(&CloseReply);
}
});
}
let guis = CopyHashMap::new();
for dpy in self.state.displays.lock().values() {
if dpy.outputs.len() > 0 && dpy.jc.version >= CREATE_EI_SESSION_SINCE {
guis.set(dpy.id, SelectionGui::new(self, dpy));
}
}
if guis.is_empty() {
self.kill();
reply.err("There are no running displays");
return;
}
self.phase
.set(RemoteDesktopPhase::Selecting(Rc::new(SelectingDisplay {
session: self.clone(),
request_obj: Rc::new(request_obj),
reply: Rc::new(reply),
guis,
})));
}
fn dbus_connect_to_eis(
self: &Rc<Self>,
_req: ConnectToEIS,
reply: PendingReply<ConnectToEISReply>,
) {
let RemoteDesktopPhase::Started(started) = self.phase.get() else {
self.kill();
reply.err("Sources have already been selected");
return;
};
let Some(fd) = started.ei_fd.take() else {
self.kill();
reply.err("EI file descriptor has already been consumed");
return;
};
reply.ok(&ConnectToEISReply { fd });
}
}
impl UsrJayEiSessionOwner for StartedRemoteDesktop {
fn destroyed(&self) {
self.session.kill();
}
}
pub(super) fn add_remote_desktop_dbus_members(state_: &Rc<PortalState>, object: &DbusObject) {
use org::freedesktop::impl_::portal::remote_desktop::*;
let state = state_.clone();
object.add_method::<CreateSession, _>(move |req, pr| {
dbus_create_session(&state, req, pr);
});
let state = state_.clone();
object.add_method::<SelectDevices, _>(move |req, pr| {
dbus_select_devices(&state, req, pr);
});
let state = state_.clone();
object.add_method::<Start, _>(move |req, pr| {
dbus_start(&state, req, pr);
});
let state = state_.clone();
object.add_method::<ConnectToEIS, _>(move |req, pr| {
dbus_connect_to_eis(&state, req, pr);
});
object.set_property::<AvailableDeviceTypes>(Variant::U32(DeviceTypes::all().0));
object.set_property::<version>(Variant::U32(2));
}
fn dbus_create_session(
state: &Rc<PortalState>,
req: CreateSession,
reply: PendingReply<CreateSessionReply<'static>>,
) {
log::info!("Create remote desktop session {:#?}", req);
if state
.remote_desktop_sessions
.contains(req.session_handle.0.deref())
{
reply.err("Session already exists");
return;
}
let obj = match state.dbus.add_object(req.session_handle.0.to_string()) {
Ok(obj) => obj,
Err(_) => {
reply.err("Session path is not unique");
return;
}
};
let session = Rc::new(RemoteDesktopSession {
_id: state.id(),
state: state.clone(),
app: req.app_id.to_string(),
session_obj: obj,
phase: CloneCell::new(RemoteDesktopPhase::Init),
});
{
use org::freedesktop::impl_::portal::session::*;
let ses = session.clone();
session.session_obj.add_method::<Close, _>(move |_, pr| {
ses.kill();
pr.ok(&SessionCloseReply);
});
session.session_obj.set_property::<version>(Variant::U32(2));
}
state
.remote_desktop_sessions
.set(req.session_handle.0.to_string(), session);
reply.ok(&CreateSessionReply {
response: PORTAL_SUCCESS,
results: Default::default(),
});
}
fn dbus_select_devices(
state: &Rc<PortalState>,
req: SelectDevices,
reply: PendingReply<SelectDevicesReply<'static>>,
) {
if let Some(s) = get_session(state, &reply, &req.session_handle.0) {
s.dbus_select_devices(req, reply);
}
}
fn dbus_start(state: &Rc<PortalState>, req: Start, reply: PendingReply<StartReply<'static>>) {
if let Some(s) = get_session(state, &reply, &req.session_handle.0) {
s.dbus_start(req, reply);
}
}
fn dbus_connect_to_eis(
state: &Rc<PortalState>,
req: ConnectToEIS,
reply: PendingReply<ConnectToEISReply>,
) {
if let Some(s) = get_session(state, &reply, &req.session_handle.0) {
s.dbus_connect_to_eis(req, reply);
}
}
fn get_session<T>(
state: &Rc<PortalState>,
reply: &PendingReply<T>,
handle: &str,
) -> Option<Rc<RemoteDesktopSession>> {
let res = state.remote_desktop_sessions.get(handle);
if res.is_none() {
let msg = format!("Remote desktop session `{}` does not exist", handle);
reply.err(&msg);
}
res
}

View file

@ -0,0 +1,159 @@
use {
crate::{
ifs::wl_seat::{wl_pointer::PRESSED, BTN_LEFT},
portal::{
ptl_display::{PortalDisplay, PortalOutput, PortalSeat},
ptl_remote_desktop::{RemoteDesktopPhase, RemoteDesktopSession},
ptr_gui::{
Align, Button, ButtonOwner, Flow, GuiElement, Label, Orientation, OverlayWindow,
OverlayWindowOwner,
},
},
theme::Color,
utils::{copyhashmap::CopyHashMap, hash_map_ext::HashMapExt},
},
std::rc::Rc,
};
const H_MARGIN: f32 = 30.0;
const V_MARGIN: f32 = 20.0;
pub struct SelectionGui {
remote_desktop_session: Rc<RemoteDesktopSession>,
dpy: Rc<PortalDisplay>,
surfaces: CopyHashMap<u32, Rc<SelectionGuiSurface>>,
}
pub struct SelectionGuiSurface {
gui: Rc<SelectionGui>,
output: Rc<PortalOutput>,
overlay: Rc<OverlayWindow>,
}
struct StaticButton {
surface: Rc<SelectionGuiSurface>,
role: ButtonRole,
}
#[derive(Copy, Clone, Eq, PartialEq)]
enum ButtonRole {
Accept,
Reject,
}
impl SelectionGui {
pub fn kill(&self, upwards: bool) {
for surface in self.surfaces.lock().drain_values() {
surface.overlay.data.kill(false);
}
if let RemoteDesktopPhase::Selecting(s) = self.remote_desktop_session.phase.get() {
s.guis.remove(&self.dpy.id);
if upwards && s.guis.is_empty() {
self.remote_desktop_session.kill();
}
}
}
}
fn create_accept_gui(surface: &Rc<SelectionGuiSurface>) -> Rc<dyn GuiElement> {
let app = &surface.gui.remote_desktop_session.app;
let text = if app.is_empty() {
format!("An application wants to generate/monitor input")
} else {
format!("`{}` wants to generate/monitor input", app)
};
let label = Rc::new(Label::default());
*label.text.borrow_mut() = text;
let accept_button = static_button(surface, ButtonRole::Accept, "Allow");
let reject_button = static_button(surface, ButtonRole::Reject, "Reject");
for button in [&accept_button, &reject_button] {
button.border_color.set(Color::from_gray(100));
button.border.set(2.0);
button.padding.set(5.0);
}
accept_button.bg_color.set(Color::from_rgb(170, 200, 170));
accept_button
.bg_hover_color
.set(Color::from_rgb(170, 255, 170));
reject_button.bg_color.set(Color::from_rgb(200, 170, 170));
reject_button
.bg_hover_color
.set(Color::from_rgb(255, 170, 170));
let flow = Rc::new(Flow::default());
flow.orientation.set(Orientation::Vertical);
flow.cross_align.set(Align::Center);
flow.in_margin.set(V_MARGIN);
flow.cross_margin.set(H_MARGIN);
*flow.elements.borrow_mut() = vec![label, accept_button, reject_button];
flow
}
impl OverlayWindowOwner for SelectionGuiSurface {
fn kill(&self, upwards: bool) {
self.gui.dpy.windows.remove(&self.overlay.data.surface.id);
self.gui.surfaces.remove(&self.output.global_id);
if upwards && self.gui.surfaces.is_empty() {
self.gui.kill(true);
}
}
}
impl SelectionGui {
pub fn new(ss: &Rc<RemoteDesktopSession>, dpy: &Rc<PortalDisplay>) -> Rc<Self> {
let gui = Rc::new(SelectionGui {
remote_desktop_session: ss.clone(),
dpy: dpy.clone(),
surfaces: Default::default(),
});
for output in dpy.outputs.lock().values() {
let sgs = Rc::new(SelectionGuiSurface {
gui: gui.clone(),
output: output.clone(),
overlay: OverlayWindow::new(output),
});
let element = create_accept_gui(&sgs);
sgs.overlay.data.content.set(Some(element));
gui.dpy
.windows
.set(sgs.overlay.data.surface.id, sgs.overlay.data.clone());
gui.surfaces.set(output.global_id, sgs);
}
gui
}
}
impl ButtonOwner for StaticButton {
fn button(&self, _seat: &PortalSeat, button: u32, state: u32) {
if button != BTN_LEFT || state != PRESSED {
return;
}
match self.role {
ButtonRole::Accept => {
log::info!("User has accepted the request");
let selecting = match self.surface.gui.remote_desktop_session.phase.get() {
RemoteDesktopPhase::Selecting(selecting) => selecting,
_ => return,
};
for gui in selecting.guis.lock().drain_values() {
gui.kill(false);
}
selecting.starting(&self.surface.output.dpy);
}
ButtonRole::Reject => {
log::info!("User has rejected the remote desktop request");
self.surface.gui.remote_desktop_session.kill();
}
}
}
}
fn static_button(surface: &Rc<SelectionGuiSurface>, role: ButtonRole, text: &str) -> Rc<Button> {
let button = Rc::new(Button::default());
let slf = Rc::new(StaticButton {
surface: surface.clone(),
role,
});
button.owner.set(Some(slf));
*button.text.borrow_mut() = text.to_string();
button
}

View file

@ -5,7 +5,7 @@ use {
};
pub struct PidInfo {
pub _uid: c::uid_t,
pub uid: c::uid_t,
pub pid: c::pid_t,
pub comm: String,
}
@ -18,11 +18,7 @@ pub fn get_pid_info(uid: c::uid_t, pid: c::pid_t) -> PidInfo {
"Unknown".to_string()
}
};
PidInfo {
_uid: uid,
pid,
comm,
}
PidInfo { uid, pid, comm }
}
pub fn get_socket_creds(socket: &OwnedFd) -> Option<(c::uid_t, c::pid_t)> {

View file

@ -1,4 +1,6 @@
pub mod usr_jay_compositor;
pub mod usr_jay_ei_session;
pub mod usr_jay_ei_session_builder;
pub mod usr_jay_output;
pub mod usr_jay_pointer;
pub mod usr_jay_render_ctx;

View file

@ -6,8 +6,9 @@ use {
wire::{jay_compositor::*, JayCompositorId},
wl_usr::{
usr_ifs::{
usr_jay_output::UsrJayOutput, usr_jay_pointer::UsrJayPointer,
usr_jay_render_ctx::UsrJayRenderCtx, usr_jay_screencast::UsrJayScreencast,
usr_jay_ei_session_builder::UsrJayEiSessionBuilder, usr_jay_output::UsrJayOutput,
usr_jay_pointer::UsrJayPointer, usr_jay_render_ctx::UsrJayRenderCtx,
usr_jay_screencast::UsrJayScreencast,
usr_jay_select_toplevel::UsrJaySelectToplevel,
usr_jay_select_workspace::UsrJaySelectWorkspace,
usr_jay_workspace_watcher::UsrJayWorkspaceWatcher, usr_wl_output::UsrWlOutput,
@ -156,6 +157,20 @@ impl UsrJayCompositor {
self.con.add_object(sc.clone());
sc
}
pub fn create_ei_session(&self) -> Rc<UsrJayEiSessionBuilder> {
let obj = Rc::new(UsrJayEiSessionBuilder {
id: self.con.id(),
con: self.con.clone(),
version: self.version,
});
self.con.request(CreateEiSession {
self_id: self.id,
id: obj.id,
});
self.con.add_object(obj.clone());
obj
}
}
impl JayCompositorEventHandler for UsrJayCompositor {

View file

@ -0,0 +1,72 @@
use {
crate::{
object::Version,
utils::clonecell::CloneCell,
wire::{
jay_ei_session::{Created, Destroyed, Failed, JayEiSessionEventHandler, Release},
JayEiSessionId,
},
wl_usr::{usr_object::UsrObject, UsrCon},
},
std::{convert::Infallible, rc::Rc},
uapi::OwnedFd,
};
pub struct UsrJayEiSession {
pub id: JayEiSessionId,
pub con: Rc<UsrCon>,
pub owner: CloneCell<Option<Rc<dyn UsrJayEiSessionOwner>>>,
pub version: Version,
}
pub trait UsrJayEiSessionOwner {
fn destroyed(&self) {}
fn created(&self, fd: &Rc<OwnedFd>) {
let _ = fd;
}
fn failed(&self, reason: &str) {
let _ = reason;
}
}
impl JayEiSessionEventHandler for UsrJayEiSession {
type Error = Infallible;
fn destroyed(&self, _ev: Destroyed, _slf: &Rc<Self>) -> Result<(), Self::Error> {
if let Some(owner) = self.owner.get() {
owner.destroyed();
}
Ok(())
}
fn created(&self, ev: Created, _slf: &Rc<Self>) -> Result<(), Self::Error> {
if let Some(owner) = self.owner.get() {
owner.created(&ev.fd);
}
Ok(())
}
fn failed(&self, ev: Failed<'_>, _slf: &Rc<Self>) -> Result<(), Self::Error> {
if let Some(owner) = self.owner.get() {
owner.failed(ev.reason);
}
Ok(())
}
}
usr_object_base! {
self = UsrJayEiSession = JayEiSession;
version = self.version;
}
impl UsrObject for UsrJayEiSession {
fn destroy(&self) {
self.con.request(Release { self_id: self.id });
}
fn break_loops(&self) {
self.owner.take();
}
}

View file

@ -0,0 +1,57 @@
use {
crate::{
object::Version,
wire::{
jay_ei_session_builder::{Commit, JayEiSessionBuilderEventHandler, SetAppId},
JayEiSessionBuilderId,
},
wl_usr::{usr_ifs::usr_jay_ei_session::UsrJayEiSession, usr_object::UsrObject, UsrCon},
},
std::{convert::Infallible, rc::Rc},
};
pub struct UsrJayEiSessionBuilder {
pub id: JayEiSessionBuilderId,
pub con: Rc<UsrCon>,
pub version: Version,
}
impl UsrJayEiSessionBuilder {
pub fn set_app_id(&self, app_id: &str) {
self.con.request(SetAppId {
self_id: self.id,
app_id,
});
}
pub fn commit(&self) -> Rc<UsrJayEiSession> {
let obj = Rc::new(UsrJayEiSession {
id: self.con.id(),
con: self.con.clone(),
owner: Default::default(),
version: self.version,
});
self.con.add_object(obj.clone());
self.con.request(Commit {
self_id: self.id,
id: obj.id,
});
self.con.remove_obj(self);
obj
}
}
impl JayEiSessionBuilderEventHandler for UsrJayEiSessionBuilder {
type Error = Infallible;
}
usr_object_base! {
self = UsrJayEiSessionBuilder = JayEiSessionBuilder;
version = self.version;
}
impl UsrObject for UsrJayEiSessionBuilder {
fn destroy(&self) {
// nothing
}
}

View file

@ -0,0 +1,41 @@
fn CreateSession(
handle: object_path,
session_handle: object_path,
app_id: string,
options: array(dict(string, variant)),
) {
response: u32,
results: array(dict(string, variant)),
}
fn SelectDevices(
handle: object_path,
session_handle: object_path,
app_id: string,
options: array(dict(string, variant)),
) {
response: u32,
results: array(dict(string, variant)),
}
fn Start(
handle: object_path,
session_handle: object_path,
app_id: string,
parent_window: string,
options: array(dict(string, variant)),
) {
response: u32,
results: array(dict(string, variant)),
}
fn ConnectToEIS(
session_handle: object_path,
app_id: string,
options: array(dict(string, variant)),
) {
fd: fd,
}
prop AvailableDeviceTypes = u32
prop version = u32

View file

@ -88,6 +88,10 @@ request select_workspace {
seat: id(wl_seat),
}
request create_ei_session (since = 5) {
id: id(jay_ei_session_builder),
}
# events
event client_id {

15
wire/jay_ei_session.txt Normal file
View file

@ -0,0 +1,15 @@
request release (since = 5) {
}
event destroyed (since = 5) {
}
event created (since = 5) {
fd: fd,
}
event failed (since = 5) {
reason: str,
}

View file

@ -0,0 +1,7 @@
request commit (since = 5) {
id: id(jay_ei_session),
}
request set_app_id (since = 5) {
app_id: str,
}