Implement native inhibition and acknowledged session-ending confirmation
This commit is contained in:
1 parent
cc441c049e
commit
29a71e9974
27 files changed
+1915
-142
No files matched your search
@@ -105,8 +105,7 @@ impl CompositorHandler for Ferese {
|
||||
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
|
||||
crate::backends::direct::render_all(self);
|
||||
if self.idle_inhibitors.remove(surface).is_some() {
|
||||
self.idle_notifier_state
|
||||
.set_is_inhibited(!self.idle_inhibitors.is_empty());
|
||||
self.refresh_idle_inhibition();
|
||||
}
|
||||
if matches!(&self.cursor_status, smithay::input::pointer::CursorImageStatus::Surface(cursor) if cursor == surface)
|
||||
{
|
||||
|
||||
@@ -126,8 +126,7 @@ impl IdleInhibitHandler for Ferese {
|
||||
self.idle_inhibitors.remove(&surface);
|
||||
}
|
||||
}
|
||||
self.idle_notifier_state
|
||||
.set_is_inhibited(!self.idle_inhibitors.is_empty());
|
||||
self.refresh_idle_inhibition();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+137
-4
@@ -122,10 +122,33 @@ pub(crate) fn init(
|
||||
IpcEvent::Call(call) => call,
|
||||
IpcEvent::Closed(owner) => {
|
||||
state.portal_shortcuts.remove(owner);
|
||||
state.portal_session.remove(owner);
|
||||
state.refresh_idle_inhibition();
|
||||
return;
|
||||
}
|
||||
};
|
||||
if call.request.command == "screenshot-window" {
|
||||
if call.request.command == "session-watch" {
|
||||
if let Err(error) = validate_request(&call.request) {
|
||||
let _ = call.response.send(Response::error(
|
||||
call.request.id,
|
||||
error.code,
|
||||
error.message,
|
||||
));
|
||||
} else if let Some(since) = call.request.args["since"].as_u64() {
|
||||
state.portal_session.watch(
|
||||
call.owner,
|
||||
call.request.id,
|
||||
since,
|
||||
call.response,
|
||||
);
|
||||
} else {
|
||||
let _ = call.response.send(Response::error(
|
||||
call.request.id,
|
||||
"invalid_argument",
|
||||
"Missing session revision",
|
||||
));
|
||||
}
|
||||
} else if call.request.command == "screenshot-window" {
|
||||
state.start_window_screenshot(call.request, call.response);
|
||||
} else if call.request.command == "screenshot" {
|
||||
// Deferred: this path answers the caller itself, exactly
|
||||
@@ -322,8 +345,34 @@ fn serve_connection(
|
||||
tracing::warn!("disconnecting IPC client because the request queue is full");
|
||||
return;
|
||||
}
|
||||
let Ok(response) = receiver.recv() else {
|
||||
return;
|
||||
let response = loop {
|
||||
match receiver.recv_timeout(Duration::from_secs(1)) {
|
||||
Ok(response) => break response,
|
||||
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => return,
|
||||
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
|
||||
let mut byte = 0_u8;
|
||||
// SAFETY: the live stream owns this fd and byte is writable.
|
||||
let result = unsafe {
|
||||
libc::recv(
|
||||
std::os::fd::AsRawFd::as_raw_fd(&stream),
|
||||
(&mut byte as *mut u8).cast(),
|
||||
1,
|
||||
libc::MSG_PEEK | libc::MSG_DONTWAIT,
|
||||
)
|
||||
};
|
||||
if result == 0 {
|
||||
return;
|
||||
}
|
||||
if result < 0
|
||||
&& !matches!(
|
||||
io::Error::last_os_error().kind(),
|
||||
io::ErrorKind::WouldBlock | io::ErrorKind::Interrupted
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let exit_accepted = exit_requested && response.error.is_none();
|
||||
if let Err(error) = write_frame(&mut stream, &response) {
|
||||
@@ -342,6 +391,13 @@ fn serve_connection(
|
||||
}
|
||||
}
|
||||
|
||||
fn u32_arg(args: &Value, key: &str) -> Result<u32, CommandError> {
|
||||
args[key]
|
||||
.as_u64()
|
||||
.and_then(|value| u32::try_from(value).ok())
|
||||
.ok_or_else(|| CommandError::new("invalid_argument", format!("Missing or invalid {key}")))
|
||||
}
|
||||
|
||||
fn discard_undelivered_screenshot(response: &Response) {
|
||||
if response.error.is_none()
|
||||
&& let Some(path) = response
|
||||
@@ -357,7 +413,18 @@ fn discard_undelivered_screenshot(response: &Response) {
|
||||
|
||||
impl Ferese {
|
||||
fn handle_ipc_request(&mut self, owner: u64, request: Request) -> Response {
|
||||
if self.session_lock.active && request.command != "portal-shortcuts-poll" {
|
||||
if self.session_lock.active
|
||||
&& !matches!(
|
||||
request.command.as_str(),
|
||||
"portal-shortcuts-poll"
|
||||
| "get-session-state"
|
||||
| "portal-inhibit"
|
||||
| "portal-monitor-register"
|
||||
| "portal-monitor-ack"
|
||||
| "logind-session-ending"
|
||||
| "cancel-session-end"
|
||||
)
|
||||
{
|
||||
return Response::error(
|
||||
request.id,
|
||||
"session_locked",
|
||||
@@ -381,6 +448,72 @@ impl Ferese {
|
||||
args: &Value,
|
||||
) -> Result<Value, CommandError> {
|
||||
match command {
|
||||
"portal-inhibit" => {
|
||||
let inhibition = serde_json::from_value(args.clone())
|
||||
.map_err(|error| CommandError::new("invalid_argument", error.to_string()))?;
|
||||
self.portal_session
|
||||
.register(owner, inhibition)
|
||||
.map_err(|error| CommandError::new("invalid_argument", error))?;
|
||||
self.refresh_idle_inhibition();
|
||||
return Ok(json!({}));
|
||||
}
|
||||
"get-session-state" => {
|
||||
return Ok(self.portal_session.snapshot());
|
||||
}
|
||||
"portal-monitor-register" => {
|
||||
self.portal_session
|
||||
.register_monitor(owner)
|
||||
.map_err(|error| CommandError::new("invalid_argument", error))?;
|
||||
let mut result = self.portal_session.snapshot();
|
||||
result["monitor-owner"] = json!(owner);
|
||||
return Ok(result);
|
||||
}
|
||||
"portal-monitor-ack" => {
|
||||
let token = u32_arg(args, "token")?;
|
||||
self.portal_session
|
||||
.acknowledge(owner, token)
|
||||
.map_err(|error| CommandError::new("invalid_argument", error))?;
|
||||
return Ok(json!({}));
|
||||
}
|
||||
"begin-session-end" => {
|
||||
self.portal_session.begin_owned_query(owner);
|
||||
return Ok(self.portal_session.snapshot());
|
||||
}
|
||||
"cancel-session-end" => {
|
||||
self.portal_session.cancel_query(u32_arg(args, "token")?);
|
||||
return Ok(json!({}));
|
||||
}
|
||||
"validate-session-end" => {
|
||||
self.portal_session
|
||||
.validate_end(
|
||||
u32_arg(args, "token")?,
|
||||
u32_arg(args, "inhibitor-revision")?,
|
||||
args["force"].as_bool().ok_or_else(|| {
|
||||
CommandError::new("invalid_argument", "Missing force confirmation")
|
||||
})?,
|
||||
)
|
||||
.map_err(|error| CommandError::new("inhibited", error))?;
|
||||
return Ok(json!({}));
|
||||
}
|
||||
"commit-session-end" => {
|
||||
self.portal_session
|
||||
.commit_end(
|
||||
u32_arg(args, "token")?,
|
||||
u32_arg(args, "inhibitor-revision")?,
|
||||
args["force"].as_bool().ok_or_else(|| {
|
||||
CommandError::new("invalid_argument", "Missing force confirmation")
|
||||
})?,
|
||||
)
|
||||
.map_err(|error| CommandError::new("inhibited", error))?;
|
||||
return Ok(json!({}));
|
||||
}
|
||||
"logind-session-ending" => {
|
||||
let ending = args["ending"].as_bool().ok_or_else(|| {
|
||||
CommandError::new("invalid_argument", "Missing shutdown state")
|
||||
})?;
|
||||
self.portal_session.set_phase(if ending { 3 } else { 1 });
|
||||
return Ok(json!({}));
|
||||
}
|
||||
"portal-shortcuts-register" => {
|
||||
let shortcuts = serde_json::from_value(args.clone())
|
||||
.map_err(|error| CommandError::new("invalid_argument", error.to_string()))?;
|
||||
|
||||
@@ -13,6 +13,7 @@ mod input;
|
||||
mod ipc;
|
||||
mod metrics;
|
||||
mod overview;
|
||||
mod portal_session;
|
||||
mod portal_shortcuts;
|
||||
mod presentation;
|
||||
mod private_client;
|
||||
|
||||
@@ -0,0 +1,422 @@
|
||||
use ferese_ipc::Response;
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Value, json};
|
||||
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet, VecDeque},
|
||||
sync::mpsc::SyncSender,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub(crate) struct Inhibition {
|
||||
flags: u32,
|
||||
app: String,
|
||||
reason: String,
|
||||
}
|
||||
|
||||
struct Query {
|
||||
token: u32,
|
||||
owner: Option<u64>,
|
||||
deadline: Instant,
|
||||
pending: HashSet<u64>,
|
||||
}
|
||||
|
||||
pub(crate) struct PortalSession {
|
||||
inhibitors: HashMap<u64, Inhibition>,
|
||||
monitors: HashSet<u64>,
|
||||
waiters: HashMap<u64, (u64, SyncSender<Response>)>,
|
||||
phase: u32,
|
||||
locked: bool,
|
||||
revision: u64,
|
||||
inhibitor_revision: u32,
|
||||
next_query: u32,
|
||||
query: Option<Query>,
|
||||
transitions: VecDeque<Value>,
|
||||
}
|
||||
|
||||
impl Default for PortalSession {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
inhibitors: HashMap::new(),
|
||||
monitors: HashSet::new(),
|
||||
waiters: HashMap::new(),
|
||||
phase: 1,
|
||||
locked: false,
|
||||
revision: 0,
|
||||
inhibitor_revision: 0,
|
||||
next_query: 0,
|
||||
query: None,
|
||||
transitions: VecDeque::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PortalSession {
|
||||
pub(crate) fn register(&mut self, owner: u64, inhibition: Inhibition) -> Result<(), String> {
|
||||
if inhibition.flags == 0
|
||||
|| inhibition.flags & !15 != 0
|
||||
|| inhibition.app.len() > 512
|
||||
|| inhibition.reason.len() > 1024
|
||||
|| inhibition
|
||||
.app
|
||||
.chars()
|
||||
.chain(inhibition.reason.chars())
|
||||
.any(char::is_control)
|
||||
{
|
||||
return Err("Invalid session inhibition".into());
|
||||
}
|
||||
if self.inhibitors.len() >= 16 && !self.inhibitors.contains_key(&owner) {
|
||||
return Err("Session inhibition limit reached".into());
|
||||
}
|
||||
self.inhibitors.insert(owner, inhibition);
|
||||
self.inhibitor_revision = self.inhibitor_revision.wrapping_add(1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn register_monitor(&mut self, owner: u64) -> Result<(), String> {
|
||||
if self.monitors.len() >= 8 && !self.monitors.contains(&owner) {
|
||||
return Err("Session monitor limit reached".into());
|
||||
}
|
||||
self.monitors.insert(owner);
|
||||
if let Some(query) = &mut self.query
|
||||
&& self.phase == 2
|
||||
{
|
||||
query.pending.insert(owner);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn acknowledge(&mut self, owner: u64, token: u32) -> Result<(), String> {
|
||||
if !self.monitors.contains(&owner) {
|
||||
return Err("Unknown session monitor".into());
|
||||
}
|
||||
if let Some(query) = &mut self.query
|
||||
&& query.token == token
|
||||
&& self.phase == 2
|
||||
&& query.pending.remove(&owner)
|
||||
{
|
||||
self.changed();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn remove(&mut self, owner: u64) {
|
||||
self.waiters.remove(&owner);
|
||||
if self.phase == 2
|
||||
&& self
|
||||
.query
|
||||
.as_ref()
|
||||
.is_some_and(|query| query.owner == Some(owner))
|
||||
{
|
||||
self.set_phase(1);
|
||||
}
|
||||
if self.inhibitors.remove(&owner).is_some() {
|
||||
self.inhibitor_revision = self.inhibitor_revision.wrapping_add(1);
|
||||
}
|
||||
let monitor_removed = self.monitors.remove(&owner);
|
||||
let acknowledged = self
|
||||
.query
|
||||
.as_mut()
|
||||
.is_some_and(|query| query.pending.remove(&owner));
|
||||
if monitor_removed || acknowledged {
|
||||
self.changed();
|
||||
}
|
||||
}
|
||||
|
||||
fn idle_inhibited(&self) -> bool {
|
||||
self.inhibitors
|
||||
.values()
|
||||
.any(|inhibitor| inhibitor.flags & 8 != 0)
|
||||
}
|
||||
|
||||
pub(crate) fn begin_query(&mut self) -> u32 {
|
||||
self.next_query = self.next_query.wrapping_add(1).max(1);
|
||||
let token = self.next_query;
|
||||
self.query = Some(Query {
|
||||
token,
|
||||
owner: None,
|
||||
deadline: Instant::now() + Duration::from_secs(1),
|
||||
pending: self.monitors.clone(),
|
||||
});
|
||||
self.phase = 2;
|
||||
self.changed();
|
||||
token
|
||||
}
|
||||
|
||||
pub(crate) fn begin_owned_query(&mut self, owner: u64) -> u32 {
|
||||
let token = self.begin_query();
|
||||
self.query.as_mut().unwrap().owner = Some(owner);
|
||||
token
|
||||
}
|
||||
|
||||
pub(crate) fn cancel_query(&mut self, token: u32) {
|
||||
if self
|
||||
.query
|
||||
.as_ref()
|
||||
.is_some_and(|query| query.token == token)
|
||||
{
|
||||
self.set_phase(1);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn query_ready(&self) -> bool {
|
||||
self.phase == 2
|
||||
&& self
|
||||
.query
|
||||
.as_ref()
|
||||
.is_some_and(|query| query.pending.is_empty() || Instant::now() >= query.deadline)
|
||||
}
|
||||
|
||||
pub(crate) fn validate_end(
|
||||
&self,
|
||||
token: u32,
|
||||
inhibitor_revision: u32,
|
||||
force: bool,
|
||||
) -> Result<(), String> {
|
||||
if !self.query_ready()
|
||||
|| !self
|
||||
.query
|
||||
.as_ref()
|
||||
.is_some_and(|query| query.token == token)
|
||||
{
|
||||
return Err(
|
||||
"Session-ending confirmation expired or applications are still responding".into(),
|
||||
);
|
||||
}
|
||||
if self.inhibitor_revision != inhibitor_revision
|
||||
|| (!force
|
||||
&& self
|
||||
.inhibitors
|
||||
.values()
|
||||
.any(|inhibitor| inhibitor.flags & 1 != 0))
|
||||
{
|
||||
return Err(
|
||||
"Applications changed their inhibitors; review the confirmation again".into(),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn commit_end(
|
||||
&mut self,
|
||||
token: u32,
|
||||
inhibitor_revision: u32,
|
||||
force: bool,
|
||||
) -> Result<(), String> {
|
||||
self.validate_end(token, inhibitor_revision, force)?;
|
||||
self.set_phase(3);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn set_phase(&mut self, phase: u32) {
|
||||
if phase == 2 {
|
||||
self.begin_query();
|
||||
return;
|
||||
}
|
||||
if phase == 1 {
|
||||
self.query = None;
|
||||
}
|
||||
if self.phase != phase {
|
||||
self.phase = phase;
|
||||
self.changed();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_locked(&mut self, locked: bool) {
|
||||
if self.locked != locked {
|
||||
self.locked = locked;
|
||||
self.changed();
|
||||
}
|
||||
}
|
||||
|
||||
fn transition(&self) -> Value {
|
||||
json!({"screensaver-active": self.locked, "session-state": self.phase, "revision": self.revision, "query-token": self.query.as_ref().map_or(0, |query| query.token)})
|
||||
}
|
||||
|
||||
fn changed(&mut self) {
|
||||
self.revision = self.revision.wrapping_add(1);
|
||||
if self.transitions.len() == 64 {
|
||||
self.transitions.pop_front();
|
||||
}
|
||||
self.transitions.push_back(self.transition());
|
||||
let snapshot = self.snapshot();
|
||||
for (_, (id, response)) in self.waiters.drain() {
|
||||
let _ = response.try_send(Response::success(id, snapshot.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn watch(
|
||||
&mut self,
|
||||
owner: u64,
|
||||
id: u64,
|
||||
since: u64,
|
||||
response: SyncSender<Response>,
|
||||
) {
|
||||
if since != self.revision {
|
||||
let _ = response.try_send(Response::success(id, self.snapshot()));
|
||||
} else {
|
||||
self.waiters.insert(owner, (id, response));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn snapshot(&self) -> Value {
|
||||
let inhibitors = self.inhibitors.values().map(|inhibitor| json!({"flags": inhibitor.flags, "app": inhibitor.app, "reason": inhibitor.reason})).collect::<Vec<_>>();
|
||||
let remaining = self.query.as_ref().map_or(0, |query| {
|
||||
query
|
||||
.deadline
|
||||
.saturating_duration_since(Instant::now())
|
||||
.as_millis() as u64
|
||||
});
|
||||
json!({"screensaver-active": self.locked, "session-state": self.phase, "revision": self.revision,
|
||||
"inhibitors": inhibitors, "inhibitor-revision": self.inhibitor_revision,
|
||||
"query-token": self.query.as_ref().map_or(0, |query| query.token), "query-ready": self.query_ready(),
|
||||
"query-remaining-ms": remaining, "transitions": self.transitions, "monitor-owners": self.monitors})
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::Ferese {
|
||||
pub(crate) fn refresh_idle_inhibition(&mut self) {
|
||||
self.idle_notifier_state.set_is_inhibited(
|
||||
!self.idle_inhibitors.is_empty() || self.portal_session.idle_inhibited(),
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn end_portal_session(&mut self) {
|
||||
let token = self.portal_session.query.as_ref().map(|query| query.token);
|
||||
let result = self.loop_handle.insert_source(
|
||||
Timer::from_duration(Duration::from_millis(200)),
|
||||
move |_, _, state| {
|
||||
if state.portal_session.phase == 3
|
||||
&& state.portal_session.query.as_ref().map(|query| query.token) == token
|
||||
{
|
||||
state.loop_signal.stop();
|
||||
}
|
||||
TimeoutAction::Drop
|
||||
},
|
||||
);
|
||||
if let Err(error) = result {
|
||||
tracing::error!(%error, "failed to schedule session ending");
|
||||
self.loop_signal.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn inhibition(flags: u32) -> Inhibition {
|
||||
Inhibition {
|
||||
flags,
|
||||
app: "test.app".into(),
|
||||
reason: "Playing media".into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inhibition_is_connection_scoped_and_invalid_flags_do_not_replace_it() {
|
||||
let mut session = PortalSession::default();
|
||||
session.register(1, inhibition(8)).unwrap();
|
||||
session.register(2, inhibition(8)).unwrap();
|
||||
assert!(session.register(1, inhibition(16)).is_err());
|
||||
session.remove(1);
|
||||
assert!(session.idle_inhibited());
|
||||
session.remove(2);
|
||||
assert!(!session.idle_inhibited());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acknowledgements_are_generation_scoped_and_disconnect_releases_pending_monitor() {
|
||||
let mut session = PortalSession::default();
|
||||
session.register_monitor(1).unwrap();
|
||||
session.register_monitor(2).unwrap();
|
||||
let old = session.begin_query();
|
||||
assert!(!session.query_ready());
|
||||
let token = session.begin_query();
|
||||
session.acknowledge(1, old).unwrap();
|
||||
assert!(!session.query_ready());
|
||||
session.acknowledge(1, token).unwrap();
|
||||
assert!(!session.query_ready());
|
||||
session.remove(2);
|
||||
assert!(session.query_ready());
|
||||
session.cancel_query(old);
|
||||
assert!(session.query_ready());
|
||||
session.cancel_query(token);
|
||||
assert!(!session.query_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn late_inhibitor_requires_a_new_confirmation_and_force_is_explicit() {
|
||||
let mut session = PortalSession::default();
|
||||
let token = session.begin_query();
|
||||
let approved = session.inhibitor_revision;
|
||||
session.register(1, inhibition(1)).unwrap();
|
||||
assert!(session.commit_end(token, approved, true).is_err());
|
||||
assert!(
|
||||
session
|
||||
.commit_end(token, session.inhibitor_revision, false)
|
||||
.is_err()
|
||||
);
|
||||
session
|
||||
.commit_end(token, session.inhibitor_revision, true)
|
||||
.unwrap();
|
||||
assert_eq!(session.snapshot()["session-state"], 3);
|
||||
assert!(
|
||||
session
|
||||
.commit_end(token, session.inhibitor_revision, true)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_keeps_query_open_and_owner_disconnect_cancels_it() {
|
||||
let mut session = PortalSession::default();
|
||||
let token = session.begin_owned_query(9);
|
||||
session.validate_end(token, 0, false).unwrap();
|
||||
assert_eq!(session.snapshot()["session-state"], 2);
|
||||
session.remove(9);
|
||||
assert_eq!(session.snapshot()["session-state"], 1);
|
||||
assert!(session.validate_end(token, 0, false).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn old_logout_token_does_not_cancel_a_new_shutdown_query() {
|
||||
let mut session = PortalSession::default();
|
||||
let logout = session.begin_query();
|
||||
let shutdown = session.begin_owned_query(9);
|
||||
session.cancel_query(logout);
|
||||
session.validate_end(shutdown, 0, false).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unresponsive_monitors_have_a_one_second_deadline() {
|
||||
let mut session = PortalSession::default();
|
||||
session.register_monitor(1).unwrap();
|
||||
session.begin_query();
|
||||
assert!(!session.query_ready());
|
||||
session.query.as_mut().unwrap().deadline = Instant::now() - Duration::from_millis(1);
|
||||
assert!(session.query_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_watch_preserves_fast_transitions_and_stops_on_disconnect() {
|
||||
let mut session = PortalSession::default();
|
||||
let (sender, receiver) = std::sync::mpsc::sync_channel(1);
|
||||
session.watch(1, 10, 0, sender);
|
||||
assert!(receiver.try_recv().is_err());
|
||||
session.set_locked(true);
|
||||
let response = receiver.try_recv().unwrap();
|
||||
assert_eq!(response.result.unwrap()["screensaver-active"], true);
|
||||
session.set_locked(false);
|
||||
assert_eq!(
|
||||
session.snapshot()["transitions"].as_array().unwrap().len(),
|
||||
2
|
||||
);
|
||||
let (sender, receiver) = std::sync::mpsc::sync_channel(1);
|
||||
session.watch(1, 11, session.revision, sender);
|
||||
session.remove(1);
|
||||
assert!(receiver.recv().is_err());
|
||||
}
|
||||
}
|
||||
@@ -123,6 +123,7 @@ impl SessionLockHandler for Ferese {
|
||||
}
|
||||
self.cancel_logout_confirmation();
|
||||
self.session_lock.active = true;
|
||||
self.portal_session.set_locked(true);
|
||||
self.session_lock.owner = Some(confirmation.ext_session_lock().clone());
|
||||
self.session_lock.confirmation = Some(confirmation);
|
||||
for capture in self.pending_screencopies.drain(..) {
|
||||
@@ -165,6 +166,7 @@ impl SessionLockHandler for Ferese {
|
||||
|
||||
fn unlock(&mut self) {
|
||||
self.session_lock = Lock::default();
|
||||
self.portal_session.set_locked(false);
|
||||
self.restore_keyboard_focus();
|
||||
crate::backends::direct::render_all(self);
|
||||
tracing::info!("session unlocked by lock owner");
|
||||
|
||||
@@ -14,7 +14,7 @@ use smithay::{
|
||||
use crate::{Ferese, private_client::ClientCapabilities, state::ClientState};
|
||||
|
||||
pub(crate) fn init_global(display: &DisplayHandle) {
|
||||
display.create_global::<Ferese, FereseShellManagerV1, _>(3, ());
|
||||
display.create_global::<Ferese, FereseShellManagerV1, _>(4, ());
|
||||
}
|
||||
|
||||
pub(crate) fn config_chunks(source: &str) -> Vec<&str> {
|
||||
@@ -137,11 +137,25 @@ impl Dispatch<FereseShellV1, ()> for Ferese {
|
||||
}
|
||||
}
|
||||
ferese_shell_v1::Request::ConfirmLogout { serial } => {
|
||||
if state.logout_owner.as_ref() == Some(&shell.id())
|
||||
&& consume_logout_confirmation(&mut state.pending_logout, serial)
|
||||
{
|
||||
state.logout_owner = None;
|
||||
state.loop_signal.stop();
|
||||
let snapshot = state.portal_session.snapshot();
|
||||
let token = snapshot["query-token"].as_u64().unwrap_or(0) as u32;
|
||||
let revision = snapshot["inhibitor-revision"].as_u64().unwrap_or(0) as u32;
|
||||
state.confirm_portal_logout(shell, serial, token, revision, false);
|
||||
}
|
||||
ferese_shell_v1::Request::ConfirmLogoutWithInhibitors {
|
||||
serial,
|
||||
inhibitor_revision,
|
||||
query_token,
|
||||
force,
|
||||
} => {
|
||||
if force <= 1 {
|
||||
state.confirm_portal_logout(
|
||||
shell,
|
||||
serial,
|
||||
query_token,
|
||||
inhibitor_revision,
|
||||
force == 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
ferese_shell_v1::Request::CancelLogout { serial } => {
|
||||
@@ -183,12 +197,20 @@ impl Dispatch<FereseShellV1, ()> for Ferese {
|
||||
_data: &(),
|
||||
) {
|
||||
if state.logout_owner.as_ref() == Some(&shell.id()) {
|
||||
state.pending_logout = None;
|
||||
state.logout_owner = None;
|
||||
state.cancel_logout_confirmation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn logout_confirmation_matches(
|
||||
pending: Option<u32>,
|
||||
query: Option<u32>,
|
||||
serial: u32,
|
||||
token: u32,
|
||||
) -> bool {
|
||||
pending == Some(serial) && query == Some(token)
|
||||
}
|
||||
|
||||
fn consume_logout_confirmation(pending: &mut Option<u32>, serial: u32) -> bool {
|
||||
if *pending == Some(serial) {
|
||||
*pending = None;
|
||||
@@ -199,6 +221,34 @@ fn consume_logout_confirmation(pending: &mut Option<u32>, serial: u32) -> bool {
|
||||
}
|
||||
|
||||
impl Ferese {
|
||||
fn confirm_portal_logout(
|
||||
&mut self,
|
||||
shell: &FereseShellV1,
|
||||
serial: u32,
|
||||
token: u32,
|
||||
revision: u32,
|
||||
force: bool,
|
||||
) {
|
||||
if self.logout_owner.as_ref() != Some(&shell.id())
|
||||
|| !logout_confirmation_matches(self.pending_logout, self.logout_query, serial, token)
|
||||
{
|
||||
return;
|
||||
}
|
||||
match self.portal_session.commit_end(token, revision, force) {
|
||||
Ok(()) => {
|
||||
consume_logout_confirmation(&mut self.pending_logout, serial);
|
||||
self.logout_owner = None;
|
||||
self.logout_query = None;
|
||||
self.end_portal_session();
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::debug!(%error, "logout requires a new confirmation");
|
||||
self.cancel_logout_confirmation();
|
||||
self.request_logout_confirmation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn request_logout_confirmation(&mut self) {
|
||||
if self.session_lock.active {
|
||||
return;
|
||||
@@ -216,6 +266,7 @@ impl Ferese {
|
||||
self.cancel_logout_confirmation();
|
||||
let serial = u32::from(smithay::utils::SERIAL_COUNTER.next_serial());
|
||||
let shell = &shells[0];
|
||||
self.logout_query = Some(self.portal_session.begin_query());
|
||||
self.pending_logout = Some(serial);
|
||||
self.logout_owner = Some(shell.id());
|
||||
let output_name = self
|
||||
@@ -225,6 +276,9 @@ impl Ferese {
|
||||
}
|
||||
|
||||
pub(crate) fn cancel_logout_confirmation(&mut self) {
|
||||
if let Some(token) = self.logout_query.take() {
|
||||
self.portal_session.cancel_query(token);
|
||||
}
|
||||
if let Some(serial) = self.pending_logout.take() {
|
||||
for shell in self
|
||||
.shell_resources
|
||||
@@ -507,6 +561,18 @@ fn send_request_failed(shell: &FereseShellV1, request: ferese_shell_v1::FailedRe
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn logout_serial_cannot_authorize_a_different_session_query() {
|
||||
assert!(super::logout_confirmation_matches(Some(42), Some(9), 42, 9));
|
||||
assert!(!super::logout_confirmation_matches(
|
||||
Some(42),
|
||||
Some(9),
|
||||
42,
|
||||
10
|
||||
));
|
||||
assert!(!super::logout_confirmation_matches(None, Some(9), 42, 9));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logout_requires_the_current_confirmation_and_can_be_cancelled() {
|
||||
let mut pending = Some(42);
|
||||
|
||||
@@ -231,6 +231,8 @@ pub struct Ferese {
|
||||
pub socket_name: OsString,
|
||||
pub display_handle: DisplayHandle,
|
||||
pub loop_signal: LoopSignal,
|
||||
pub(crate) loop_handle: smithay::reexports::calloop::LoopHandle<'static, Self>,
|
||||
pub(crate) portal_session: crate::portal_session::PortalSession,
|
||||
pub space: Space<Window>,
|
||||
pub workspaces: WorkspaceSet,
|
||||
pub output_workspaces: OutputWorkspaceMap,
|
||||
@@ -259,6 +261,7 @@ pub struct Ferese {
|
||||
pub(crate) material_buffers: HashMap<WlSurface, crate::winit::MaterialBuffers>,
|
||||
pub(crate) portal_shortcuts: crate::portal_shortcuts::PortalShortcuts,
|
||||
pub(crate) pending_logout: Option<u32>,
|
||||
pub(crate) logout_query: Option<u32>,
|
||||
pub(crate) logout_owner: Option<smithay::reexports::wayland_server::backend::ObjectId>,
|
||||
pub(crate) blur_programs: HashMap<ErasedContextId, crate::winit::BlurProgram>,
|
||||
pub(crate) backdrop_generation: u64,
|
||||
@@ -448,6 +451,8 @@ impl Ferese {
|
||||
socket_name,
|
||||
display_handle,
|
||||
loop_signal: event_loop.get_signal(),
|
||||
loop_handle: event_loop.handle(),
|
||||
portal_session: Default::default(),
|
||||
space: Space::default(),
|
||||
workspaces: WorkspaceSet::new(
|
||||
config.layout_mode,
|
||||
@@ -483,6 +488,7 @@ impl Ferese {
|
||||
material_buffers: HashMap::new(),
|
||||
portal_shortcuts: crate::portal_shortcuts::PortalShortcuts::default(),
|
||||
pending_logout: None,
|
||||
logout_query: None,
|
||||
logout_owner: None,
|
||||
blur_programs: HashMap::new(),
|
||||
backdrop_generation: 0,
|
||||
|
||||
Reference in new issue
Block a user