1
0
Fork 0
forked from wry/wry

portal: implement session restoration

This commit is contained in:
Julian Orth 2024-10-10 16:48:46 +02:00
parent 4f431eec5c
commit 3e3532574b
13 changed files with 436 additions and 58 deletions

View file

@ -5,7 +5,7 @@ use {
allocator::{AllocatorError, BufferObject, BufferUsage, BO_USE_RENDERING},
dbus::{prelude::Variant, DbusObject, DictEntry, DynamicType, PendingReply},
format::{Format, XRGB8888},
ifs::jay_screencast::CLIENT_BUFFERS_SINCE,
ifs::{jay_compositor::GET_TOPLEVEL_SINCE, jay_screencast::CLIENT_BUFFERS_SINCE},
pipewire::{
pw_con::PwCon,
pw_ifs::pw_client_node::{
@ -29,6 +29,7 @@ use {
copyhashmap::CopyHashMap,
errorfmt::ErrorFmt,
hash_map_ext::HashMapExt,
opaque::Opaque,
},
video::{dmabuf::DmaBuf, Modifier, LINEAR_MODIFIER},
wire::jay_screencast::Ready,
@ -54,6 +55,7 @@ use {
usr_wl_buffer::UsrWlBuffer,
},
},
serde::{Deserialize, Serialize},
std::{
borrow::Cow,
cell::{Cell, RefCell},
@ -77,7 +79,7 @@ pub struct ScreencastSession {
#[derive(Clone)]
pub enum ScreencastPhase {
Init,
SourcesSelected,
SourcesSelected(Rc<SourcesSelectedScreencast>),
Selecting(Rc<SelectingScreencast>),
SelectingWindow(Rc<SelectingWindowScreencast>),
SelectingWorkspace(Rc<SelectingWorkspaceScreencast>),
@ -88,6 +90,10 @@ pub enum ScreencastPhase {
unsafe impl UnsafeCellCloneSafe for ScreencastPhase {}
pub struct SourcesSelectedScreencast {
pub restore_data: Cell<Option<Result<RestoreData, RestoreError>>>,
}
#[derive(Clone)]
pub struct SelectingScreencastCore {
pub session: Rc<ScreencastSession>,
@ -98,12 +104,14 @@ pub struct SelectingScreencastCore {
pub struct SelectingScreencast {
pub core: SelectingScreencastCore,
pub guis: CopyHashMap<PortalDisplayId, Rc<SelectionGui>>,
pub restore_data: Cell<Option<RestoreData>>,
}
pub struct SelectingWindowScreencast {
pub core: SelectingScreencastCore,
pub dpy: Rc<PortalDisplay>,
pub selector: Rc<UsrJaySelectToplevel>,
pub restoring: bool,
}
pub struct SelectingWorkspaceScreencast {
@ -123,7 +131,7 @@ pub struct StartingScreencast {
pub enum ScreencastTarget {
Output(Rc<PortalOutput>),
Workspace(Rc<PortalOutput>, Rc<UsrJayWorkspace>),
Workspace(Rc<PortalOutput>, Rc<UsrJayWorkspace>, bool),
Toplevel(Rc<UsrJayToplevel>),
}
@ -171,16 +179,22 @@ impl PwClientNodeOwner for StartingScreencast {
DynamicType::U32,
DynamicType::Array(Box::new(inner_type.clone())),
]);
let variants = &[DictEntry {
let mut variants = vec![DictEntry {
key: "streams".into(),
value: Variant::Array(
kt,
vec![Variant::U32(node_id), Variant::Array(inner_type, vec![])],
),
}];
if let Some(rd) = create_restore_data(&self.dpy, &self.target) {
variants.push(DictEntry {
key: "restore_data".into(),
value: rd,
});
}
self.reply.ok(&StartReply {
response: PORTAL_SUCCESS,
results: Cow::Borrowed(variants),
results: Cow::Owned(variants),
});
}
let mut supported_formats = PwClientNodePortSupportedFormats {
@ -221,7 +235,7 @@ impl PwClientNodeOwner for StartingScreencast {
jsc.set_output(&o.jay);
jsc.set_allow_all_workspaces(true);
}
ScreencastTarget::Workspace(o, ws) => {
ScreencastTarget::Workspace(o, ws, _) => {
jsc.set_output(&o.jay);
jsc.allow_workspace(ws);
}
@ -231,9 +245,10 @@ impl PwClientNodeOwner for StartingScreencast {
jsc.configure();
match &self.target {
ScreencastTarget::Output(_) => {}
ScreencastTarget::Workspace(_, w) => {
ScreencastTarget::Workspace(_, w, true) => {
self.dpy.con.remove_obj(&**w);
}
ScreencastTarget::Workspace(_, _, false) => {}
ScreencastTarget::Toplevel(t) => {
self.dpy.con.remove_obj(&**t);
}
@ -439,7 +454,7 @@ impl ScreencastSession {
self.state.screencasts.remove(self.session_obj.path());
match self.phase.set(ScreencastPhase::Terminated) {
ScreencastPhase::Init => {}
ScreencastPhase::SourcesSelected => {}
ScreencastPhase::SourcesSelected(_) => {}
ScreencastPhase::Terminated => {}
ScreencastPhase::Selecting(s) => {
s.core.reply.err("Session has been terminated");
@ -461,9 +476,10 @@ impl ScreencastSession {
s.dpy.screencasts.remove(self.session_obj.path());
match &s.target {
ScreencastTarget::Output(_) => {}
ScreencastTarget::Workspace(_, w) => {
ScreencastTarget::Workspace(_, w, true) => {
s.dpy.con.remove_obj(&**w);
}
ScreencastTarget::Workspace(_, _, false) => {}
ScreencastTarget::Toplevel(t) => {
s.dpy.con.remove_obj(&**t);
}
@ -482,7 +498,7 @@ impl ScreencastSession {
fn dbus_select_sources(
self: &Rc<Self>,
_req: SelectSources,
req: SelectSources,
reply: PendingReply<SelectSourcesReply<'static>>,
) {
match self.phase.get() {
@ -493,7 +509,11 @@ impl ScreencastSession {
return;
}
}
self.phase.set(ScreencastPhase::SourcesSelected);
self.phase.set(ScreencastPhase::SourcesSelected(Rc::new(
SourcesSelectedScreencast {
restore_data: Cell::new(get_restore_data(&req)),
},
)));
reply.ok(&SelectSourcesReply {
response: PORTAL_SUCCESS,
results: Default::default(),
@ -501,16 +521,16 @@ impl ScreencastSession {
}
fn dbus_start(self: &Rc<Self>, req: Start<'_>, reply: PendingReply<StartReply<'static>>) {
match self.phase.get() {
ScreencastPhase::SourcesSelected => {}
let restore_data = match self.phase.get() {
ScreencastPhase::SourcesSelected(s) => s.restore_data.take(),
_ => {
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,
Ok(r) => Rc::new(r),
Err(_) => {
self.kill();
reply.err("Request handle is not unique");
@ -527,10 +547,20 @@ impl ScreencastSession {
}
});
}
let reply = Rc::new(reply);
self.restore(&request_obj, &reply, restore_data, None);
}
fn start_interactive_selection(
self: &Rc<Self>,
request_obj: &Rc<DbusObject>,
reply: &Rc<PendingReply<StartReply<'static>>>,
restore_data: Option<RestoreData>,
) {
let guis = CopyHashMap::new();
for dpy in self.state.displays.lock().values() {
if dpy.outputs.len() > 0 {
guis.set(dpy.id, SelectionGui::new(self, dpy));
guis.set(dpy.id, SelectionGui::new(self, dpy, restore_data.is_some()));
}
}
if guis.is_empty() {
@ -542,12 +572,120 @@ impl ScreencastSession {
.set(ScreencastPhase::Selecting(Rc::new(SelectingScreencast {
core: SelectingScreencastCore {
session: self.clone(),
request_obj: Rc::new(request_obj),
reply: Rc::new(reply),
request_obj: request_obj.clone(),
reply: reply.clone(),
},
guis,
restore_data: Cell::new(restore_data),
})));
}
fn restore(
self: &Rc<Self>,
request_obj: &Rc<DbusObject>,
reply: &Rc<PendingReply<StartReply<'static>>>,
restore_data: Option<Result<RestoreData, RestoreError>>,
display: Option<Rc<PortalDisplay>>,
) {
if let Some(rd) = restore_data {
if let Err(e) = self.try_restore(&request_obj, &reply, rd, display) {
log::error!("Could not restore session: {}", ErrorFmt(e));
} else {
return;
}
}
self.start_interactive_selection(&request_obj, &reply, None);
}
fn try_restore(
self: &Rc<Self>,
request_obj: &Rc<DbusObject>,
reply: &Rc<PendingReply<StartReply<'static>>>,
restore_data: Result<RestoreData, RestoreError>,
display: Option<Rc<PortalDisplay>>,
) -> Result<(), RestoreError> {
let rd = restore_data?;
let dpy = if let Some(dpy) = display {
dpy
} else {
let dpy = self
.state
.displays
.lock()
.values()
.find(|d| d.unique_id == rd.display)
.cloned();
match dpy {
Some(dpy) => dpy,
_ => {
if self.state.displays.len() == 0 {
return Err(RestoreError::UnknownDisplay);
} else if self.state.displays.len() == 1 {
self.state.displays.lock().values().next().unwrap().clone()
} else {
self.start_interactive_selection(&request_obj, &reply, Some(rd));
return Ok(());
}
}
}
};
let start = |target: ScreencastTarget| {
SelectingScreencastCore {
session: self.clone(),
request_obj: request_obj.clone(),
reply: reply.clone(),
}
.starting(&dpy, target);
};
match &rd.ty {
RestoreDataType::Output(d) => {
let output = dpy
.outputs
.lock()
.values()
.find(|o| o.wl.name.borrow().as_ref() == Some(&d.name))
.cloned();
let Some(output) = output else {
return Err(RestoreError::UnknownOutput);
};
start(ScreencastTarget::Output(output));
}
RestoreDataType::Workspace(ws) => {
let ws = dpy
.workspaces
.lock()
.values()
.find(|w| w.name.borrow().as_ref() == Some(&ws.name))
.cloned();
let Some(ws) = ws else {
return Err(RestoreError::UnknownWorkspace);
};
let Some(output) = dpy.outputs.get(&ws.output.get()) else {
return Err(RestoreError::UnknownOutput);
};
start(ScreencastTarget::Workspace(output, ws, false));
}
RestoreDataType::Toplevel(d) => {
if dpy.jc.version < GET_TOPLEVEL_SINCE {
return Err(RestoreError::GetToplevel);
}
let selector = dpy.jc.get_toplevel(&d.id);
let selecting = Rc::new(SelectingWindowScreencast {
core: SelectingScreencastCore {
session: self.clone(),
request_obj: request_obj.clone(),
reply: reply.clone(),
},
dpy: dpy.clone(),
selector: selector.clone(),
restoring: true,
});
selector.owner.set(Some(selecting.clone()));
self.phase.set(ScreencastPhase::SelectingWindow(selecting));
}
}
Ok(())
}
}
impl UsrJayScreencastOwner for StartedScreencast {
@ -760,3 +898,122 @@ fn get_session<T>(
}
res
}
fn create_restore_data(dpy: &PortalDisplay, rd: &ScreencastTarget) -> Option<Variant<'static>> {
let rd = RestoreData {
display: dpy.unique_id,
ty: match rd {
ScreencastTarget::Output(o) => RestoreDataType::Output(RestoreDataOutput {
name: o.wl.name.borrow().clone()?,
}),
ScreencastTarget::Workspace(_, w, _) => {
RestoreDataType::Workspace(RestoreDataWorkspace {
name: w.name.borrow().clone()?,
})
}
ScreencastTarget::Toplevel(tl) => RestoreDataType::Toplevel(RestoreDataToplevel {
id: tl.toplevel_id.borrow().clone()?,
}),
},
};
Some(Variant::Struct(vec![
Variant::String("Jay".into()),
Variant::U32(1),
Variant::Variant(Box::new(Variant::String(
serde_json::to_string(&rd).unwrap().into(),
))),
]))
}
#[derive(Debug, Error)]
pub enum RestoreError {
#[error("DBus restore data is not a struct")]
NotAStruct,
#[error("DBus restore data is not a struct with 3 fields")]
NotLen3,
#[error("DBus restore data first field is not a string")]
FirstNotString,
#[error("DBus restore data second field is not a u32")]
SecondNotU32,
#[error("DBus restore data third field is not a variant")]
ThirdNotVariant,
#[error("DBus restore data third field is not a string")]
ThirdNotString,
#[error("DBus restore data is not for Jay")]
NotJay,
#[error("DBus restore data is not version 1")]
NotVersion1,
#[error("DBus restore data could not be deserialized")]
Parse(#[source] serde_json::Error),
#[error("The display no longer exists")]
UnknownDisplay,
#[error("The output no longer exists")]
UnknownOutput,
#[error("The workspace no longer exists")]
UnknownWorkspace,
#[error("The display does not support toplevel restoration")]
GetToplevel,
}
fn get_restore_data(req: &SelectSources) -> Option<Result<RestoreData, RestoreError>> {
let restore_data = req.options.iter().find(|n| n.key == "restore_data")?;
Some(get_restore_data_(restore_data))
}
fn get_restore_data_(
restore_data: &DictEntry<Cow<str>, Variant>,
) -> Result<RestoreData, RestoreError> {
let Variant::Struct(s) = &restore_data.value else {
return Err(RestoreError::NotAStruct);
};
if s.len() != 3 {
return Err(RestoreError::NotLen3);
}
let Variant::String(compositor) = &s[0] else {
return Err(RestoreError::FirstNotString);
};
let Variant::U32(version) = &s[1] else {
return Err(RestoreError::SecondNotU32);
};
let Variant::Variant(restore_data) = &s[2] else {
return Err(RestoreError::ThirdNotVariant);
};
let Variant::String(restore_data) = &**restore_data else {
return Err(RestoreError::ThirdNotString);
};
if compositor != "Jay" {
return Err(RestoreError::NotJay);
}
if *version != 1 {
return Err(RestoreError::NotVersion1);
}
serde_json::from_str(restore_data).map_err(RestoreError::Parse)
}
#[derive(Serialize, Deserialize)]
pub struct RestoreData {
display: Opaque,
ty: RestoreDataType,
}
#[derive(Serialize, Deserialize)]
enum RestoreDataType {
Output(RestoreDataOutput),
Workspace(RestoreDataWorkspace),
Toplevel(RestoreDataToplevel),
}
#[derive(Serialize, Deserialize)]
struct RestoreDataOutput {
name: String,
}
#[derive(Serialize, Deserialize)]
struct RestoreDataWorkspace {
name: String,
}
#[derive(Serialize, Deserialize)]
struct RestoreDataToplevel {
id: String,
}