fix compositor edge cases, process handing and other cool improvements
This commit is contained in:
1 parent
5706a71941
commit
405c74af9f
38 files changed
+3391
-875
No files matched your search
@@ -1,4 +1,9 @@
|
||||
use std::time::Duration;
|
||||
use std::collections::VecDeque;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use smithay::input::Seat;
|
||||
use smithay::reexports::wayland_server::{Resource, backend::ClientId};
|
||||
use smithay::utils::Serial;
|
||||
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::SERIAL_COUNTER;
|
||||
@@ -9,6 +14,50 @@ use smithay::wayland::xdg_activation::{
|
||||
use crate::Ferese;
|
||||
|
||||
const TOKEN_MAX_AGE: Duration = Duration::from_secs(10);
|
||||
const MAX_INTERACTIONS: usize = 64;
|
||||
|
||||
pub(crate) struct InputHistory<C = ClientId> {
|
||||
events: VecDeque<(Serial, C, Instant)>,
|
||||
}
|
||||
|
||||
impl<C> Default for InputHistory<C> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
events: VecDeque::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: PartialEq> InputHistory<C> {
|
||||
fn record(&mut self, serial: Serial, client: C, time: Instant) {
|
||||
while self.events.len() >= MAX_INTERACTIONS {
|
||||
self.events.pop_front();
|
||||
}
|
||||
|
||||
self.events.push_back((serial, client, time));
|
||||
}
|
||||
|
||||
fn validate(&self, serial: Serial, client: &C, now: Instant) -> Option<Instant> {
|
||||
self.events.iter().rev().find_map(|(event, owner, time)| {
|
||||
(*event == serial && owner == client && now.saturating_duration_since(*time) <= TOKEN_MAX_AGE)
|
||||
.then_some(*time)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct ApprovedInteraction(Instant);
|
||||
|
||||
impl Ferese {
|
||||
pub(crate) fn record_activation_input(&mut self, serial: Serial, surface: Option<WlSurface>) {
|
||||
if self.session_lock.active() || self.input_capture.active() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(client) = surface.and_then(|surface| surface.client()) {
|
||||
self.activation_inputs.record(serial, client.id(), Instant::now());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl XdgActivationHandler for Ferese {
|
||||
fn activation_state(&mut self) -> &mut XdgActivationState {
|
||||
@@ -16,15 +65,46 @@ impl XdgActivationHandler for Ferese {
|
||||
}
|
||||
|
||||
fn token_created(&mut self, _token: XdgActivationToken, data: XdgActivationTokenData) -> bool {
|
||||
data.serial.is_some() && data.timestamp.elapsed() <= TOKEN_MAX_AGE
|
||||
if self.session_lock.active() || self.input_capture.active() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some((serial, seat)) = &data.serial else {
|
||||
return false;
|
||||
};
|
||||
let Some(client) = &data.client_id else {
|
||||
return false;
|
||||
};
|
||||
if Seat::<Self>::from_resource(seat).as_ref() != Some(&self.seat)
|
||||
|| seat.client().is_none_or(|owner| owner.id() != *client)
|
||||
|| data
|
||||
.surface
|
||||
.as_ref()
|
||||
.is_some_and(|surface| surface.client().is_none_or(|owner| owner.id() != *client))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some(time) = self.activation_inputs.validate(*serial, client, Instant::now()) else {
|
||||
return false;
|
||||
};
|
||||
data.user_data.insert_if_missing(|| ApprovedInteraction(time));
|
||||
true
|
||||
}
|
||||
|
||||
fn request_activation(&mut self, token: XdgActivationToken, data: XdgActivationTokenData, surface: WlSurface) {
|
||||
self.xdg_activation_state.remove_token(&token);
|
||||
if self.session_lock.active() || self.input_capture.captures(1) {
|
||||
return;
|
||||
}
|
||||
let valid = data.serial.is_some() && data.timestamp.elapsed() <= TOKEN_MAX_AGE;
|
||||
self.xdg_activation_state.remove_token(&token);
|
||||
|
||||
// The recipient may be a different client: launcher-to-app token handoff.
|
||||
// Retain the original interaction time rather than renewing it at commit.
|
||||
let valid = data.timestamp.elapsed() <= TOKEN_MAX_AGE
|
||||
&& data
|
||||
.user_data
|
||||
.get::<ApprovedInteraction>()
|
||||
.is_some_and(|proof| proof.0.elapsed() <= TOKEN_MAX_AGE);
|
||||
if !valid {
|
||||
tracing::debug!("ignored stale or non-interactive activation request");
|
||||
return;
|
||||
@@ -60,3 +140,163 @@ impl XdgActivationHandler for Ferese {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn token_creation_validates_real_seat_owner_and_input_age() {
|
||||
use smithay::reexports::calloop::EventLoop;
|
||||
use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat;
|
||||
use std::io::{Read, Write};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::sync::Arc;
|
||||
|
||||
if !crate::startup_tests::private_runtime(
|
||||
"handlers::activation::tests::token_creation_validates_real_seat_owner_and_input_age",
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut events = EventLoop::try_new().unwrap();
|
||||
let mut state = crate::startup_tests::state(&mut events);
|
||||
let (server, mut wire) = UnixStream::pair().unwrap();
|
||||
let launcher = state
|
||||
.display_handle
|
||||
.insert_client(server, Arc::new(crate::state::ClientState::default()))
|
||||
.unwrap();
|
||||
let send = |wire: &mut UnixStream, object: u32, opcode: u32, args: &[u32]| {
|
||||
let bytes: Vec<_> = [object, ((args.len() as u32 + 2) * 4) << 16 | opcode]
|
||||
.into_iter()
|
||||
.chain(args.iter().copied())
|
||||
.flat_map(u32::to_ne_bytes)
|
||||
.collect();
|
||||
wire.write_all(&bytes).unwrap();
|
||||
};
|
||||
send(&mut wire, 1, 1, &[2]); // wl_display.get_registry
|
||||
events.dispatch(Duration::from_millis(1), &mut state).unwrap();
|
||||
state.display_handle.flush_clients().unwrap();
|
||||
wire.set_nonblocking(true).unwrap();
|
||||
let mut globals = Vec::new();
|
||||
let _ = wire.read_to_end(&mut globals);
|
||||
wire.set_nonblocking(false).unwrap();
|
||||
let mut offset = 0;
|
||||
let mut seat_name = None;
|
||||
while offset < globals.len() {
|
||||
let header = u32::from_ne_bytes(globals[offset + 4..offset + 8].try_into().unwrap());
|
||||
if u32::from_ne_bytes(globals[offset..offset + 4].try_into().unwrap()) == 2 && header & 0xffff == 0 {
|
||||
let length = u32::from_ne_bytes(globals[offset + 12..offset + 16].try_into().unwrap()) as usize;
|
||||
if &globals[offset + 16..offset + 16 + length] == b"wl_seat\0" {
|
||||
seat_name = Some(u32::from_ne_bytes(globals[offset + 8..offset + 12].try_into().unwrap()));
|
||||
}
|
||||
}
|
||||
|
||||
offset += (header >> 16) as usize;
|
||||
}
|
||||
|
||||
let mut args = vec![seat_name.expect("seat global"), 8];
|
||||
args.extend(
|
||||
b"wl_seat\0"
|
||||
.chunks_exact(4)
|
||||
.map(|word| u32::from_ne_bytes(word.try_into().unwrap())),
|
||||
);
|
||||
args.extend([7, 3]);
|
||||
send(&mut wire, 2, 0, &args);
|
||||
events.dispatch(Duration::from_millis(1), &mut state).unwrap();
|
||||
let seat: WlSeat = state
|
||||
.seat
|
||||
.client_seats(&launcher)
|
||||
.into_iter()
|
||||
.next()
|
||||
.expect("bound seat");
|
||||
let (token, _) = state.xdg_activation_state.create_external_token(None);
|
||||
let token = token.clone();
|
||||
let serial = SERIAL_COUNTER.next_serial();
|
||||
let data = || XdgActivationTokenData {
|
||||
client_id: Some(launcher.id()),
|
||||
serial: Some((serial, seat.clone())),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
!state.token_created(token.clone(), data()),
|
||||
"fabricated serial accepted"
|
||||
);
|
||||
state.activation_inputs.record(
|
||||
serial,
|
||||
launcher.id(),
|
||||
Instant::now() - TOKEN_MAX_AGE - Duration::from_millis(1),
|
||||
);
|
||||
assert!(
|
||||
!state.token_created(token.clone(), data()),
|
||||
"fresh token renewed stale input"
|
||||
);
|
||||
state.activation_inputs.record(serial, launcher.id(), Instant::now());
|
||||
let approved = data();
|
||||
assert!(state.token_created(token.clone(), approved.clone()));
|
||||
assert!(approved.user_data.get::<ApprovedInteraction>().is_some());
|
||||
|
||||
let (server, mut recipient_wire) = UnixStream::pair().unwrap();
|
||||
let recipient = state
|
||||
.display_handle
|
||||
.insert_client(server, Arc::new(crate::state::ClientState::default()))
|
||||
.unwrap();
|
||||
let mut stolen = data();
|
||||
stolen.client_id = Some(recipient.id());
|
||||
assert!(
|
||||
!state.token_created(token.clone(), stolen),
|
||||
"other client claimed launcher's seat/input"
|
||||
);
|
||||
// The proof is attached to the token, not the recipient identity. This
|
||||
// is what lets a validated launcher hand it to a different application.
|
||||
assert_ne!(approved.client_id, Some(recipient.id()));
|
||||
assert!(approved.user_data.get::<ApprovedInteraction>().unwrap().0.elapsed() < TOKEN_MAX_AGE);
|
||||
|
||||
// Exercise the production handler with a real recipient toplevel.
|
||||
// Creating the window needs no GPU or client buffer.
|
||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_wm_base::XdgWmBase;
|
||||
use smithay::reexports::wayland_server::protocol::wl_compositor::WlCompositor;
|
||||
use smithay::wayland::shell::xdg::XdgWmBaseUserData;
|
||||
let compositor = recipient
|
||||
.create_resource::<WlCompositor, (), Ferese>(&state.display_handle, 6, ())
|
||||
.unwrap();
|
||||
let shell = recipient
|
||||
.create_resource::<XdgWmBase, XdgWmBaseUserData, Ferese>(&state.display_handle, 6, Default::default())
|
||||
.unwrap();
|
||||
send(&mut recipient_wire, compositor.id().protocol_id(), 0, &[2]);
|
||||
send(&mut recipient_wire, shell.id().protocol_id(), 2, &[3, 2]);
|
||||
send(&mut recipient_wire, 3, 1, &[4]);
|
||||
events.dispatch(Duration::from_millis(1), &mut state).unwrap();
|
||||
let target = state
|
||||
.space
|
||||
.elements()
|
||||
.find_map(|window| {
|
||||
let surface = window.toplevel()?.wl_surface();
|
||||
surface
|
||||
.client()
|
||||
.is_some_and(|client| client.id() == recipient.id())
|
||||
.then(|| surface.clone())
|
||||
})
|
||||
.expect("recipient's toplevel");
|
||||
state.request_activation(token, approved, target.clone());
|
||||
assert_eq!(state.seat.get_keyboard().unwrap().current_focus(), Some(target));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validates_owner_serial_and_original_interaction_age() {
|
||||
let now = Instant::now();
|
||||
let mut history = InputHistory::default();
|
||||
let serial = Serial::from(12);
|
||||
history.record(serial, "launcher", now);
|
||||
assert_eq!(history.validate(serial, &"launcher", now), Some(now));
|
||||
assert_eq!(history.validate(Serial::from(13), &"launcher", now), None);
|
||||
assert_eq!(history.validate(serial, &"other-client", now), None);
|
||||
assert_eq!(
|
||||
history.validate(serial, &"launcher", now + TOKEN_MAX_AGE + Duration::from_millis(1)),
|
||||
None
|
||||
);
|
||||
// An approved token retains the interaction time across client handoff.
|
||||
let approved = ApprovedInteraction(history.validate(serial, &"launcher", now).unwrap());
|
||||
assert_eq!(approved.0, now);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ use smithay::reexports::wayland_server::protocol::wl_buffer;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::wayland::buffer::BufferHandler;
|
||||
use smithay::wayland::compositor::{
|
||||
CompositorClientState, CompositorHandler, CompositorState, get_parent, is_sync_subsurface,
|
||||
CompositorClientState, CompositorHandler, CompositorState, get_parent, get_role, is_sync_subsurface,
|
||||
};
|
||||
use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier};
|
||||
use smithay::wayland::shm::{ShmHandler, ShmState};
|
||||
@@ -35,6 +35,14 @@ impl CompositorHandler for Ferese {
|
||||
}
|
||||
|
||||
fn commit(&mut self, surface: &WlSurface) {
|
||||
if get_role(surface) == Some(smithay::wayland::shell::xdg::XDG_TOPLEVEL_ROLE)
|
||||
&& let Some(window) = self.window_for_surface(surface)
|
||||
&& let Some(toplevel) = window.toplevel()
|
||||
&& !xdg_shell::validate_size_constraints(toplevel)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
self.update_surface_preferences(surface);
|
||||
self.capture_resize_before_commit(surface);
|
||||
self.capture_close_before_commit(surface);
|
||||
@@ -45,12 +53,7 @@ impl CompositorHandler for Ferese {
|
||||
while let Some(parent) = get_parent(&root) {
|
||||
root = parent;
|
||||
}
|
||||
let window = {
|
||||
self.space
|
||||
.elements()
|
||||
.find(|window| window.toplevel().is_some_and(|toplevel| toplevel.wl_surface() == &root))
|
||||
.cloned()
|
||||
};
|
||||
let window = self.window_for_surface(&root);
|
||||
|
||||
if let Some(window) = window {
|
||||
window.on_commit();
|
||||
@@ -76,7 +79,7 @@ impl CompositorHandler for Ferese {
|
||||
self.refresh_idle_inhibition();
|
||||
|
||||
layer_shell::handle_commit(self, surface);
|
||||
xdg_shell::handle_commit(&mut self.popups, &mut self.space, surface);
|
||||
xdg_shell::handle_commit(self, surface);
|
||||
crate::backends::direct::render_surface(self, surface);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
mod activation;
|
||||
pub(crate) mod activation;
|
||||
mod compositor;
|
||||
mod input_method;
|
||||
mod layer_shell;
|
||||
@@ -60,6 +60,12 @@ impl SeatHandler for Ferese {
|
||||
}
|
||||
|
||||
fn focus_changed(&mut self, seat: &Seat<Self>, focused: Option<&WlSurface>) {
|
||||
// Smithay has already sent enter. last_enter uses a separate mutex;
|
||||
// querying current_focus here would reenter the held keyboard lock.
|
||||
if let Some(serial) = seat.get_keyboard().and_then(|keyboard| keyboard.last_enter()) {
|
||||
self.record_activation_input(serial, focused.cloned());
|
||||
}
|
||||
|
||||
if let Some(id) = focused.and_then(|surface| self.windows.id_for_surface(surface)) {
|
||||
self.focus_history.record(id);
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ pub(crate) enum CaptureSink {
|
||||
part: usize,
|
||||
complete: PartSender,
|
||||
published: AtomicBool,
|
||||
permit: Option<super::screenshot::BudgetPermit>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -59,6 +60,7 @@ impl CaptureSink {
|
||||
part,
|
||||
complete,
|
||||
published: AtomicBool::new(false),
|
||||
permit: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,12 +75,14 @@ impl CaptureSink {
|
||||
part,
|
||||
complete,
|
||||
published,
|
||||
permit,
|
||||
} => {
|
||||
if !published.swap(true, Ordering::AcqRel) {
|
||||
let _ = complete.send(PartOutcome {
|
||||
request: *request,
|
||||
part: *part,
|
||||
result: Err("Screenshot part failed".to_string()),
|
||||
permit: permit.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -93,6 +97,7 @@ impl CaptureSink {
|
||||
part,
|
||||
complete,
|
||||
published,
|
||||
permit,
|
||||
} = self
|
||||
else {
|
||||
return false;
|
||||
@@ -105,12 +110,20 @@ impl CaptureSink {
|
||||
request: *request,
|
||||
part: *part,
|
||||
result: Ok(buffer),
|
||||
permit: permit.clone(),
|
||||
})
|
||||
.is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl PendingScreencopy {
|
||||
pub(crate) fn with_permit(mut self, permit: Option<super::screenshot::BudgetPermit>) -> Self {
|
||||
if let CaptureSink::Owned { permit: stored, .. } = &mut self.sink {
|
||||
*stored = permit;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn owned(
|
||||
request: u64,
|
||||
part: usize,
|
||||
@@ -333,6 +346,19 @@ impl Ferese {
|
||||
.iter()
|
||||
.any(|capture| capture.output == *output && cursor_overlay_matches(capture.overlay_cursor, overlay_cursor))
|
||||
}
|
||||
|
||||
pub(crate) fn fail_screencopies(&mut self, output: &Output, overlay_cursor: bool) {
|
||||
let mut remaining = Vec::new();
|
||||
for capture in std::mem::take(&mut self.pending_screencopies) {
|
||||
if capture.output == *output && cursor_overlay_matches(capture.overlay_cursor, overlay_cursor) {
|
||||
capture.sink.fail();
|
||||
} else {
|
||||
remaining.push(capture);
|
||||
}
|
||||
}
|
||||
|
||||
self.pending_screencopies = remaining;
|
||||
}
|
||||
}
|
||||
|
||||
fn cursor_overlay_matches(requested: bool, rendered: bool) -> bool {
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::mpsc::SyncSender as ReplySender;
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use ferese_ipc::Response;
|
||||
@@ -315,6 +319,7 @@ pub(crate) struct PartOutcome {
|
||||
pub(crate) request: u64,
|
||||
pub(crate) part: usize,
|
||||
pub(crate) result: Result<CaptureBuffer, String>,
|
||||
pub(crate) permit: Option<BudgetPermit>,
|
||||
}
|
||||
|
||||
// A calloop channel so publishing a part also wakes the event loop, whichever
|
||||
@@ -352,11 +357,16 @@ struct Request {
|
||||
parts: Vec<(PartSpec, PartState)>,
|
||||
received: usize,
|
||||
deadline: Instant,
|
||||
permit: Option<BudgetPermit>,
|
||||
}
|
||||
|
||||
pub(crate) enum Action {
|
||||
None,
|
||||
Encode { request: u64, frames: Vec<OutputFrame> },
|
||||
Encode {
|
||||
request: u64,
|
||||
frames: Vec<OutputFrame>,
|
||||
permit: BudgetPermit,
|
||||
},
|
||||
// The path was already sent to the caller; it must not be cleaned up.
|
||||
Delivered(PathBuf),
|
||||
// The request is gone, so this file is ours to remove.
|
||||
@@ -365,6 +375,45 @@ pub(crate) enum Action {
|
||||
|
||||
const MAX_OUTSTANDING: usize = 8;
|
||||
const MAX_PENDING_BYTES: usize = 256 * 1024 * 1024;
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
struct Budget {
|
||||
used: AtomicUsize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Reservation {
|
||||
budget: Arc<Budget>,
|
||||
bytes: usize,
|
||||
}
|
||||
|
||||
impl Drop for Reservation {
|
||||
fn drop(&mut self) {
|
||||
self.budget.used.fetch_sub(self.bytes, Ordering::AcqRel);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct BudgetPermit {
|
||||
_reservation: Arc<Reservation>,
|
||||
}
|
||||
|
||||
impl Budget {
|
||||
fn reserve(self: &Arc<Self>, bytes: usize) -> Result<BudgetPermit, String> {
|
||||
self.used
|
||||
.fetch_update(Ordering::AcqRel, Ordering::Acquire, |used| {
|
||||
used.checked_add(bytes).filter(|total| *total <= MAX_PENDING_BYTES)
|
||||
})
|
||||
.map_err(|_| "Screenshot memory budget is exhausted".to_string())?;
|
||||
Ok(BudgetPermit {
|
||||
_reservation: Arc::new(Reservation {
|
||||
budget: self.clone(),
|
||||
bytes,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// A request is answered or abandoned this long after it is admitted. Parts are
|
||||
// produced by the render path, so an output that is never repainted would
|
||||
// otherwise leave the request in Collecting forever, holding one of the few
|
||||
@@ -375,6 +424,7 @@ pub(crate) const REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
pub(crate) struct Coordinator {
|
||||
requests: std::collections::HashMap<u64, Request>,
|
||||
next_id: u64,
|
||||
budget: Arc<Budget>,
|
||||
deadline_changed: Option<Box<dyn Fn(Option<Instant>)>>,
|
||||
notified_deadline: Option<Instant>,
|
||||
}
|
||||
@@ -432,6 +482,7 @@ impl Coordinator {
|
||||
return Err("Screenshot region is too large".into());
|
||||
}
|
||||
|
||||
let permit = self.budget.reserve(pending)?;
|
||||
self.next_id = self.next_id.wrapping_add(1);
|
||||
let id = self.next_id;
|
||||
self.requests.insert(
|
||||
@@ -444,12 +495,17 @@ impl Coordinator {
|
||||
parts: specs.into_iter().map(|spec| (spec, PartState::Pending)).collect(),
|
||||
received: 0,
|
||||
deadline: now + REQUEST_TIMEOUT,
|
||||
permit: Some(permit),
|
||||
},
|
||||
);
|
||||
self.notify_deadline_change();
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
pub(crate) fn permit(&self, request: u64) -> Option<BudgetPermit> {
|
||||
self.requests.get(&request).and_then(|entry| entry.permit.clone())
|
||||
}
|
||||
|
||||
// Used by the completion tests to assert that a request is still tracked.
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn is_live(&self, request: u64) -> bool {
|
||||
@@ -513,7 +569,11 @@ impl Coordinator {
|
||||
})
|
||||
.collect();
|
||||
entry.state = RequestState::Encoding;
|
||||
Action::Encode { request, frames }
|
||||
Action::Encode {
|
||||
request,
|
||||
frames,
|
||||
permit: entry.permit.take().expect("collecting request owns its reservation"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn on_encoded(&mut self, request: u64, result: Result<PathBuf, String>) -> Action {
|
||||
@@ -1045,6 +1105,43 @@ mod completion {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admission_enforces_one_budget_across_requests_and_late_readbacks() {
|
||||
let (reply, _received) = sync_channel(8);
|
||||
let mut coordinator = Coordinator::new();
|
||||
let first = coordinator.admit(1, 1, reply.clone(), vec![spec(4096, 8192)]).unwrap();
|
||||
let second = coordinator.admit(1, 2, reply.clone(), vec![spec(4096, 8192)]).unwrap();
|
||||
assert!(coordinator.admit(1, 3, reply.clone(), vec![spec(2, 2)]).is_err());
|
||||
|
||||
// A cancelled request can still have a readback retained by a sink or channel.
|
||||
let late_readback = coordinator.permit(first).unwrap();
|
||||
coordinator.reject(first, "cancelled");
|
||||
assert!(coordinator.admit(1, 4, reply.clone(), vec![spec(2, 2)]).is_err());
|
||||
drop(late_readback);
|
||||
assert!(coordinator.admit(1, 5, reply, vec![spec(2, 2)]).is_ok());
|
||||
coordinator.reject(second, "finished");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aggregate_reservations_survive_request_cancellation_during_encoding() {
|
||||
let mut harness = Harness::new(1);
|
||||
let id = harness.coordinator.next_id;
|
||||
let bytes = harness.coordinator.budget.used.load(Ordering::Acquire);
|
||||
let Action::Encode { frames, permit, .. } = harness.coordinator.on_part(id, 0, Ok(buffer(2, 2))) else {
|
||||
panic!("ready request should encode");
|
||||
};
|
||||
harness.coordinator.reject(id, "cancelled while encoder owns pixels");
|
||||
assert_eq!(harness.coordinator.budget.used.load(Ordering::Acquire), bytes);
|
||||
assert!(harness.coordinator.budget.reserve(MAX_PENDING_BYTES).is_err());
|
||||
drop(frames);
|
||||
drop(permit);
|
||||
assert_eq!(harness.coordinator.budget.used.load(Ordering::Acquire), 0);
|
||||
let first = harness.coordinator.budget.reserve(MAX_PENDING_BYTES / 2 + 1).unwrap();
|
||||
assert!(harness.coordinator.budget.reserve(MAX_PENDING_BYTES / 2).is_err());
|
||||
drop(first);
|
||||
assert!(harness.coordinator.budget.reserve(MAX_PENDING_BYTES).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_parts_ready_hands_frames_to_the_encoder() {
|
||||
let mut harness = Harness::new(2);
|
||||
@@ -1055,7 +1152,7 @@ mod completion {
|
||||
Action::None
|
||||
));
|
||||
let action = harness.coordinator.on_part(id, 1, Ok(buffer(2, 2)));
|
||||
let Action::Encode { request, frames } = action else {
|
||||
let Action::Encode { request, frames, .. } = action else {
|
||||
panic!("expected the request to move to encoding");
|
||||
};
|
||||
assert_eq!(request, id);
|
||||
@@ -1407,7 +1504,9 @@ mod completion {
|
||||
// buffer that was handed off is never replaced.
|
||||
assert!(!sink.publish(buffer(64, 64)));
|
||||
|
||||
let PartOutcome { request, part, result } = receiver.try_recv().expect("published once");
|
||||
let PartOutcome {
|
||||
request, part, result, ..
|
||||
} = receiver.try_recv().expect("published once");
|
||||
assert_eq!((request, part), (7, 1));
|
||||
assert_eq!(result.unwrap().width, 2);
|
||||
assert!(receiver.try_recv().is_err(), "no second buffer was sent");
|
||||
@@ -1422,7 +1521,9 @@ mod completion {
|
||||
// failure has already been reported.
|
||||
assert!(!sink.publish(buffer(2, 2)));
|
||||
|
||||
let PartOutcome { request, part, result } = receiver.try_recv().expect("answered");
|
||||
let PartOutcome {
|
||||
request, part, result, ..
|
||||
} = receiver.try_recv().expect("answered");
|
||||
assert_eq!((request, part), (3, 0));
|
||||
assert!(result.is_err());
|
||||
assert!(receiver.try_recv().is_err());
|
||||
|
||||
@@ -20,6 +20,7 @@ const FILE_MODE: u32 = 0o600;
|
||||
pub(crate) struct Job {
|
||||
pub(crate) request: u64,
|
||||
pub(crate) frames: Vec<OutputFrame>,
|
||||
pub(crate) _permit: Option<super::screenshot::BudgetPermit>,
|
||||
}
|
||||
|
||||
pub(crate) struct Encoded {
|
||||
@@ -54,17 +55,13 @@ impl Worker {
|
||||
fn run(receiver: Receiver<Job>, results: channel::SyncSender<Encoded>) {
|
||||
while let Ok(job) = receiver.recv() {
|
||||
let result = encode(&job);
|
||||
let request = job.request;
|
||||
drop(job);
|
||||
// This send cannot block. Each request produces exactly one Encode, so at
|
||||
// most QUEUE_CAPACITY jobs can be queued while one more is being encoded,
|
||||
// and RESULT_QUEUE_CAPACITY is asserted to exceed that. Blocking here
|
||||
// would stall the worker with no way for the loop to recover it.
|
||||
if results
|
||||
.send(Encoded {
|
||||
request: job.request,
|
||||
result,
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
if results.send(Encoded { request, result }).is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -131,25 +128,51 @@ fn encode_png(canvas: &Canvas) -> Result<Vec<u8>, String> {
|
||||
|
||||
// The client is told to open this path and unlink it immediately, so the
|
||||
// directory is ours, private, and files are created with the final mode.
|
||||
struct StagedFile {
|
||||
path: PathBuf,
|
||||
keep: bool,
|
||||
}
|
||||
|
||||
impl Drop for StagedFile {
|
||||
fn drop(&mut self) {
|
||||
if !self.keep {
|
||||
let _ = fs::remove_file(&self.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_private_file(png: &[u8]) -> Result<PathBuf, String> {
|
||||
let directory = private_directory()?;
|
||||
for attempt in 0..64u32 {
|
||||
let path = directory.join(format!("shot-{}-{attempt}.png", std::process::id()));
|
||||
let mut options = fs::OpenOptions::new();
|
||||
options.write(true).create_new(true).mode(FILE_MODE);
|
||||
match options.open(&path) {
|
||||
Ok(mut file) => {
|
||||
use io::Write;
|
||||
file.write_all(png)
|
||||
.and_then(|()| file.sync_all())
|
||||
.map_err(|error| format!("Could not write screenshot: {error}"))?;
|
||||
return Ok(path);
|
||||
}
|
||||
write_private_file_in(&private_directory()?, png, |file, bytes| {
|
||||
use io::Write;
|
||||
file.write_all(bytes)?;
|
||||
file.sync_all()
|
||||
})
|
||||
}
|
||||
|
||||
fn write_private_file_in(
|
||||
directory: &std::path::Path,
|
||||
png: &[u8],
|
||||
write: impl FnOnce(&mut fs::File, &[u8]) -> io::Result<()>,
|
||||
) -> Result<PathBuf, String> {
|
||||
static NEXT_FILE: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
let (mut file, mut staged) = loop {
|
||||
let sequence = NEXT_FILE.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
let path = directory.join(format!("shot-{}-{sequence}.png", std::process::id()));
|
||||
match fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.mode(FILE_MODE)
|
||||
.open(&path)
|
||||
{
|
||||
Ok(file) => break (file, StagedFile { path, keep: false }),
|
||||
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => continue,
|
||||
Err(error) => return Err(format!("Could not write screenshot: {error}")),
|
||||
}
|
||||
}
|
||||
Err("Could not find an unused screenshot filename".into())
|
||||
};
|
||||
|
||||
write(&mut file, png).map_err(|error| format!("Could not write screenshot: {error}"))?;
|
||||
staged.keep = true;
|
||||
Ok(std::mem::take(&mut staged.path))
|
||||
}
|
||||
|
||||
fn private_directory() -> Result<PathBuf, String> {
|
||||
@@ -209,6 +232,7 @@ mod tests {
|
||||
let job = Job {
|
||||
request: 1,
|
||||
frames: vec![frame(4, 3)],
|
||||
_permit: None,
|
||||
};
|
||||
let mut canvas = compose(&job.frames).unwrap();
|
||||
for (index, pixel) in canvas.pixels.chunks_exact_mut(4).enumerate() {
|
||||
@@ -227,6 +251,26 @@ mod tests {
|
||||
assert_eq!(decoded.get_pixel(0, 0)[3], 255);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn staging_has_no_fixed_filename_capacity_and_cleans_partial_writes() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
for _ in 0..100 {
|
||||
write_private_file_in(directory.path(), b"png", |file, bytes| {
|
||||
use io::Write;
|
||||
file.write_all(bytes)
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
assert_eq!(fs::read_dir(directory.path()).unwrap().count(), 100);
|
||||
let result = write_private_file_in(directory.path(), b"partial", |file, bytes| {
|
||||
use io::Write;
|
||||
file.write_all(bytes)?;
|
||||
Err(io::Error::other("injected write/sync failure"))
|
||||
});
|
||||
assert!(result.is_err());
|
||||
assert_eq!(fs::read_dir(directory.path()).unwrap().count(), 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn staged_files_are_owner_only() {
|
||||
let png = b"\x89PNG\r\n\x1a\nnot really a png".to_vec();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use smithay::desktop::{
|
||||
PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, Space, Window, WindowSurfaceType,
|
||||
find_popup_root_surface, get_popup_toplevel_coords,
|
||||
PopupKeyboardGrab, PopupKind, PopupPointerGrab, Window, WindowSurfaceType, find_popup_root_surface,
|
||||
get_popup_toplevel_coords,
|
||||
};
|
||||
use smithay::input::Seat;
|
||||
use smithay::input::pointer::{Focus, GrabStartData};
|
||||
@@ -17,7 +17,7 @@ use smithay::wayland::shell::xdg::{
|
||||
};
|
||||
|
||||
use crate::Ferese;
|
||||
use crate::grabs::{MoveSurfaceGrab, ResizeEdge, ResizeSurfaceGrab, handle_resize_commit};
|
||||
use crate::grabs::{MoveSurfaceGrab, ResizeEdge, ResizeSurfaceGrab};
|
||||
|
||||
impl XdgShellHandler for Ferese {
|
||||
fn xdg_shell_state(&mut self) -> &mut XdgShellState {
|
||||
@@ -82,19 +82,7 @@ impl XdgShellHandler for Ferese {
|
||||
|
||||
fn toplevel_destroyed(&mut self, surface: ToplevelSurface) {
|
||||
// Hidden workspace windows are managed but unmapped from Space.
|
||||
let Some(window) = self
|
||||
.windows
|
||||
.id_for_surface(surface.wl_surface())
|
||||
.and_then(|id| self.windows.window(id))
|
||||
.or_else(|| {
|
||||
self.space.elements().find(|window| {
|
||||
window
|
||||
.toplevel()
|
||||
.is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface())
|
||||
})
|
||||
})
|
||||
.cloned()
|
||||
else {
|
||||
let Some(window) = self.window_for_surface(surface.wl_surface()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -330,17 +318,27 @@ pub(crate) fn initial_configure_sent(toplevel: &ToplevelSurface) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn handle_commit(popups: &mut PopupManager, space: &mut Space<Window>, surface: &WlSurface) {
|
||||
handle_resize_commit(surface);
|
||||
if let Some(window) = space
|
||||
.elements()
|
||||
.find(|window| {
|
||||
window
|
||||
.toplevel()
|
||||
.is_some_and(|toplevel| toplevel.wl_surface() == surface)
|
||||
})
|
||||
.cloned()
|
||||
{
|
||||
pub(crate) fn validate_size_constraints(toplevel: &ToplevelSurface) -> bool {
|
||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
|
||||
use smithay::wayland::shell::xdg::SurfaceCachedState;
|
||||
|
||||
let valid = with_states(toplevel.wl_surface(), |states| {
|
||||
let mut cached = states.cached_state.get::<SurfaceCachedState>();
|
||||
let state = cached.current();
|
||||
let valid_axis = |min: i32, max: i32| min >= 0 && max >= 0 && (max == 0 || min <= max);
|
||||
valid_axis(state.min_size.w, state.max_size.w) && valid_axis(state.min_size.h, state.max_size.h)
|
||||
});
|
||||
if !valid {
|
||||
toplevel
|
||||
.xdg_toplevel()
|
||||
.post_error(xdg_toplevel::Error::InvalidSize, "invalid minimum/maximum window size");
|
||||
}
|
||||
|
||||
valid
|
||||
}
|
||||
|
||||
pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) {
|
||||
if let Some(window) = state.window_for_surface(surface) {
|
||||
let configured = with_states(surface, |states| {
|
||||
states
|
||||
.data_map
|
||||
@@ -358,8 +356,8 @@ pub fn handle_commit(popups: &mut PopupManager, space: &mut Space<Window>, surfa
|
||||
}
|
||||
}
|
||||
|
||||
popups.commit(surface);
|
||||
if let Some(PopupKind::Xdg(popup)) = popups.find_popup(surface)
|
||||
state.popups.commit(surface);
|
||||
if let Some(PopupKind::Xdg(popup)) = state.popups.find_popup(surface)
|
||||
&& !popup.is_initial_configure_sent()
|
||||
{
|
||||
popup.send_configure().expect("initial popup configure is valid");
|
||||
|
||||
Reference in new issue
Block a user