fix compositor edge cases, process handing and other cool improvements

This commit is contained in:
Samuel Onoja committed 2026-10-04 17:11:40 +01:00
1 parent 5706a71941
commit 405c74af9f
38 files changed
+3391 -875

No files matched your search

+2
View File
@@ -36,3 +36,5 @@ tempfile.workspace = true
[features]
# Opt-in counters/histograms with a per-client exit summary.
resize-metrics = []
# One-shot KMS recovery checks; never enabled by the installer.
drm-fault-injection = []
+82 -28
View File
@@ -10,6 +10,7 @@ mod lid;
mod mirror;
mod output_power;
mod planes;
mod recovery;
mod redraw;
mod scheduling;
mod topology;
@@ -260,6 +261,8 @@ struct DirectOutput {
capture_texture: Option<GlesTexture>,
render_metrics: RenderMetrics,
frame_pending: bool,
completion: recovery::Completion,
completion_timer: Option<RegistrationToken>,
animation_fallback: AnimationFallback,
cursor_visible: bool,
scheduler: crate::frame_scheduler::FrameScheduler,
@@ -800,23 +803,43 @@ fn open_device(state: &mut Ferese, path: &Path) -> Result<DrmNode, Box<dyn Error
.loop_handle
.insert_source(notifier, move |event, metadata, state| match event {
DrmEvent::VBlank(crtc) => {
#[cfg(feature = "drm-fault-injection")]
if state
.direct_backend
.as_ref()
.and_then(|backend| backend.devices.get(&node))
.and_then(|device| device.outputs.get(&crtc))
.is_some_and(|output| output.frame_pending)
&& recovery::inject("missing-completion")
{
tracing::warn!(?node, ?crtc, "injecting missing DRM completion");
return;
}
if let Some(metadata) = metadata {
let mut retired = false;
let mut retirement_failed = false;
let feedback = state
.direct_backend
.as_mut()
.and_then(|backend| backend.devices.get_mut(&node))
.and_then(|device| device.outputs.get_mut(&crtc))
.and_then(|output| match output.surface.frame_submitted() {
Ok(feedback) => {
retired = feedback.is_some();
feedback
.and_then(|output| {
#[cfg(feature = "drm-fault-injection")]
if output.frame_pending && recovery::inject("retirement") {
tracing::warn!(?node, ?crtc, "injecting DRM retirement failure");
retirement_failed = true;
return None;
}
Err(error) => {
let (feedback, completed, error) = recovery::retirement(output.surface.frame_submitted());
retired = completed;
if let Some(error) = error {
tracing::error!(?node, ?crtc, %error, "failed to retire DRM frame");
None
retirement_failed = true;
}
feedback
});
if let (Some(mut feedback), DrmEventTime::Monotonic(time)) = (feedback, metadata.time) {
@@ -852,6 +875,7 @@ fn open_device(state: &mut Ferese, path: &Path) -> Result<DrmNode, Box<dyn Error
.and_then(|device| device.outputs.get_mut(&crtc))
&& retired
{
output.completion.retired();
output.frame_pending = false;
output.power.presented();
if let DrmEventTime::Monotonic(timestamp) = metadata.time {
@@ -886,6 +910,11 @@ fn open_device(state: &mut Ferese, path: &Path) -> Result<DrmNode, Box<dyn Error
state.refresh_idle_inhibition();
}
if retirement_failed {
recovery::reconcile_device(state, node);
return;
}
apply_output_power(state);
state.record_drm_presentation(node, crtc, metadata.time, metadata.sequence);
@@ -1168,6 +1197,10 @@ pub(crate) fn system_resumed(state: &mut Ferese) {
lid::request_refresh(state, true);
}
pub(crate) fn reconcile_after_monitor_recovery(state: &mut Ferese) {
lid::request_reconcile(state);
}
fn internal_connector(interface: connector::Interface) -> bool {
matches!(
interface,
@@ -1268,19 +1301,19 @@ fn render_output(
primary.sync.wait()?;
}
if output.mirror_source.is_some() {
capture::capture_mirror(state, &mut device.renderer, &scene_output, &frame, &mut output)?;
} else {
capture::capture_output(
state,
&mut device.renderer,
&mut output.capture_texture,
&output.output,
&frame,
)?;
}
if result.is_empty {
if output.mirror_source.is_some() {
capture::capture_mirror(state, &mut device.renderer, &scene_output, &frame, &mut output);
} else {
capture::capture_output(
state,
&mut device.renderer,
&mut output.capture_texture,
&output.output,
&frame,
);
}
output
.render_metrics
.record_no_damage(render_started.elapsed(), missed_deadlines);
@@ -1327,16 +1360,35 @@ fn render_output(
};
let visibility = result.states.clone();
drop(result);
output.surface.queue_frame(presentation)?;
if output.mirror_source.is_none() {
state.display_presentation.queued(&output.output, &visibility);
}
output.primary_commit = primary_commit;
output
.render_metrics
.record_frame(render_started.elapsed(), &damage, missed_deadlines, effects);
output.frame_pending = true;
output.lock_frame_pending = state.session_lock.active();
capture::after_queue(output.surface.queue_frame(presentation), || {
if output.mirror_source.is_none() {
state.display_presentation.queued(&output.output, &visibility);
}
output.primary_commit = primary_commit;
output
.render_metrics
.record_frame(render_started.elapsed(), &damage, missed_deadlines, effects);
output.frame_pending = true;
if !recovery::arm_completion(&state.loop_handle, node, crtc, &mut output) {
recovery::reconcile_device(state, node);
}
output.lock_frame_pending = state.session_lock.active();
if output.mirror_source.is_some() {
capture::capture_mirror(state, &mut device.renderer, &scene_output, &frame, &mut output);
} else {
capture::capture_output(
state,
&mut device.renderer,
&mut output.capture_texture,
&output.output,
&frame,
);
}
})?;
Ok(true)
})()
};
@@ -2042,6 +2094,8 @@ fn create_direct_output(
capture_texture: None,
render_metrics,
frame_pending: false,
completion: Default::default(),
completion_timer: None,
animation_fallback: AnimationFallback::default(),
cursor_visible: false,
power: OutputPower::default(),
+130 -8
View File
@@ -3,6 +3,7 @@ use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::{GlesError, GlesRenderer, GlesTarget, GlesTexture};
use smithay::backend::renderer::{Bind, Offscreen, Texture};
use smithay::output::Output;
use std::fmt::Display;
use crate::Ferese;
use crate::render::{capture_output_elements, redraw_output};
@@ -14,7 +15,7 @@ pub(crate) fn capture_output(
texture: &mut Option<GlesTexture>,
output: &Output,
scene: &FrameScene,
) -> Result<(), GlesError> {
) {
for include_cursor in [true, false] {
if !state.has_pending_screencopy(output, include_cursor) {
continue;
@@ -23,11 +24,13 @@ pub(crate) fn capture_output(
// Sample the same scene into an independent target: the displayed
// primary buffer may omit both a scanned-out client and hardware cursor.
let elements = capture_output_elements(state, renderer, output, include_cursor, scene);
let target = render_capture(renderer, texture, output, &elements)?;
let Some(target) = capture_request(state, output, include_cursor, || {
render_capture(renderer, texture, output, &elements)
}) else {
continue;
};
state.process_screencopies(renderer, &target, output, include_cursor);
}
Ok(())
}
pub(super) fn capture_mirror(
@@ -36,7 +39,7 @@ pub(super) fn capture_mirror(
source: &Output,
scene: &FrameScene,
output: &mut super::DirectOutput,
) -> Result<(), GlesError> {
) {
for include_cursor in [true, false] {
if !state.has_pending_screencopy(&output.output, include_cursor) {
continue;
@@ -44,17 +47,46 @@ pub(super) fn capture_mirror(
let elements = capture_output_elements(state, renderer, source, include_cursor, scene);
// Never sample mirror_texture: it contains the unfiltered display.
let canvas = output.mirror_canvas.as_ref().expect("mirror display composed");
super::mirror::compose(renderer, &mut output.mirror_capture_texture, canvas, source, &elements)?;
let Some(()) = capture_request(state, &output.output, include_cursor, || {
super::mirror::compose(renderer, &mut output.mirror_capture_texture, canvas, source, &elements)
}) else {
continue;
};
let element = super::mirror::fitted_texture(
output.mirror_capture_texture.as_ref().unwrap().clone(),
&output.output,
smithay::backend::renderer::element::Id::new(),
Default::default(),
);
let target = render_capture(renderer, &mut output.capture_texture, &output.output, &[element])?;
let Some(target) = capture_request(state, &output.output, include_cursor, || {
render_capture(renderer, &mut output.capture_texture, &output.output, &[element])
}) else {
continue;
};
state.process_screencopies(renderer, &target, &output.output, include_cursor);
}
Ok(())
}
fn capture_request<T, E: Display>(
state: &mut Ferese,
output: &Output,
include_cursor: bool,
render: impl FnOnce() -> Result<T, E>,
) -> Option<T> {
match render() {
Ok(target) => Some(target),
Err(error) => {
tracing::warn!(?output, include_cursor, %error, "screencopy render failed");
state.fail_screencopies(output, include_cursor);
None
}
}
}
pub(super) fn after_queue<T, E>(queued: Result<T, E>, capture: impl FnOnce()) -> Result<T, E> {
let queued = queued?;
capture();
Ok(queued)
}
pub(super) fn render_capture<'a, E: RenderElement<GlesRenderer>>(
@@ -178,3 +210,93 @@ mod tests {
assert_eq!(capture.as_ref().unwrap().size(), (48, 16).into());
}
}
#[cfg(test)]
mod failure_tests {
use std::cell::Cell;
use calloop::channel::channel;
use smithay::output::Output;
use smithay::utils::{Buffer, Rectangle};
use super::*;
#[test]
fn capture_allocation_failure_fails_one_request_after_display_submission() {
if !crate::startup_tests::private_runtime(
"backends::direct::capture::failure_tests::capture_allocation_failure_fails_one_request_after_display_submission",
) {
return;
}
let mut events = calloop::EventLoop::try_new().unwrap();
let mut state = crate::startup_tests::state(&mut events);
let output = Output::new(
"capture-failure".into(),
smithay::output::PhysicalProperties {
size: (0, 0).into(),
subpixel: smithay::output::Subpixel::Unknown,
make: "test".into(),
model: "test".into(),
},
);
let (sender, receiver) = channel();
state
.pending_screencopies
.push(crate::handlers::screencopy::PendingScreencopy::owned(
41,
0,
sender,
output.clone(),
Rectangle::<i32, Buffer>::from_size((16, 16).into()),
));
let other_output = Output::new(
"other-capture".into(),
smithay::output::PhysicalProperties {
size: (0, 0).into(),
subpixel: smithay::output::Subpixel::Unknown,
make: "test".into(),
model: "test".into(),
},
);
let (other_sender, other_receiver) = channel();
state
.pending_screencopies
.push(crate::handlers::screencopy::PendingScreencopy::owned(
42,
0,
other_sender,
other_output.clone(),
Rectangle::<i32, Buffer>::from_size((16, 16).into()),
));
let submitted = Cell::new(false);
let queued = {
submitted.set(true);
Ok::<(), ()>(())
};
after_queue(queued, || {
assert!(submitted.get(), "capture must run after the display queue succeeds");
assert!(
capture_request(&mut state, &output, false, || Err::<(), _>(
"injected allocation failure"
))
.is_none()
);
})
.unwrap();
assert!(submitted.get(), "capture failure must not undo queued display work");
assert_eq!(state.pending_screencopies.len(), 1);
assert_eq!(state.pending_screencopies[0].output, other_output);
let outcome = receiver.try_recv().unwrap();
assert_eq!(outcome.request, 41);
assert_eq!(outcome.part, 0);
assert!(outcome.result.is_err());
assert!(receiver.try_recv().is_err(), "request should fail exactly once");
assert!(
other_receiver.try_recv().is_err(),
"other output's request must stay pending"
);
}
}
+45 -13
View File
@@ -8,6 +8,7 @@ pub(super) struct Probe {
serial: u64,
revision: u64,
reactivate: bool,
wake: bool,
}
#[derive(Default)]
@@ -33,12 +34,13 @@ impl LidState {
changed
}
fn begin(&mut self, reactivate: bool) -> Probe {
fn begin(&mut self, reactivate: bool, wake: bool) -> Probe {
self.serial = self.serial.wrapping_add(1);
let probe = Probe {
serial: self.serial,
revision: self.revision,
reactivate: reactivate || self.pending.is_some_and(|pending| pending.reactivate),
wake: wake || self.pending.is_some_and(|pending| pending.wake),
};
self.pending = Some(probe);
probe
@@ -269,6 +271,15 @@ impl Reader {
}
pub(super) fn request_refresh(state: &mut Ferese, reactivate: bool) {
request(state, reactivate, true);
}
/// Listener recovery reconciles hardware without resetting idle activity.
pub(super) fn request_reconcile(state: &mut Ferese) {
request(state, false, false);
}
fn request(state: &mut Ferese, reactivate: bool, wake: bool) {
let Some(backend) = state.direct_backend.as_mut() else {
return;
};
@@ -289,7 +300,7 @@ pub(super) fn request_refresh(state: &mut Ferese, reactivate: bool) {
}
backend.lid_reader.suspend_requested = false;
backend.lid_reader.lease.lock().unwrap().suspend = false;
let probe = backend.lid.begin(reactivate);
let probe = backend.lid.begin(reactivate, wake);
let sent = backend.lid_reader.requests.send(probe).is_ok();
let timer = state
.loop_handle
@@ -329,10 +340,13 @@ fn complete_refresh(state: &mut Ferese, probe: Probe, value: Option<bool>) {
// Waking a locked session may request redraws. Hold them until the fresh
// device/connector snapshot has been reconciled.
backend.lid_reader.suspend_requested = false;
backend.topology.reconciling = true;
state.reset_animation_clock();
state.lock_input_activity();
state.direct_backend.as_mut().unwrap().topology.reconciling = false;
if probe.wake {
backend.topology.reconciling = true;
state.reset_animation_clock();
state.lock_input_activity();
state.direct_backend.as_mut().unwrap().topology.reconciling = false;
}
reconcile_outputs(state, reactivate);
}
@@ -431,7 +445,7 @@ mod tests {
fn newer_input_wins_over_a_delayed_resume_read() {
let mut lid = LidState::default();
lid.observe(true);
let probe = lid.begin(true);
let probe = lid.begin(true, true);
lid.observe(false);
assert_eq!(lid.complete(probe, Some(true)), Some(true));
assert!(!lid.closed());
@@ -440,11 +454,11 @@ mod tests {
#[test]
fn failed_reads_preserve_known_state_and_unknown_is_not_an_open_observation() {
let mut lid = LidState::default();
let probe = lid.begin(false);
let probe = lid.begin(false, true);
lid.complete(probe, None);
assert_eq!(lid.value, None);
lid.observe(true);
let probe = lid.begin(true);
let probe = lid.begin(true, true);
lid.complete(probe, None);
assert_eq!(lid.value, Some(true));
}
@@ -452,21 +466,39 @@ mod tests {
#[test]
fn pause_and_newer_resume_invalidate_old_replies() {
let mut lid = LidState::default();
let old = lid.begin(true);
let old = lid.begin(true, true);
lid.pause();
assert_eq!(lid.complete(old, Some(true)), None);
let current = lid.begin(true);
let current = lid.begin(true, true);
assert_eq!(lid.complete(old, Some(true)), None);
assert!(lid.pending());
assert_eq!(lid.complete(current, Some(false)), Some(true));
assert!(!lid.pending());
}
#[test]
fn listener_recovery_does_not_request_idle_wake_or_reactivation() {
let mut lid = LidState::default();
let recovery = lid.begin(false, false);
assert!(!recovery.wake);
assert_eq!(lid.complete(recovery, Some(false)), Some(false));
}
#[test]
fn listener_recovery_preserves_a_pending_real_wake() {
let mut lid = LidState::default();
let resume = lid.begin(true, true);
let recovery = lid.begin(false, false);
assert!(recovery.wake);
assert_eq!(lid.complete(resume, Some(false)), None);
assert_eq!(lid.complete(recovery, Some(false)), Some(true));
}
#[test]
fn system_wake_cannot_cancel_pending_session_reactivation() {
let mut lid = LidState::default();
lid.begin(true);
let probe = lid.begin(false);
lid.begin(true, true);
let probe = lid.begin(false, true);
assert_eq!(lid.complete(probe, Some(false)), Some(true));
}
}
+165
View File
@@ -0,0 +1,165 @@
//! Uncertain KMS completion requires device reconciliation, not buffer reuse.
use super::*;
const COMPLETION_TIMEOUT: Duration = Duration::from_secs(2);
#[cfg(feature = "drm-fault-injection")]
pub(super) fn inject(mode: &str) -> bool {
use std::sync::LazyLock;
use std::sync::atomic::{AtomicBool, Ordering};
static MODE: LazyLock<Option<String>> = LazyLock::new(|| std::env::var("FERESE_TEST_DRM_FAILURE").ok());
static USED: AtomicBool = AtomicBool::new(false);
MODE.as_deref() == Some(mode) && !USED.swap(true, Ordering::AcqRel)
}
#[derive(Default)]
pub(super) struct Completion {
pending: Option<Instant>,
}
impl Completion {
fn queued(&mut self, now: Instant) {
self.pending = Some(now);
}
pub fn retired(&mut self) {
self.pending = None;
}
fn remaining(&self, now: Instant) -> Option<Duration> {
self.pending
.map(|queued| COMPLETION_TIMEOUT.saturating_sub(now.saturating_duration_since(queued)))
}
}
// Keep the submission result and the recovery decision together. An error must
// not be mistaken for a completed frame with no presentation feedback.
pub(super) fn retirement<T, E>(result: Result<Option<T>, E>) -> (Option<T>, bool, Option<E>) {
match result {
Ok(feedback) => {
let retired = feedback.is_some();
(feedback, retired, None)
}
Err(error) => (None, false, Some(error)),
}
}
pub(super) fn arm_completion(
handle: &smithay::reexports::calloop::LoopHandle<'static, Ferese>,
node: DrmNode,
crtc: crtc::Handle,
output: &mut DirectOutput,
) -> bool {
output.completion.queued(Instant::now());
if output.completion_timer.is_some() {
return true;
}
let identity = output.output.clone();
match handle.insert_source(Timer::from_duration(COMPLETION_TIMEOUT), move |_, _, state| {
let Some(output) = state
.direct_backend
.as_mut()
.and_then(|backend| backend.devices.get_mut(&node))
.and_then(|device| device.outputs.get_mut(&crtc))
.filter(|output| output.output == identity)
else {
return TimeoutAction::Drop;
};
if let Some(remaining) = output.completion.remaining(Instant::now()) {
if !remaining.is_zero() {
return TimeoutAction::ToDuration(remaining);
}
output.completion_timer = None;
tracing::error!(?node, ?crtc, "DRM frame completion timed out; reconciling device");
reconcile_device(state, node);
} else {
output.completion_timer = None;
}
TimeoutAction::Drop
}) {
Ok(token) => {
output.completion_timer = Some(token);
true
}
Err(error) => {
tracing::error!(%error, ?node, ?crtc, "could not arm DRM completion watchdog");
false
}
}
}
pub(super) fn reconcile_device(state: &mut Ferese, node: DrmNode) {
let Some(backend) = state.direct_backend.as_mut() else {
return;
};
backend.topology.dirty = true;
if !backend.topology.invalidated.insert(node) {
return;
}
// Defer teardown until the DRM notifier callback has returned. Inactive
// sessions retain the invalidation for fresh enumeration on activation.
let active = backend.active;
if active {
match state.loop_handle.insert_source(Timer::immediate(), |_, _, state| {
reconcile_outputs(state, false);
TimeoutAction::Drop
}) {
Ok(_) => (),
Err(error) => tracing::error!(%error, "could not schedule DRM device recovery"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn failed_retirement_does_not_release_pending_buffer_ownership() {
let now = Instant::now();
let mut completion = Completion::default();
completion.queued(now);
let (feedback, retired, error) = retirement::<(), _>(Err("injected KMS retirement failure"));
assert_eq!(feedback, None);
assert!(!retired);
assert!(error.is_some(), "the production handler must reconcile the device");
assert_eq!(completion.remaining(now), Some(COMPLETION_TIMEOUT));
assert_eq!(retirement::<(), &str>(Ok(None)), (None, false, None));
assert_eq!(retirement::<(), &str>(Ok(Some(()))), (Some(()), true, None));
// Only successful retirement or device teardown can end uncertainty.
completion = Completion::default();
assert_eq!(completion.remaining(now), None);
}
#[test]
fn missing_completion_remains_pending_until_hardware_reconciliation() {
let now = Instant::now();
let mut completion = Completion::default();
completion.queued(now);
assert_eq!(
completion.remaining(now + COMPLETION_TIMEOUT - Duration::from_millis(1)),
Some(Duration::from_millis(1))
);
// A timeout observes uncertainty; it never frees the current buffer.
assert_eq!(completion.remaining(now + COMPLETION_TIMEOUT), Some(Duration::ZERO));
assert_eq!(
completion.remaining(now + COMPLETION_TIMEOUT + Duration::from_secs(1)),
Some(Duration::ZERO)
);
completion.retired();
assert_eq!(completion.remaining(now), None);
completion.queued(now + Duration::from_secs(3));
assert_eq!(
completion.remaining(now + Duration::from_secs(4)),
Some(Duration::from_secs(1)),
"existing timer must respect the newer frame's deadline"
);
}
}
+8 -3
View File
@@ -9,9 +9,14 @@ pub(super) fn cancel_output_timers(
handle: &smithay::reexports::calloop::LoopHandle<'static, Ferese>,
output: &mut DirectOutput,
) {
for token in [output.render_timer.take(), output.callback_timer.take()]
.into_iter()
.flatten()
output.completion.retired();
for token in [
output.render_timer.take(),
output.callback_timer.take(),
output.completion_timer.take(),
]
.into_iter()
.flatten()
{
handle.remove(token);
}
+4 -18
View File
@@ -1,7 +1,6 @@
//! Session-owned services. Never grant shell/effects privileges to daemons.
use std::cell::RefCell;
use std::collections::HashMap;
use std::os::fd::{FromRawFd, OwnedFd};
use std::process::Child;
use std::rc::Rc;
use std::time::{Duration, Instant};
@@ -13,6 +12,7 @@ use smithay::reexports::calloop::{Interest, Mode, PostAction, RegistrationToken}
use crate::Ferese;
use crate::config::DaemonConfig;
use crate::private_client::ClientCapabilities;
use crate::process::{pidfd, spawn_client, terminate_child};
struct Service {
config: DaemonConfig,
@@ -27,16 +27,6 @@ pub(crate) struct Runner(
Option<(RegistrationToken, Instant)>,
);
fn pidfd(pid: u32) -> std::io::Result<OwnedFd> {
// SAFETY: pidfd_open has no pointer arguments and returns a new owned fd.
let fd = unsafe { libc::syscall(libc::SYS_pidfd_open, pid, 0) };
if fd < 0 {
return Err(std::io::Error::last_os_error());
}
// SAFETY: a successful syscall returned a unique valid descriptor.
Ok(unsafe { OwnedFd::from_raw_fd(fd as i32) })
}
impl Runner {
pub fn start(state: &mut Ferese) -> Rc<RefCell<Self>> {
let runner = Rc::new(RefCell::new(Self::new(
@@ -153,7 +143,7 @@ impl Runner {
// TERM/grace/reaping must not pause the compositor's event loop.
for mut removed in previous {
if let Some(mut child) = removed.child.take() {
std::thread::spawn(move || crate::terminate_child(&mut child));
std::thread::spawn(move || terminate_child(&mut child));
}
}
}
@@ -195,11 +185,7 @@ impl Runner {
}
}
if !service.finished && now >= service.next_start {
service.child = crate::spawn_client(
state,
service.config.command.iter().map(Into::into).collect(),
ClientCapabilities::default(),
);
service.child = spawn_client(state, service.config.command.iter(), ClientCapabilities::default());
service.next_start = now + Duration::from_secs(5);
if service.child.is_none() && !service.config.restart {
service.finished = true;
@@ -211,7 +197,7 @@ impl Runner {
pub fn stop(&mut self) {
for service in &mut self.0 {
if let Some(mut child) = service.child.take() {
crate::terminate_child(&mut child);
terminate_child(&mut child);
}
service.finished = true;
}
+1 -1
View File
@@ -2,4 +2,4 @@ mod move_grab;
mod resize_grab;
pub use move_grab::MoveSurfaceGrab;
pub use resize_grab::{ResizeEdge, ResizeSurfaceGrab, handle_resize_commit};
pub use resize_grab::{ResizeEdge, ResizeSurfaceGrab};
+55 -74
View File
@@ -1,5 +1,3 @@
use std::cell::RefCell;
use smithay::desktop::Window;
use smithay::input::pointer::{
AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent,
@@ -98,10 +96,6 @@ impl ResizeSurfaceGrab {
edges: ResizeEdge,
initial_rect: Rectangle<i32, Logical>,
) -> Self {
ResizeState::with(
window.toplevel().expect("managed window has a toplevel").wl_surface(),
|state| *state = ResizeState::Resizing { edges, initial_rect },
);
Self {
start_data,
window,
@@ -128,8 +122,8 @@ impl PointerGrab<Ferese> for ResizeSurfaceGrab {
let initial = self.initial_rect;
let raw_left = initial.loc.x as f64 + if self.edges.left() { delta.x } else { 0. };
let raw_top = initial.loc.y as f64 + if self.edges.top() { delta.y } else { 0. };
let raw_right = (initial.loc.x + initial.size.w) as f64 + if self.edges.right() { delta.x } else { 0. };
let raw_bottom = (initial.loc.y + initial.size.h) as f64 + if self.edges.bottom() { delta.y } else { 0. };
let raw_right = initial.loc.x as f64 + initial.size.w as f64 + if self.edges.right() { delta.x } else { 0. };
let raw_bottom = initial.loc.y as f64 + initial.size.h as f64 + if self.edges.bottom() { delta.y } else { 0. };
let raw = ferese_layout::Rect::new(
raw_left,
raw_top,
@@ -203,12 +197,6 @@ impl PointerGrab<Ferese> for ResizeSurfaceGrab {
state.size = Some(self.last_size);
});
surface.send_pending_configure();
ResizeState::with(surface.wl_surface(), |state| {
*state = ResizeState::WaitingForFinalCommit {
edges: self.edges,
initial_rect: self.initial_rect,
};
});
}
}
@@ -307,57 +295,10 @@ impl PointerGrab<Ferese> for ResizeSurfaceGrab {
state.size = Some(self.initial_rect.size);
});
surface.send_pending_configure();
ResizeState::with(surface.wl_surface(), |state| *state = ResizeState::Idle);
data.set_floating_window_geometry(&self.window, self.initial_rect.loc, self.initial_rect.size);
}
}
#[derive(Clone, Copy, Debug, Default)]
enum ResizeState {
#[default]
Idle,
Resizing {
edges: ResizeEdge,
initial_rect: Rectangle<i32, Logical>,
},
WaitingForFinalCommit {
edges: ResizeEdge,
initial_rect: Rectangle<i32, Logical>,
},
}
impl ResizeState {
fn with<T>(surface: &WlSurface, callback: impl FnOnce(&mut Self) -> T) -> T {
with_states(surface, |states| {
states.data_map.insert_if_missing(RefCell::<Self>::default);
callback(
&mut states
.data_map
.get::<RefCell<Self>>()
.expect("resize state was inserted")
.borrow_mut(),
)
})
}
fn commit(&mut self) -> Option<(ResizeEdge, Rectangle<i32, Logical>)> {
match *self {
Self::Idle => None,
Self::Resizing { edges, initial_rect } => Some((edges, initial_rect)),
Self::WaitingForFinalCommit { edges, initial_rect } => {
*self = Self::Idle;
Some((edges, initial_rect))
}
}
}
}
pub fn handle_resize_commit(surface: &WlSurface) {
// The compositor's visual rect owns the anchored edge. A delayed client
// commit must not move the window or change its stacking order.
ResizeState::with(surface, ResizeState::commit);
}
fn constrained_size(
initial: Size<i32, Logical>,
delta: Point<f64, Logical>,
@@ -368,21 +309,34 @@ fn constrained_size(
let mut width = initial.w;
let mut height = initial.h;
if edges.left() {
width -= delta.x as i32;
width = width.saturating_sub(delta.x as i32);
} else if edges.right() {
width += delta.x as i32;
}
if edges.top() {
height -= delta.y as i32;
} else if edges.bottom() {
height += delta.y as i32;
width = width.saturating_add(delta.x as i32);
}
let maximum_width = if maximum.w == 0 { i32::MAX } else { maximum.w };
let maximum_height = if maximum.h == 0 { i32::MAX } else { maximum.h };
if edges.top() {
height = height.saturating_sub(delta.y as i32);
} else if edges.bottom() {
height = height.saturating_add(delta.y as i32);
}
// Protocol validation rejects these ranges, but a grab may still be alive
// when its client is disconnected. Keep this path safe for any cached state.
let minimum_width = minimum.w.max(1);
let minimum_height = minimum.h.max(1);
let maximum_width = if maximum.w <= 0 {
i32::MAX
} else {
maximum.w.max(minimum_width)
};
let maximum_height = if maximum.h <= 0 {
i32::MAX
} else {
maximum.h.max(minimum_height)
};
(
width.clamp(minimum.w.max(1), maximum_width),
height.clamp(minimum.h.max(1), maximum_height),
width.clamp(minimum_width, maximum_width),
height.clamp(minimum_height, maximum_height),
)
.into()
}
@@ -395,10 +349,11 @@ fn resized_rect(
let mut location = initial.loc;
if edges.left() {
location.x += initial.size.w - size.w;
location.x = location.x.saturating_add(initial.size.w.saturating_sub(size.w));
}
if edges.top() {
location.y += initial.size.h - size.h;
location.y = location.y.saturating_add(initial.size.h.saturating_sub(size.h));
}
Rectangle::new(location, size)
@@ -469,6 +424,32 @@ mod tests {
assert_eq!(size, (1_024, 768).into());
}
#[test]
fn invalid_and_extreme_client_limits_cannot_panic_or_overflow() {
for (minimum, maximum) in [
((800, 700), (400, 300)),
((-1, -2), (-3, -4)),
((i32::MAX, i32::MAX), (1, 1)),
((0, 0), (0, 0)),
] {
for delta in [(f64::MAX, f64::MAX), (-f64::MAX, -f64::MAX), (0.0, 0.0)] {
let size = constrained_size(
(800, 600).into(),
delta.into(),
ResizeEdge(xdg_toplevel::ResizeEdge::BottomRight),
minimum.into(),
maximum.into(),
);
assert!(size.w >= 1 && size.h >= 1);
let _ = resized_rect(
Rectangle::new((100, 80).into(), (800, 600).into()),
size,
ResizeEdge(xdg_toplevel::ResizeEdge::TopLeft),
);
}
}
}
#[test]
fn resized_rect_keeps_the_opposite_edge_fixed() {
let initial = Rectangle::new((100, 80).into(), (800, 600).into());
+244 -4
View File
@@ -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);
}
}
+11 -8
View File
@@ -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);
}
+7 -1
View File
@@ -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);
}
+26
View File
@@ -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 {
+106 -5
View File
@@ -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());
+66 -22
View File
@@ -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();
+27 -29
View File
@@ -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");
+70 -17
View File
@@ -1,5 +1,3 @@
use std::process::Command;
use ferese_layout::Direction;
use smithay::backend::input::{
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, GestureBeginEvent, GestureEndEvent,
@@ -22,6 +20,7 @@ use crate::Ferese;
use crate::config::BindingAction;
use crate::gestures::SwipeDirection;
use crate::grabs::{MoveSurfaceGrab, ResizeEdge, ResizeSurfaceGrab};
use crate::process::spawn_client;
const BTN_LEFT: u32 = 0x110;
const BTN_RIGHT: u32 = 0x111;
@@ -278,11 +277,16 @@ impl Ferese {
keyboard.set_focus(self, None, SERIAL_COUNTER.next_serial());
}
}
keyboard.input::<(), _>(
let serial = SERIAL_COUNTER.next_serial();
let interaction_focus = keyboard.current_focus();
let client_grab = keyboard
.with_grab(|_, grab| grab.is::<smithay::desktop::PopupKeyboardGrab<Self>>())
.unwrap_or(true);
let intercepted = keyboard.input::<(), _>(
self,
keycode,
state,
SERIAL_COUNTER.next_serial(),
serial,
event.time_msec(),
|data, modifiers, keysym| {
if data.focus_cycle.is_some() {
@@ -423,6 +427,10 @@ impl Ferese {
FilterResult::Intercept(())
},
);
if state == KeyState::Pressed && intercepted.is_none() && client_grab {
self.record_activation_input(serial, interaction_focus);
}
if was_captured && !self.input_capture.active() {
self.restore_input_capture_focus();
}
@@ -534,6 +542,10 @@ impl Ferese {
if self.overview.is_active() {
let position = pointer.current_location();
if self.layer_under(position).is_some() {
if event.state() == ButtonState::Pressed {
self.record_activation_input(serial, pointer.current_focus());
}
pointer.button(
self,
&ButtonEvent {
@@ -584,6 +596,16 @@ impl Ferese {
self.focus_window_at(pointer.current_location(), serial, true);
}
let client_grab = pointer
.with_grab(|_, grab| {
grab.is::<smithay::desktop::PopupPointerGrab<Self>>()
|| grab.is::<smithay::input::pointer::ClickGrab<Self>>()
})
.unwrap_or(true);
if event.state() == ButtonState::Pressed && client_grab {
self.record_activation_input(serial, pointer.current_focus());
}
pointer.button(
self,
&ButtonEvent {
@@ -654,6 +676,10 @@ impl Ferese {
let touch = self.seat.get_touch().expect("seat has touch capability");
let serial = SERIAL_COUNTER.next_serial();
if !touch.is_grabbed() {
self.record_activation_input(serial, self.surface_under(location).map(|(surface, _)| surface));
}
touch.down(
self,
self.surface_under(location),
@@ -1042,20 +1068,10 @@ impl Ferese {
match action {
BindingAction::None => {}
BindingAction::Spawn(argv) => {
let (program, args) = argv.split_first().expect("binding commands are validated");
match Command::new(program)
.args(args)
.env("WAYLAND_DISPLAY", &self.socket_name)
.env_remove("WAYLAND_SOCKET")
.spawn()
if let Some(child) =
spawn_client(self, argv.iter(), crate::private_client::ClientCapabilities::default())
{
Ok(child) => {
tracing::info!(%program, pid = child.id(), "spawned binding command")
}
Err(error) => {
tracing::warn!(%program, %error, "failed to spawn binding command")
}
crate::process::watch_client_exit(&self.loop_handle, child);
}
}
BindingAction::Close => self.close_focused_window(),
@@ -1166,6 +1182,43 @@ fn overview_escape(symbol: u32, overview_active: bool) -> bool {
mod tests {
use super::{emergency_shortcut_escape, overview_escape, virtual_terminal};
#[test]
fn binding_commands_are_reaped_while_the_compositor_runs() {
if !crate::startup_tests::private_runtime("input::tests::binding_commands_are_reaped_while_the_compositor_runs")
{
return;
}
let mut events = smithay::reexports::calloop::EventLoop::try_new().unwrap();
let mut state = crate::startup_tests::state(&mut events);
let children_path = format!("/proc/self/task/{}/children", unsafe { libc::gettid() });
assert!(std::fs::read_to_string(&children_path).unwrap().trim().is_empty());
for _ in 0..24 {
state.execute_binding(crate::config::BindingAction::Spawn(vec!["/bin/true".into()].into()));
}
let children: Vec<i32> = std::fs::read_to_string(&children_path)
.unwrap()
.split_whitespace()
.map(|pid| pid.parse().unwrap())
.collect();
assert_eq!(children.len(), 24);
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(3);
while !std::fs::read_to_string(&children_path).unwrap().trim().is_empty() {
assert!(std::time::Instant::now() < deadline, "binding children were not reaped");
events
.dispatch(Some(std::time::Duration::from_millis(20)), &mut state)
.unwrap();
crate::after_dispatch(&mut state);
}
for pid in children {
let mut status = 0;
assert_eq!(unsafe { libc::waitpid(pid, &mut status, libc::WNOHANG) }, -1);
assert_eq!(std::io::Error::last_os_error().raw_os_error(), Some(libc::ECHILD));
}
}
#[test]
fn escape_closes_only_an_active_overview() {
assert!(overview_escape(keysyms::KEY_Escape, true));
+29 -9
View File
@@ -908,14 +908,16 @@ impl Ferese {
for (position, part) in planned.iter().enumerate() {
let (output, _) = &targets[part.index];
self.pending_screencopies
.push(crate::handlers::screencopy::PendingScreencopy::owned(
self.pending_screencopies.push(
crate::handlers::screencopy::PendingScreencopy::owned(
id,
position,
parts.clone(),
output.clone(),
part.buffer,
));
)
.with_permit(self.screenshot.permit(id)),
);
}
// The nested backend redraws on its refresh timer; this drives the
@@ -1006,11 +1008,20 @@ impl Ferese {
request: capture,
part: 0,
result,
permit: self.screenshot.permit(capture),
});
}
pub(crate) fn on_screenshot_part(&mut self, outcome: PartOutcome) {
let action = self.screenshot.on_part(outcome.request, outcome.part, outcome.result);
let PartOutcome {
request,
part,
result,
permit,
} = outcome;
let action = self.screenshot.on_part(request, part, result);
// Pixels have moved into the coordinator/job before the channel lease is dropped.
drop(permit);
self.apply_screenshot_action(action);
}
@@ -1022,11 +1033,20 @@ impl Ferese {
fn apply_screenshot_action(&mut self, action: Action) {
match action {
Action::None => {}
Action::Encode { request, frames } => {
let submitted = self
.screenshot_worker
.as_ref()
.is_some_and(|worker| worker.submit(Job { request, frames }).is_ok());
Action::Encode {
request,
frames,
permit,
} => {
let submitted = self.screenshot_worker.as_ref().is_some_and(|worker| {
worker
.submit(Job {
request,
frames,
_permit: Some(permit),
})
.is_ok()
});
if !submitted {
self.screenshot.reject(request, "Screenshot encoding queue is full");
}
+70 -176
View File
@@ -1,5 +1,3 @@
#![allow(irrefutable_let_patterns)]
mod backends;
mod config;
mod cursor;
@@ -26,6 +24,7 @@ mod portal_shortcuts;
mod presentation;
mod presentation_policy;
mod private_client;
mod process;
mod reload;
mod render;
#[cfg(feature = "resize-metrics")]
@@ -35,6 +34,8 @@ mod resume;
mod session_lock;
mod shell_control;
mod stacking;
#[cfg(test)]
mod startup_tests;
mod state;
mod theme;
mod wallpaper;
@@ -42,11 +43,10 @@ mod window_rules;
mod winit;
use std::error::Error;
use std::process::{Child, Command};
use std::time::Duration;
use std::{io, thread};
use std::io;
use calloop::signals::{Signal, Signals};
use process::{spawn_client, terminate_child, watch_client_exit};
use smithay::reexports::calloop::EventLoop;
use smithay::reexports::wayland_server::Display;
pub use state::{Ferese, RuntimeConfig};
@@ -64,13 +64,8 @@ fn main() -> Result<(), Box<dyn Error>> {
if args.len() != 2 {
return Err("usage: ferese --check-config PATH".into());
}
let source = std::fs::read_to_string(&args[1])?;
theme::prepare(
&source,
std::path::Path::new(&args[1])
.parent()
.unwrap_or(std::path::Path::new(".")),
)?;
check_config(std::path::Path::new(&args[1]))?;
println!("Configuration is valid");
return Ok(());
}
@@ -81,11 +76,10 @@ fn main() -> Result<(), Box<dyn Error>> {
.filter(|path| path.exists())
.map(|path| theme::read_source(&path))
.transpose()?;
let source = initial_source.as_deref().unwrap_or("");
let directory = config::config_path()
.and_then(|path| path.parent().map(ToOwned::to_owned))
.unwrap_or_else(|| std::path::PathBuf::from("."));
let (_, runtime, candidate) = theme::prepare(source, &directory)?;
let (_, runtime, candidate) = theme::prepare(initial_source.as_deref().unwrap_or(""), &directory)?;
let mut event_loop = EventLoop::try_new()?;
let signals = Signals::new(&[Signal::SIGINT, Signal::SIGTERM])?;
event_loop
@@ -109,56 +103,18 @@ fn main() -> Result<(), Box<dyn Error>> {
value
});
theme::init(&mut event_loop, &mut state, candidate)?;
resume::init(&mut event_loop)?;
let _resume_monitor = resume::init(&mut event_loop)?;
idle_inhibition::media::init(&mut event_loop)?;
overview::init_font_loader(&mut event_loop, &mut state)?;
backends::init(launch.backend, &mut event_loop, &mut state)?;
info!(socket = ?state.socket_name, backend = ?launch.backend, "Ferese is accepting Wayland clients");
let mut child = spawn_client(&mut state, launch.client, launch.client_capabilities);
let child = spawn_client(&mut state, launch.client, launch.client_capabilities)
.map(|child| watch_client_exit(&state.loop_handle, child));
let runner = daemon::Runner::start(&mut state);
state.daemons = Some(runner.clone());
let reload_handle = event_loop.handle();
let result = event_loop.run(None, &mut state, |state| {
state.poll_theme();
// The decoder wakes the loop after publishing its result, including
// when no output has a pending frame.
let wallpaper_changed = state.wallpaper.poll();
// All input/Wayland callbacks have returned, releasing seat locks.
if state.focus_cycle.is_some() && (state.session_lock.active() || state.input_capture.captures(1)) {
state.cancel_focus_cycle();
}
// Coalesce cursor changes and redraw here, never inside cursor_image.
if state.input_capture.restore_focus {
state.restore_input_capture_focus();
}
state.apply_unlocked_pointer_hint();
let wallpaper_retry = state.wallpaper.take_retry_wakeup();
let cursor_changed = std::mem::take(&mut state.cursor_redraw_pending);
let output_redraws = std::mem::take(&mut state.output_redraw_pending);
if wallpaper_changed || wallpaper_retry {
backends::direct::render_all(state);
} else {
if cursor_changed {
backends::direct::render_cursor(state);
}
if !output_redraws.is_empty() {
backends::direct::render_on(state, &output_redraws);
}
}
if let Err(error) = state
.wallpaper
.arm_retry_timer(&reload_handle, std::time::Instant::now())
{
warn!(%error, "could not schedule wallpaper upload retry");
}
// Registry/sync/configure replies must not depend on a submitted
// frame: at startup clients need these before they can draw anything.
// This also keeps idle DRM sessions responsive when there is no damage.
if let Err(error) = state.display_handle.flush_clients() {
tracing::warn!(%error, "failed to flush Wayland clients");
}
});
let result = event_loop.run(None, &mut state, after_dispatch);
if let Some(backend) = state.direct_backend.as_ref() {
backend.dump_scheduling_metrics();
@@ -168,8 +124,10 @@ fn main() -> Result<(), Box<dyn Error>> {
state.resize_metrics.dump();
runner.borrow_mut().stop();
if let Some(child) = &mut child {
terminate_child(child);
if let Some(child) = child
&& let Some(mut child) = child.borrow_mut().take()
{
terminate_child(&mut child);
}
result.map_err(Into::into)
@@ -181,130 +139,66 @@ fn init_logging() {
tracing_subscriber::fmt().with_env_filter(filter).init();
}
fn spawn_client(
state: &mut Ferese,
mut args: Vec<std::ffi::OsString>,
capabilities: private_client::ClientCapabilities,
) -> Option<Child> {
if args.is_empty() {
info!("no client requested; pass one after `--`, for example `-- foot`");
return None;
}
let program = args.remove(0);
fn check_config(path: &std::path::Path) -> Result<(), String> {
let source = theme::read_source(path)?;
theme::prepare(&source, path.parent().unwrap_or(std::path::Path::new(".")))?;
Ok(())
}
let mut command = Command::new(&program);
command.args(args);
// Wallpaper decoding and drivers may already have worker threads. Set
// the child's display explicitly instead of mutating the process env.
command.env("WAYLAND_DISPLAY", &state.socket_name);
command.env_remove("WAYLAND_SOCKET");
command.env_remove("FERESE_SHELL_CONTROL_SOCKET");
command.env("FERESE_COMPOSITOR_WALLPAPER", "1");
let private_connection = if capabilities.is_empty() {
None
#[derive(Debug)]
enum RedrawRequest<'a> {
Global,
Cursor,
Outputs(&'a [smithay::output::Output]),
}
fn after_dispatch(state: &mut Ferese) {
after_dispatch_with_redraw(state, |state, request| match request {
RedrawRequest::Global => backends::direct::render_all(state),
RedrawRequest::Cursor => backends::direct::render_cursor(state),
RedrawRequest::Outputs(outputs) => backends::direct::render_on(state, outputs),
});
}
fn after_dispatch_with_redraw(state: &mut Ferese, mut redraw: impl FnMut(&mut Ferese, RedrawRequest<'_>)) {
state.poll_theme();
// The decoder wakes the loop after publishing its result, including
// when no output has a pending frame.
let wallpaper_changed = state.wallpaper.poll();
// All input/Wayland callbacks have returned, releasing seat locks.
if state.focus_cycle.is_some() && (state.session_lock.active() || state.input_capture.captures(1)) {
state.cancel_focus_cycle();
}
if state.input_capture.restore_focus {
state.restore_input_capture_focus();
}
state.apply_unlocked_pointer_hint();
let wallpaper_retry = state.wallpaper.take_retry_wakeup();
let cursor_changed = std::mem::take(&mut state.cursor_redraw_pending);
let output_redraws = std::mem::take(&mut state.output_redraw_pending);
if wallpaper_changed || wallpaper_retry {
redraw(state, RedrawRequest::Global);
} else {
match private_client::prepare_command(state, &mut command, capabilities) {
Ok(connection) => Some(connection),
Err(error) => {
warn!(program = ?program, %error, "failed to create private Wayland connection");
return None;
}
if cursor_changed {
redraw(state, RedrawRequest::Cursor);
}
};
let child = match command.spawn() {
Ok(child) => {
info!(program = ?program, pid = child.id(), "spawned Wayland client");
Some(child)
if !output_redraws.is_empty() {
redraw(state, RedrawRequest::Outputs(&output_redraws));
}
Err(error) => {
warn!(program = ?program, %error, "failed to spawn Wayland client");
None
}
};
drop(private_connection);
child
}
fn terminate_child(child: &mut Child) {
if child.try_wait().ok().flatten().is_some() {
return;
}
// SAFETY: the process ID comes from the live Child owned by Ferese.
unsafe {
libc::kill(child.id() as i32, libc::SIGTERM);
if let Err(error) = state
.wallpaper
.arm_retry_timer(&state.loop_handle, std::time::Instant::now())
{
warn!(%error, "could not schedule wallpaper upload retry");
}
for _ in 0..20 {
if child.try_wait().ok().flatten().is_some() {
return;
}
thread::sleep(Duration::from_millis(10));
}
let _ = child.kill();
let _ = child.wait();
}
#[cfg(test)]
mod startup_tests {
use std::io::{Read, Write};
use std::os::unix::net::UnixStream;
use std::sync::Arc;
use super::*;
#[test]
fn deferred_cursor_redraw_runs_after_input_lock_is_released() {
use std::sync::Mutex;
let mut event_loop = EventLoop::<bool>::try_new().unwrap();
let pointer_lock = Arc::new(Mutex::new(()));
let callback_lock = pointer_lock.clone();
event_loop.handle().insert_idle(move |pending| {
// Mirrors Smithay's cursor_image callback during pointer motion.
let _guard = callback_lock.lock().unwrap();
*pending = true;
*pending = true; // Multiple cursor changes must coalesce.
assert!(callback_lock.try_lock().is_err());
});
let signal = event_loop.get_signal();
let mut pending = false;
let mut redraws = 0;
event_loop
.run(Some(Duration::ZERO), &mut pending, |pending| {
if std::mem::take(pending) {
// Rendering reads the pointer location at this point.
let _guard = pointer_lock.try_lock().expect("input lock still held");
redraws += 1;
}
assert!(!std::mem::take(pending));
signal.stop();
})
.unwrap();
assert_eq!(redraws, 1);
}
#[test]
fn wayland_roundtrip_completes_without_a_rendered_frame() {
let mut display = Display::<()>::new().unwrap();
let (server, mut client) = UnixStream::pair().unwrap();
display
.handle()
.insert_client(server, Arc::new(state::ClientState::default()))
.unwrap();
// wl_display.sync(new_id=2), using native-endian Wayland wire format.
for word in [1u32, 12u32 << 16, 2u32] {
client.write_all(&word.to_ne_bytes()).unwrap();
}
display.dispatch_clients(&mut ()).unwrap();
display.handle().flush_clients().unwrap();
client.set_read_timeout(Some(Duration::from_secs(1))).unwrap();
let mut reply = [0u8; 12];
client.read_exact(&mut reply).unwrap();
assert_eq!(u32::from_ne_bytes(reply[0..4].try_into().unwrap()), 2);
assert_eq!(u32::from_ne_bytes(reply[4..8].try_into().unwrap()) & 0xffff, 0);
// Clients need registry/sync/configure replies even when no frame is drawn.
if let Err(error) = state.display_handle.flush_clients() {
warn!(%error, "failed to flush Wayland clients");
}
}
+172
View File
@@ -0,0 +1,172 @@
use std::cell::RefCell;
use std::ffi::OsStr;
use std::io;
use std::os::fd::{FromRawFd, OwnedFd};
use std::os::unix::process::CommandExt;
use std::process::{Child, Command};
use std::rc::Rc;
use std::time::Duration;
use calloop::generic::Generic;
use calloop::timer::{TimeoutAction, Timer};
use calloop::{Interest, LoopHandle, Mode, PostAction};
use tracing::{info, warn};
use crate::Ferese;
use crate::private_client::{self, ClientCapabilities};
pub(crate) fn command(program: impl AsRef<OsStr>) -> Command {
let mut command = Command::new(program);
// Calloop blocks these on the compositor thread; fork/exec inherits that
// mask. Leave unrelated signal masks alone, including those of the launcher.
// SAFETY: only async-signal-safe signal-set operations run between fork and exec.
unsafe {
command.pre_exec(|| {
let mut mask = std::mem::zeroed();
libc::sigemptyset(&mut mask);
libc::sigaddset(&mut mask, libc::SIGINT);
libc::sigaddset(&mut mask, libc::SIGTERM);
if libc::sigprocmask(libc::SIG_UNBLOCK, &mask, std::ptr::null_mut()) == -1 {
return Err(io::Error::last_os_error());
}
Ok(())
});
}
command
}
pub(crate) fn spawn_client<S: AsRef<OsStr>>(
state: &mut Ferese,
args: impl IntoIterator<Item = S>,
capabilities: ClientCapabilities,
) -> Option<Child> {
let mut args = args.into_iter();
let Some(program) = args.next() else {
info!("no client requested; pass one after `--`, for example `-- foot`");
return None;
};
let program = program.as_ref();
let mut command = command(program);
command.args(args);
// Workers may already exist. Change only this child's environment.
command.env("WAYLAND_DISPLAY", &state.socket_name);
command.env_remove("WAYLAND_SOCKET");
command.env_remove("FERESE_SHELL_CONTROL_SOCKET");
command.env("FERESE_COMPOSITOR_WALLPAPER", "1");
let private_connection = if capabilities.is_empty() {
None
} else {
match private_client::prepare_command(state, &mut command, capabilities) {
Ok(connection) => Some(connection),
Err(error) => {
warn!(program = ?program, %error, "failed to create private Wayland connection");
return None;
}
}
};
let child = match command.spawn() {
Ok(child) => {
info!(program = ?program, pid = child.id(), "spawned Wayland client");
Some(child)
}
Err(error) => {
warn!(program = ?program, %error, "failed to spawn Wayland client");
None
}
};
drop(private_connection);
child
}
pub(crate) fn pidfd(pid: u32) -> io::Result<OwnedFd> {
// SAFETY: pidfd_open has no pointer arguments and returns a new owned fd.
let fd = unsafe { libc::syscall(libc::SYS_pidfd_open, pid, 0) };
if fd < 0 {
return Err(io::Error::last_os_error());
}
// SAFETY: a successful syscall returned a unique valid descriptor.
Ok(unsafe { OwnedFd::from_raw_fd(fd as i32) })
}
fn reap(child: &RefCell<Option<Child>>) -> bool {
let mut child = child.borrow_mut();
let Some(process) = child.as_mut() else {
return true;
};
match process.try_wait() {
Ok(Some(status)) => {
info!(pid = process.id(), %status, "Wayland client exited");
*child = None;
true
}
Ok(None) => false,
Err(error) => {
warn!(%error, "cannot reap Wayland client");
false
}
}
}
pub(crate) fn watch_client_exit(handle: &LoopHandle<'static, Ferese>, child: Child) -> Rc<RefCell<Option<Child>>> {
let pid = child.id();
let owner = Rc::new(RefCell::new(Some(child)));
let watched = owner.clone();
let result = pidfd(pid).and_then(|fd| {
handle
.insert_source(Generic::new(fd, Interest::READ, Mode::Level), move |_, _, _| {
Ok(if reap(&watched) {
PostAction::Remove
} else {
PostAction::Continue
})
})
.map_err(io::Error::other)
});
if let Err(error) = result {
warn!(%error, pid, "could not watch client exit with pidfd; polling instead");
let watched = owner.clone();
if let Err(error) = handle.insert_source(Timer::from_duration(Duration::from_millis(250)), move |_, _, _| {
if reap(&watched) {
TimeoutAction::Drop
} else {
TimeoutAction::ToDuration(Duration::from_millis(250))
}
}) {
warn!(%error, pid, "could not schedule client exit polling; stopping child");
if let Some(mut child) = owner.borrow_mut().take() {
terminate_child(&mut child);
}
}
}
owner
}
pub(crate) fn terminate_child(child: &mut Child) {
if child.try_wait().ok().flatten().is_some() {
return;
}
// SAFETY: the process ID comes from the live Child owned by Ferese.
unsafe {
libc::kill(child.id() as i32, libc::SIGTERM);
}
for _ in 0..20 {
if child.try_wait().ok().flatten().is_some() {
return;
}
std::thread::sleep(Duration::from_millis(10));
}
let _ = child.kill();
let _ = child.wait();
}
+236 -36
View File
@@ -1,57 +1,257 @@
//! System wake is shared by hardware reconciliation and time-dependent UI state.
//! System wake and listener recovery share reconciliation, but not idle activity.
use std::error::Error;
use std::sync::{Arc, Condvar, Mutex};
use std::task::{Poll, Waker};
use std::thread::JoinHandle;
use std::time::Duration;
use calloop::EventLoop;
use calloop::channel::{Event, channel};
use calloop::{EventLoop, LoopHandle, RegistrationToken};
use futures_lite::{StreamExt, future};
use crate::Ferese;
pub(crate) fn init(event_loop: &mut EventLoop<Ferese>) -> Result<(), Box<dyn Error>> {
const RETRY_DELAY: Duration = Duration::from_secs(5);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Refresh {
Resumed,
Reconcile,
}
#[derive(Debug, PartialEq, Eq)]
enum MonitorExit {
Stop,
Reconnect,
}
#[derive(Default)]
struct StopState {
requested: bool,
waker: Option<Waker>,
}
#[derive(Default)]
struct Cancellation {
state: Mutex<StopState>,
changed: Condvar,
}
impl Cancellation {
fn cancel(&self) {
let mut state = self.state.lock().unwrap();
state.requested = true;
let waker = state.waker.take();
self.changed.notify_all();
drop(state);
if let Some(waker) = waker {
waker.wake();
}
}
async fn cancelled(&self) {
future::poll_fn(|context| {
let mut state = self.state.lock().unwrap();
if state.requested {
return Poll::Ready(());
}
state.waker = Some(context.waker().clone());
Poll::Pending
})
.await;
}
fn retry(&self, delay: Duration) -> bool {
let (state, _) = self
.changed
.wait_timeout_while(self.state.lock().unwrap(), delay, |state| !state.requested)
.unwrap();
!state.requested
}
}
pub(crate) struct Monitor {
stop: Arc<Cancellation>,
worker: Option<JoinHandle<()>>,
source: Option<(LoopHandle<'static, Ferese>, RegistrationToken)>,
}
impl Drop for Monitor {
fn drop(&mut self) {
self.stop.cancel();
if let Some((handle, token)) = self.source.take() {
handle.remove(token);
}
// Cancellation wakes both the signal wait and retry delay. Do not join
// a DBus worker on the compositor thread during shutdown.
self.worker.take();
}
}
fn refresh(state: &mut Ferese, event: Refresh) {
match event {
Refresh::Resumed => crate::backends::direct::system_resumed(state),
Refresh::Reconcile => crate::backends::direct::reconcile_after_monitor_recovery(state),
}
state.refresh_theme();
}
pub(crate) fn init(event_loop: &mut EventLoop<'static, Ferese>) -> Result<Monitor, Box<dyn Error>> {
let (sender, receiver) = channel();
event_loop.handle().insert_source(receiver, |event, _, state| {
if let Event::Msg(()) = event {
crate::backends::direct::system_resumed(state);
state.refresh_theme();
let handle = event_loop.handle();
let token = handle.insert_source(receiver, |event, _, state| {
if let Event::Msg(event) = event {
refresh(state, event);
}
})?;
std::thread::Builder::new()
match start(zbus::connection::Builder::system, RETRY_DELAY, move |event| {
sender.send(event).is_ok()
}) {
Ok(mut monitor) => {
monitor.source = Some((handle, token));
Ok(monitor)
}
Err(error) => {
handle.remove(token);
Err(error.into())
}
}
}
#[derive(Default)]
struct Recovery {
reconnecting: bool,
warned: bool,
}
impl Recovery {
fn unavailable(&mut self) -> bool {
self.reconnecting = true;
let warn = !self.warned;
self.warned = true;
warn
}
fn subscribed(&mut self) -> bool {
self.warned = false;
std::mem::take(&mut self.reconnecting)
}
}
fn start(
connect: impl Fn() -> zbus::Result<zbus::connection::Builder<'static>> + Send + 'static,
retry_delay: Duration,
mut send: impl FnMut(Refresh) -> bool + Send + 'static,
) -> std::io::Result<Monitor> {
let stop = Arc::new(Cancellation::default());
let worker_stop = stop.clone();
let worker = std::thread::Builder::new()
.name("ferese-system-resume".into())
.spawn(move || {
let mut warned = false;
let mut recovery = Recovery::default();
loop {
let monitor = || -> zbus::Result<bool> {
let connection = zbus::blocking::connection::Builder::system()?
.method_timeout(Duration::from_secs(2))
.build()?;
let proxy = zbus::blocking::Proxy::new(
&connection,
"org.freedesktop.login1",
"/org/freedesktop/login1",
"org.freedesktop.login1.Manager",
)?;
for message in proxy.receive_signal("PrepareForSleep")? {
let (sleeping,): (bool,) = message.body().deserialize()?;
if !sleeping && sender.send(()).is_err() {
return Ok(false);
let result = future::block_on(future::race(
async {
worker_stop.cancelled().await;
Ok(MonitorExit::Stop)
},
async {
let connection = connect()?.method_timeout(Duration::from_secs(2)).build().await?;
future::race(
async {
connection.closed().await;
Ok(MonitorExit::Reconnect)
},
listen(&connection, &mut recovery, &mut send),
)
.await
},
));
match result {
Ok(MonitorExit::Stop) => break,
Ok(MonitorExit::Reconnect) => {
if recovery.unavailable() {
tracing::warn!("system resume monitoring interrupted; retrying connection");
}
}
Ok(true)
};
match monitor() {
Ok(false) => break,
Ok(true) => warned = false,
Err(error) if !warned => {
tracing::warn!(%error, "system resume monitor unavailable; retrying connection");
warned = true;
Err(error) => {
if recovery.unavailable() {
tracing::warn!(%error, "system resume monitor unavailable; retrying connection");
}
}
Err(_) => {}
}
// Recovery only: a healthy listener blocks on DBus signals.
std::thread::sleep(Duration::from_secs(5));
if !worker_stop.retry(retry_delay) {
break;
}
}
})?;
Ok(())
Ok(Monitor {
stop,
worker: Some(worker),
source: None,
})
}
async fn listen(
connection: &zbus::Connection,
recovery: &mut Recovery,
send: &mut impl FnMut(Refresh) -> bool,
) -> zbus::Result<MonitorExit> {
let proxy = zbus::Proxy::new(
connection,
"org.freedesktop.login1",
"/org/freedesktop/login1",
"org.freedesktop.login1.Manager",
)
.await?;
let mut owners = proxy.receive_owner_changed().await?;
let mut signals = proxy.receive_signal("PrepareForSleep").await?;
// AddMatch succeeds even when logind is absent. Only call the listener
// recovered once the service is present, and retry if its owner changes.
connection
.call_method(
Some("org.freedesktop.DBus"),
"/org/freedesktop/DBus",
Some("org.freedesktop.DBus"),
"GetNameOwner",
&("org.freedesktop.login1",),
)
.await?;
// Subscribe first: a resume during reconciliation must remain queued.
if recovery.subscribed() {
tracing::debug!("system resume monitoring recovered; reconciling current state");
if !send(Refresh::Reconcile) {
return Ok(MonitorExit::Stop);
}
}
future::race(
async {
owners.next().await;
Ok(MonitorExit::Reconnect)
},
async {
while let Some(message) = signals.next().await {
let (sleeping,): (bool,) = message.body().deserialize()?;
if !sleeping && !send(Refresh::Resumed) {
return Ok(MonitorExit::Stop);
}
}
Ok(MonitorExit::Reconnect)
},
)
.await
}
#[cfg(test)]
mod tests;
+249
View File
@@ -0,0 +1,249 @@
use std::io::{BufRead, BufReader};
use std::process::{Child, Stdio};
use std::sync::mpsc::{self, Receiver};
use std::time::Instant;
use super::*;
#[test]
fn warning_suppression_resets_at_subscription_not_at_next_resume() {
let mut recovery = Recovery::default();
assert!(!recovery.subscribed(), "startup is not a recovery or a resume");
assert!(recovery.unavailable());
assert!(!recovery.unavailable(), "one warning per outage");
assert!(recovery.subscribed());
assert!(!recovery.subscribed(), "one reconciliation per recovery");
assert!(recovery.unavailable(), "another outage must warn even without a resume");
assert!(!recovery.unavailable());
}
fn stop_and_join(mut monitor: Monitor) {
let worker = monitor.worker.take().unwrap();
drop(monitor);
let deadline = Instant::now() + Duration::from_secs(1);
while !worker.is_finished() && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(2));
}
assert!(worker.is_finished(), "cancellation did not stop the worker promptly");
worker.join().unwrap();
}
#[test]
fn shutdown_interrupts_retry_delay() {
let (attempt, attempted) = mpsc::channel();
let monitor = start(
move || {
attempt.send(()).unwrap();
Err(zbus::Error::Failure("test connection failure".into()))
},
RETRY_DELAY,
|_| panic!("no subscription succeeded"),
)
.unwrap();
attempted.recv_timeout(Duration::from_secs(1)).unwrap();
stop_and_join(monitor);
assert!(
attempted.try_recv().is_err(),
"shutdown must not start another connection"
);
}
struct Bus {
process: Child,
address: String,
}
impl Bus {
fn new() -> Self {
let mut process = crate::process::command("dbus-daemon")
.args(["--session", "--nofork", "--print-address=1"])
.stdout(Stdio::piped())
.spawn()
.unwrap();
let mut address = String::new();
BufReader::new(process.stdout.take().unwrap())
.read_line(&mut address)
.unwrap();
Self {
process,
address: address.trim().to_owned(),
}
}
fn stop(&mut self) {
let _ = self.process.kill();
let _ = self.process.wait();
}
fn manager(&self) -> zbus::blocking::Connection {
struct Manager;
#[zbus::interface(name = "org.freedesktop.login1.Manager")]
impl Manager {
#[zbus(property)]
fn lid_closed(&self) -> bool {
false
}
}
zbus::blocking::connection::Builder::address(self.address.as_str())
.unwrap()
.name("org.freedesktop.login1")
.unwrap()
.serve_at("/org/freedesktop/login1", Manager)
.unwrap()
.build()
.unwrap()
}
}
impl Drop for Bus {
fn drop(&mut self) {
self.stop();
}
}
fn signal(manager: &zbus::blocking::Connection, sleeping: bool) {
manager
.emit_signal(
None::<&str>,
"/org/freedesktop/login1",
"org.freedesktop.login1.Manager",
"PrepareForSleep",
&(sleeping,),
)
.unwrap();
}
fn confirm_subscription(manager: &zbus::blocking::Connection, events: &Receiver<Refresh>) {
let deadline = Instant::now() + Duration::from_secs(2);
loop {
signal(manager, false);
if let Ok(event) = events.recv_timeout(Duration::from_millis(10)) {
assert_eq!(event, Refresh::Resumed);
break;
}
assert!(Instant::now() < deadline, "listener did not subscribe");
}
}
#[test]
fn reconnect_reconciles_a_resume_missed_during_the_outage() {
if !crate::startup_tests::private_runtime("resume::tests::reconnect_reconciles_a_resume_missed_during_the_outage") {
return;
}
let mut bus = Bus::new();
let manager = bus.manager();
let address = Arc::new(Mutex::new(bus.address.clone()));
let worker_address = address.clone();
let (sender, events) = mpsc::channel();
let (attempt, attempts) = mpsc::channel();
let monitor = start(
move || {
attempt.send(()).unwrap();
zbus::connection::Builder::address(worker_address.lock().unwrap().as_str())
},
Duration::from_millis(40),
move |event| sender.send(event).is_ok(),
)
.unwrap();
confirm_subscription(&manager, &events);
attempts.recv_timeout(Duration::from_secs(1)).unwrap();
*address.lock().unwrap() = "unix:path=/nonexistent/ferese-test-bus".into();
bus.stop();
attempts.recv_timeout(Duration::from_secs(1)).unwrap();
let replacement = Bus::new();
let manager = replacement.manager();
signal(&manager, false); // No listener can receive this resume.
*address.lock().unwrap() = replacement.address.clone();
assert_eq!(events.recv_timeout(Duration::from_secs(2)).unwrap(), Refresh::Reconcile);
assert!(events.recv_timeout(Duration::from_millis(80)).is_err());
signal(&manager, true);
assert!(events.recv_timeout(Duration::from_millis(40)).is_err());
signal(&manager, false);
assert_eq!(events.recv_timeout(Duration::from_secs(1)).unwrap(), Refresh::Resumed);
stop_and_join(monitor);
}
#[test]
fn shutdown_interrupts_a_healthy_signal_wait() {
if !crate::startup_tests::private_runtime("resume::tests::shutdown_interrupts_a_healthy_signal_wait") {
return;
}
let bus = Bus::new();
let manager = bus.manager();
let address = bus.address.clone();
let (sender, events) = mpsc::channel();
let monitor = start(
move || zbus::connection::Builder::address(address.as_str()),
RETRY_DELAY,
move |event| sender.send(event).is_ok(),
)
.unwrap();
confirm_subscription(&manager, &events);
stop_and_join(monitor);
}
#[test]
fn logind_replacement_reconciles_without_a_bus_disconnect() {
if !crate::startup_tests::private_runtime("resume::tests::logind_replacement_reconciles_without_a_bus_disconnect") {
return;
}
let bus = Bus::new();
let manager = bus.manager();
let address = bus.address.clone();
let (sender, events) = mpsc::channel();
let monitor = start(
move || zbus::connection::Builder::address(address.as_str()),
Duration::from_millis(40),
move |event| sender.send(event).is_ok(),
)
.unwrap();
confirm_subscription(&manager, &events);
manager.release_name("org.freedesktop.login1").unwrap();
assert!(
events.recv_timeout(Duration::from_millis(100)).is_err(),
"absent logind is not recovery"
);
let replacement = bus.manager();
assert_eq!(events.recv_timeout(Duration::from_secs(2)).unwrap(), Refresh::Reconcile);
signal(&replacement, false);
assert_eq!(events.recv_timeout(Duration::from_secs(1)).unwrap(), Refresh::Resumed);
stop_and_join(monitor);
}
#[test]
fn shutdown_interrupts_connection_authentication() {
if !crate::startup_tests::private_runtime("resume::tests::shutdown_interrupts_connection_authentication") {
return;
}
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("silent-bus");
let listener = std::os::unix::net::UnixListener::bind(&path).unwrap();
listener.set_nonblocking(true).unwrap();
let address = format!("unix:path={}", path.display());
let monitor = start(
move || zbus::connection::Builder::address(address.as_str()),
RETRY_DELAY,
|_| panic!("authentication cannot complete"),
)
.unwrap();
let deadline = Instant::now() + Duration::from_secs(1);
let _connection = loop {
match listener.accept() {
Ok((stream, _)) => break stream,
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
assert!(Instant::now() < deadline);
std::thread::sleep(Duration::from_millis(2));
}
Err(error) => panic!("{error}"),
}
};
stop_and_join(monitor);
}
+54 -361
View File
@@ -111,7 +111,7 @@ pub(crate) struct Lock {
pub(crate) idle_opacity: f32,
pub(crate) sleeping: bool,
pub(crate) idle_overlays: HashMap<Output, smithay::backend::renderer::element::solid::SolidColorBuffer>,
idle_timer: Option<RegistrationToken>,
idle_timer: Option<(RegistrationToken, Instant)>,
}
impl Lock {
@@ -130,6 +130,8 @@ impl Lock {
pub(crate) fn output_removed(&mut self, output: &Output) {
self.presented.remove(output);
self.idle_overlays.remove(output);
self.surfaces.remove(output);
self.backgrounds.remove(output);
}
fn activity(&mut self, now: Instant) -> bool {
@@ -167,7 +169,11 @@ impl Ferese {
crate::backends::direct::wake_locked_outputs(self);
self.cursor_redraw_pending = true;
}
self.rearm_lock_idle();
// Activity moves the deadline later. Keep the existing source and let
// it recheck idle_since when it fires. Sleeping outputs have no timer.
if self.session_lock.idle_timer.is_none() {
self.rearm_lock_idle();
}
}
pub(crate) fn refresh_lock_idle_policy(&mut self) {
@@ -175,32 +181,52 @@ impl Ferese {
self.rearm_lock_idle();
}
fn rearm_lock_idle(&mut self) {
if let Some(token) = self.session_lock.idle_timer.take() {
self.loop_handle.remove(token);
}
fn lock_idle_deadline(&self, now: Instant) -> Option<Instant> {
if !self.lock_outputs_protected() {
return;
return None;
}
let elapsed = self
.session_lock
.idle_since
.map_or(Duration::ZERO, |since| since.elapsed());
let Some(delay) = self
.lock_idle
.map_or(Duration::ZERO, |since| now.saturating_duration_since(since));
self.lock_idle
.next_deadline(elapsed, self.animation_duration(Duration::from_millis(500)))
else {
.map(|delay| now + delay)
}
fn rearm_lock_idle(&mut self) {
let deadline = self.lock_idle_deadline(Instant::now());
if let Some((_, armed)) = self.session_lock.idle_timer
&& deadline.is_some_and(|deadline| armed <= deadline)
{
return;
}
if let Some((token, _)) = self.session_lock.idle_timer.take() {
self.loop_handle.remove(token);
}
let Some(deadline) = deadline else {
return;
};
match self
.loop_handle
.insert_source(Timer::from_duration(delay), |_, _, state| {
state.session_lock.idle_timer = None;
state.refresh_lock_idle_policy();
TimeoutAction::Drop
.insert_source(Timer::from_deadline(deadline), |_, _, state| {
let now = Instant::now();
state.update_lock_idle(now);
if let Some(deadline) = state.lock_idle_deadline(now) {
if let Some((_, armed)) = state.session_lock.idle_timer.as_mut() {
*armed = deadline;
}
TimeoutAction::ToInstant(deadline)
} else {
state.session_lock.idle_timer = None;
TimeoutAction::Drop
}
}) {
Ok(token) => self.session_lock.idle_timer = Some(token),
Ok(token) => self.session_lock.idle_timer = Some((token, deadline)),
Err(error) => tracing::warn!(%error, "could not arm lock idle deadline"),
}
}
@@ -251,8 +277,17 @@ impl Ferese {
let surface = self
.focused_output()
.and_then(|output| self.session_lock.surfaces.get(output))
.or_else(|| self.session_lock.surfaces.values().find(|surface| surface.alive()))
.filter(|surface| surface.alive())
.or_else(|| {
// Stable output IDs keep fallback focus independent of HashMap order.
self.session_lock
.surfaces
.iter()
.filter(|(_, surface)| surface.alive())
.filter_map(|(output, surface)| self.output_id(output).map(|id| (id, surface)))
.min_by_key(|(id, _)| *id)
.map(|(_, surface)| surface)
})
.map(|surface| surface.wl_surface().clone());
self.seat
.get_keyboard()
@@ -378,7 +413,7 @@ impl SessionLockHandler for Ferese {
}
fn unlock(&mut self) {
if let Some(timer) = self.session_lock.idle_timer.take() {
if let Some((timer, _)) = self.session_lock.idle_timer.take() {
self.loop_handle.remove(timer);
}
let sleeping = self.session_lock.sleeping;
@@ -456,346 +491,4 @@ smithay::reexports::wayland_server::delegate_dispatch!(Ferese: [ExtSessionLockMa
smithay::reexports::wayland_server::delegate_dispatch!(Ferese: [ExtSessionLockSurfaceV1: smithay::wayland::session_lock::ExtLockSurfaceUserData] => SessionLockManagerState);
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[ignore = "requires a private XDG_RUNTIME_DIR and permission to bind test sockets"]
fn zero_outputs_confirm_and_dead_owner_can_be_replaced_without_unlocking() {
use std::io::{Read, Write};
use std::os::unix::net::UnixStream;
use std::sync::Arc;
use smithay::reexports::calloop::EventLoop;
use smithay::reexports::wayland_server::Display;
let runtime = std::path::PathBuf::from(std::env::var_os("XDG_RUNTIME_DIR").unwrap());
assert!(runtime.starts_with(std::env::temp_dir()));
let mut event_loop = EventLoop::try_new().unwrap();
let config = crate::config::Config::default().runtime_config().unwrap();
let mut state = Ferese::new(&mut event_loop, Display::new().unwrap(), config).unwrap();
let capture_epoch = state.capture_epoch;
let acquire = |state: &mut Ferese| {
let (server, mut wire) = UnixStream::pair().unwrap();
let client = state
.display_handle
.insert_client(server, Arc::new(crate::state::ClientState::default()))
.unwrap();
let manager = client
.create_resource::<ExtSessionLockManagerV1, (), Ferese>(&state.display_handle, 1, ())
.unwrap();
// ext_session_lock_manager_v1.lock(new_id=2).
for word in [manager.id().protocol_id(), (12u32 << 16) | 1, 2] {
wire.write_all(&word.to_ne_bytes()).unwrap();
}
wire
};
let dispatch = |event_loop: &mut EventLoop<'static, Ferese>, state: &mut Ferese| {
event_loop.dispatch(Duration::from_millis(20), state).unwrap();
state.display_handle.flush_clients().unwrap();
};
let mut first = acquire(&mut state);
dispatch(&mut event_loop, &mut state);
assert_ne!(
state.capture_epoch, capture_epoch,
"lock revokes deferred window snapshots"
);
assert!(
matches!(state.session_lock.lifecycle, Lifecycle::Locked(_)),
"no outputs must confirm immediately"
);
first.set_read_timeout(Some(Duration::from_secs(1))).unwrap();
let mut event = [0u8; 8];
first.read_exact(&mut event).unwrap();
assert_eq!(u32::from_ne_bytes(event[..4].try_into().unwrap()), 2);
assert_eq!(
u32::from_ne_bytes(event[4..].try_into().unwrap()),
8u32 << 16,
"protocol locked event"
);
let owner = state.session_lock.lifecycle.owner().cloned();
let mut competitor = acquire(&mut state);
dispatch(&mut event_loop, &mut state);
assert_eq!(
state.session_lock.lifecycle.owner(),
owner.as_ref(),
"a live owner cannot be replaced"
);
// A rejected client must not unlock the confirmed owner.
for word in [2u32, (8u32 << 16) | 2] {
competitor.write_all(&word.to_ne_bytes()).unwrap();
}
dispatch(&mut event_loop, &mut state);
assert_eq!(state.session_lock.lifecycle.owner(), owner.as_ref());
drop(competitor);
drop(first);
dispatch(&mut event_loop, &mut state);
assert!(matches!(state.session_lock.lifecycle, Lifecycle::Orphaned));
assert!(state.session_lock.active());
let replacement = acquire(&mut state);
dispatch(&mut event_loop, &mut state);
assert!(matches!(state.session_lock.lifecycle, Lifecycle::Locked(_)));
assert_ne!(state.session_lock.lifecycle.owner(), owner.as_ref());
drop(replacement);
dispatch(&mut event_loop, &mut state);
assert!(state.session_lock.active());
let output = Output::new(
"last-output".into(),
smithay::output::PhysicalProperties {
size: (0, 0).into(),
subpixel: smithay::output::Subpixel::Unknown,
make: "test".into(),
model: "test".into(),
},
);
output.change_current_state(
Some(smithay::output::Mode {
size: (800, 600).into(),
refresh: 60_000,
}),
None,
None,
None,
);
state.space.map_output(&output, (0, 0));
state.register_output(&output, "last-output".into());
let pending = acquire(&mut state);
dispatch(&mut event_loop, &mut state);
assert!(
matches!(state.session_lock.lifecycle, Lifecycle::Acquiring(_)),
"a newly added output requires a protected frame"
);
drop(pending);
dispatch(&mut event_loop, &mut state);
assert!(matches!(state.session_lock.lifecycle, Lifecycle::Orphaned));
let replacement = acquire(&mut state);
dispatch(&mut event_loop, &mut state);
assert!(matches!(state.session_lock.lifecycle, Lifecycle::Acquiring(_)));
state.unregister_output(&output);
assert!(
matches!(state.session_lock.lifecycle, Lifecycle::Locked(_)),
"removing the last pending output must confirm"
);
drop(replacement);
dispatch(&mut event_loop, &mut state);
state.space.map_output(&output, (0, 0));
state.session_lock.output_added(&output);
let mut pending = acquire(&mut state);
dispatch(&mut event_loop, &mut state);
assert!(matches!(state.session_lock.lifecycle, Lifecycle::Acquiring(_)));
assert!(state.session_lock.idle_timer.is_none());
// Remove the final display without delivering a frame or confirming during removal.
state.space.unmap_output(&output);
state.session_lock.output_removed(&output);
assert!(matches!(state.session_lock.lifecycle, Lifecycle::Acquiring(_)));
state.refresh_lock_outputs();
assert!(matches!(state.session_lock.lifecycle, Lifecycle::Locked(_)));
assert!(state.session_lock.idle_timer.is_some());
state.display_handle.flush_clients().unwrap();
pending.set_read_timeout(Some(Duration::from_secs(1))).unwrap();
pending.read_exact(&mut event).unwrap();
assert_eq!(u32::from_ne_bytes(event[..4].try_into().unwrap()), 2);
assert_eq!(u32::from_ne_bytes(event[4..].try_into().unwrap()), 8u32 << 16);
}
#[test]
fn dim_fade_respects_speed_without_changing_idle_deadlines() {
let policy = IdleSettings::default();
for (duration, half_at) in [(250, 30_125), (1000, 30_500)] {
let fade = Duration::from_millis(duration);
assert_eq!(policy.appearance(Duration::from_secs(29), fade), (0.0, false));
let halfway = policy.appearance(Duration::from_millis(half_at), fade);
assert!((halfway.0 - 0.325).abs() < 0.001 && !halfway.1);
assert_eq!(
policy.next_deadline(Duration::from_millis(half_at), fade),
Some(Duration::from_millis(16))
);
assert_eq!(policy.appearance(Duration::from_secs(120), fade), (1.0, true));
}
assert_eq!(
policy.appearance(Duration::from_secs(30), Duration::ZERO),
(0.65, false)
);
assert_eq!(policy.appearance(Duration::from_secs(120), Duration::ZERO), (1.0, true));
}
#[test]
fn idle_policy_fades_then_sleeps_and_activity_resets_it() {
let policy = IdleSettings::default();
assert_eq!(
policy.appearance(Duration::from_secs(29), Duration::from_millis(500)),
(0.0, false)
);
let halfway = policy.appearance(Duration::from_millis(30_250), Duration::from_millis(500));
assert!((halfway.0 - 0.325).abs() < 0.001 && !halfway.1);
assert_eq!(
policy.appearance(Duration::from_secs(31), Duration::from_millis(500)),
(0.65, false)
);
assert_eq!(
policy.appearance(Duration::from_secs(120), Duration::from_millis(500)),
(1.0, true)
);
assert_eq!(
policy.appearance(Duration::ZERO, Duration::from_millis(500)),
(0.0, false)
);
assert_eq!(
policy.next_deadline(Duration::from_millis(29_998), Duration::from_millis(500)),
Some(Duration::from_millis(2))
);
assert_eq!(
policy.next_deadline(Duration::from_secs(30), Duration::from_millis(500)),
Some(Duration::from_millis(16))
);
assert_eq!(
policy.next_deadline(Duration::from_secs(30), Duration::ZERO),
Some(Duration::from_secs(90))
);
assert_eq!(policy.next_deadline(Duration::from_secs(120), Duration::ZERO), None);
assert_eq!(
IdleSettings {
dim_after_seconds: 0,
sleep_after_seconds: 0
}
.next_deadline(Duration::ZERO, Duration::ZERO),
None
);
}
#[test]
fn mirror_must_present_protected_frame_before_lock_is_ready() {
let make_output = |name: &str| {
Output::new(
name.into(),
smithay::output::PhysicalProperties {
size: (0, 0).into(),
subpixel: smithay::output::Subpixel::Unknown,
make: "test".into(),
model: "test".into(),
},
)
};
let source = make_output("source");
let mirror = make_output("mirror");
let mut lock = Lock {
lifecycle: Lifecycle::Orphaned,
..Lock::default()
};
lock.presented.insert(source.clone());
let displays = [&source, &mirror];
assert!(
!lock.ready_for_idle(displays.into_iter()),
"failed mirror rendering must block confirmation"
);
lock.presented.insert(mirror.clone());
assert!(lock.ready_for_idle(displays.into_iter()));
lock.output_added(&mirror);
assert!(
!lock.ready_for_idle(displays.into_iter()),
"reconfiguration invalidates physical protection"
);
lock.output_removed(&mirror);
assert!(lock.ready_for_idle(std::iter::once(&source)));
}
#[test]
fn protected_fallback_frames_allow_idle_after_early_locker_crash() {
let output = Output::new(
"test".into(),
smithay::output::PhysicalProperties {
size: (0, 0).into(),
subpixel: smithay::output::Subpixel::Unknown,
make: "test".into(),
model: "test".into(),
},
);
let mut lock = Lock {
lifecycle: Lifecycle::Orphaned,
..Lock::default()
};
assert!(!lock.ready_for_idle(std::iter::once(&output)));
lock.presented.insert(output.clone());
assert!(lock.ready_for_idle(std::iter::once(&output)));
assert!(matches!(lock.lifecycle, Lifecycle::Orphaned));
lock.output_added(&output);
assert!(!lock.ready_for_idle(std::iter::once(&output)));
assert!(matches!(lock.lifecycle, Lifecycle::Orphaned));
}
#[test]
fn input_wakes_a_sleeping_orphan_without_unlocking() {
let mut lock = Lock {
lifecycle: Lifecycle::Orphaned,
sleeping: true,
idle_opacity: 1.0,
..Lock::default()
};
let now = Instant::now();
assert!(lock.activity(now));
assert!(lock.active() && matches!(lock.lifecycle, Lifecycle::Orphaned));
assert!(!lock.sleeping);
assert_eq!(lock.idle_opacity, 0.0);
assert_eq!(lock.idle_since, Some(now));
assert!(!lock.activity(now));
let mut unlocked = Lock::default();
assert!(!unlocked.activity(now));
assert!(unlocked.idle_since.is_none());
}
#[test]
fn idle_disabled_options_are_independent() {
assert_eq!(
IdleSettings {
dim_after_seconds: 0,
sleep_after_seconds: 0
}
.appearance(Duration::from_secs(9999), Duration::from_millis(500)),
(0.0, false)
);
assert_eq!(
IdleSettings {
dim_after_seconds: 0,
sleep_after_seconds: 10
}
.appearance(Duration::from_secs(10), Duration::from_millis(500)),
(1.0, true)
);
assert_eq!(
IdleSettings {
dim_after_seconds: 1,
sleep_after_seconds: 0
}
.appearance(Duration::from_secs(9999), Duration::from_millis(500)),
(0.65, false)
);
assert!(
IdleSettings {
dim_after_seconds: 86401,
sleep_after_seconds: 0
}
.validate()
.is_err()
);
assert!(crate::config::Config::parse_source("lock-screen { dim-after-seconds -1; }").is_err());
assert!(
crate::config::Config::parse_source("lock-screen { sleep-after-seconds 90000; }")
.unwrap()
.runtime_config()
.is_err()
);
}
}
mod tests;
+628
View File
@@ -0,0 +1,628 @@
use super::*;
#[test]
fn dim_fade_respects_speed_without_changing_idle_deadlines() {
let policy = IdleSettings::default();
for (duration, half_at) in [(250, 30_125), (1000, 30_500)] {
let fade = Duration::from_millis(duration);
assert_eq!(policy.appearance(Duration::from_secs(29), fade), (0.0, false));
let halfway = policy.appearance(Duration::from_millis(half_at), fade);
assert!((halfway.0 - 0.325).abs() < 0.001 && !halfway.1);
assert_eq!(
policy.next_deadline(Duration::from_millis(half_at), fade),
Some(Duration::from_millis(16))
);
assert_eq!(policy.appearance(Duration::from_secs(120), fade), (1.0, true));
}
assert_eq!(
policy.appearance(Duration::from_secs(30), Duration::ZERO),
(0.65, false)
);
assert_eq!(policy.appearance(Duration::from_secs(120), Duration::ZERO), (1.0, true));
}
#[test]
fn idle_policy_fades_then_sleeps_and_activity_resets_it() {
let policy = IdleSettings::default();
assert_eq!(
policy.appearance(Duration::from_secs(29), Duration::from_millis(500)),
(0.0, false)
);
let halfway = policy.appearance(Duration::from_millis(30_250), Duration::from_millis(500));
assert!((halfway.0 - 0.325).abs() < 0.001 && !halfway.1);
assert_eq!(
policy.appearance(Duration::from_secs(31), Duration::from_millis(500)),
(0.65, false)
);
assert_eq!(
policy.appearance(Duration::from_secs(120), Duration::from_millis(500)),
(1.0, true)
);
assert_eq!(
policy.appearance(Duration::ZERO, Duration::from_millis(500)),
(0.0, false)
);
assert_eq!(
policy.next_deadline(Duration::from_millis(29_998), Duration::from_millis(500)),
Some(Duration::from_millis(2))
);
assert_eq!(
policy.next_deadline(Duration::from_secs(30), Duration::from_millis(500)),
Some(Duration::from_millis(16))
);
assert_eq!(
policy.next_deadline(Duration::from_secs(30), Duration::ZERO),
Some(Duration::from_secs(90))
);
assert_eq!(policy.next_deadline(Duration::from_secs(120), Duration::ZERO), None);
assert_eq!(
IdleSettings {
dim_after_seconds: 0,
sleep_after_seconds: 0
}
.next_deadline(Duration::ZERO, Duration::ZERO),
None
);
}
#[test]
fn mirror_must_present_protected_frame_before_lock_is_ready() {
let make_output = |name: &str| {
Output::new(
name.into(),
smithay::output::PhysicalProperties {
size: (0, 0).into(),
subpixel: smithay::output::Subpixel::Unknown,
make: "test".into(),
model: "test".into(),
},
)
};
let source = make_output("source");
let mirror = make_output("mirror");
let mut lock = Lock {
lifecycle: Lifecycle::Orphaned,
..Lock::default()
};
lock.presented.insert(source.clone());
let displays = [&source, &mirror];
assert!(
!lock.ready_for_idle(displays.into_iter()),
"failed mirror rendering must block confirmation"
);
lock.presented.insert(mirror.clone());
assert!(lock.ready_for_idle(displays.into_iter()));
lock.output_added(&mirror);
assert!(
!lock.ready_for_idle(displays.into_iter()),
"reconfiguration invalidates physical protection"
);
lock.output_removed(&mirror);
assert!(lock.ready_for_idle(std::iter::once(&source)));
}
#[test]
fn protected_fallback_frames_allow_idle_after_early_locker_crash() {
let output = Output::new(
"test".into(),
smithay::output::PhysicalProperties {
size: (0, 0).into(),
subpixel: smithay::output::Subpixel::Unknown,
make: "test".into(),
model: "test".into(),
},
);
let mut lock = Lock {
lifecycle: Lifecycle::Orphaned,
..Lock::default()
};
assert!(!lock.ready_for_idle(std::iter::once(&output)));
lock.presented.insert(output.clone());
assert!(lock.ready_for_idle(std::iter::once(&output)));
assert!(matches!(lock.lifecycle, Lifecycle::Orphaned));
lock.output_added(&output);
assert!(!lock.ready_for_idle(std::iter::once(&output)));
assert!(matches!(lock.lifecycle, Lifecycle::Orphaned));
}
#[test]
fn input_wakes_a_sleeping_orphan_without_unlocking() {
let mut lock = Lock {
lifecycle: Lifecycle::Orphaned,
sleeping: true,
idle_opacity: 1.0,
..Lock::default()
};
let now = Instant::now();
assert!(lock.activity(now));
assert!(lock.active() && matches!(lock.lifecycle, Lifecycle::Orphaned));
assert!(!lock.sleeping);
assert_eq!(lock.idle_opacity, 0.0);
assert_eq!(lock.idle_since, Some(now));
assert!(!lock.activity(now));
let mut unlocked = Lock::default();
assert!(!unlocked.activity(now));
assert!(unlocked.idle_since.is_none());
}
#[test]
fn idle_disabled_options_are_independent() {
assert_eq!(
IdleSettings {
dim_after_seconds: 0,
sleep_after_seconds: 0
}
.appearance(Duration::from_secs(9999), Duration::from_millis(500)),
(0.0, false)
);
assert_eq!(
IdleSettings {
dim_after_seconds: 0,
sleep_after_seconds: 10
}
.appearance(Duration::from_secs(10), Duration::from_millis(500)),
(1.0, true)
);
assert_eq!(
IdleSettings {
dim_after_seconds: 1,
sleep_after_seconds: 0
}
.appearance(Duration::from_secs(9999), Duration::from_millis(500)),
(0.65, false)
);
assert!(
IdleSettings {
dim_after_seconds: 86401,
sleep_after_seconds: 0
}
.validate()
.is_err()
);
assert!(crate::config::Config::parse_source("lock-screen { dim-after-seconds -1; }").is_err());
assert!(
crate::config::Config::parse_source("lock-screen { sleep-after-seconds 90000; }")
.unwrap()
.runtime_config()
.is_err()
);
}
use std::io::{Read, Write};
use std::os::unix::net::UnixStream;
use std::sync::Arc;
use calloop::EventLoop;
use smithay::reexports::wayland_server::Client;
use smithay::reexports::wayland_server::protocol::wl_compositor::WlCompositor;
struct Fixture {
events: EventLoop<'static, Ferese>,
state: Ferese,
}
impl Fixture {
fn new(test: &str) -> Option<Self> {
if !crate::startup_tests::private_runtime(test) {
return None;
}
let mut events = EventLoop::try_new().unwrap();
let state = crate::startup_tests::state(&mut events);
Some(Self { events, state })
}
fn dispatch(&mut self) {
self.events.dispatch(Duration::from_millis(5), &mut self.state).unwrap();
crate::after_dispatch(&mut self.state);
}
fn output(&mut self, name: &str) -> Output {
let output = Output::new(
name.into(),
smithay::output::PhysicalProperties {
size: (0, 0).into(),
subpixel: smithay::output::Subpixel::Unknown,
make: "test".into(),
model: "test".into(),
},
);
output.change_current_state(
Some(smithay::output::Mode {
size: (800, 600).into(),
refresh: 60_000,
}),
None,
None,
None,
);
self.state.space.map_output(&output, (0, 0));
self.state.register_output(&output, name.into());
output
}
fn acquire(&mut self) -> Locker {
let (server, mut wire) = UnixStream::pair().unwrap();
let client = self
.state
.display_handle
.insert_client(server, Arc::new(crate::state::ClientState::default()))
.unwrap();
let manager = client
.create_resource::<ExtSessionLockManagerV1, (), Ferese>(&self.state.display_handle, 1, ())
.unwrap();
request(&mut wire, manager.id().protocol_id(), 1, &[2]);
self.dispatch();
Locker {
wire,
client,
next_id: 3,
}
}
}
struct Locker {
wire: UnixStream,
client: Client,
next_id: u32,
}
impl Locker {
fn id(&mut self) -> u32 {
let id = self.next_id;
self.next_id += 1;
id
}
fn expect_locked(&mut self) {
self.wire.set_read_timeout(Some(Duration::from_secs(1))).unwrap();
let mut event = [0u8; 8];
self.wire.read_exact(&mut event).unwrap();
assert_eq!(u32::from_ne_bytes(event[..4].try_into().unwrap()), 2);
assert_eq!(u32::from_ne_bytes(event[4..].try_into().unwrap()), 8u32 << 16);
}
fn surface(&mut self, fixture: &mut Fixture, output: &Output) -> LockSurface {
output.create_global::<Ferese>(&fixture.state.display_handle);
let registry = self.id();
request(&mut self.wire, 1, 1, &[registry]);
fixture.dispatch();
self.wire.set_nonblocking(true).unwrap();
let mut events = Vec::new();
let _ = self.wire.read_to_end(&mut events);
self.wire.set_nonblocking(false).unwrap();
let mut offset = 0;
let mut output_name = None;
while offset < events.len() {
let object = u32::from_ne_bytes(events[offset..offset + 4].try_into().unwrap());
let header = u32::from_ne_bytes(events[offset + 4..offset + 8].try_into().unwrap());
if object == registry && header & 0xffff == 0 {
let len = u32::from_ne_bytes(events[offset + 12..offset + 16].try_into().unwrap()) as usize;
if &events[offset + 16..offset + 16 + len] == b"wl_output\0" {
let name = u32::from_ne_bytes(events[offset + 8..offset + 12].try_into().unwrap());
output_name = Some(output_name.map_or(name, |previous: u32| previous.max(name)));
}
}
offset += (header >> 16) as usize;
}
let output_id = self.id();
let mut args = vec![output_name.expect("output global"), 10];
args.extend(
b"wl_output\0\0\0"
.chunks_exact(4)
.map(|bytes| u32::from_ne_bytes(bytes.try_into().unwrap())),
);
args.extend([4, output_id]);
request(&mut self.wire, registry, 0, &args);
let compositor = self
.client
.create_resource::<WlCompositor, (), Ferese>(&fixture.state.display_handle, 6, ())
.unwrap();
let surface_id = self.id();
request(&mut self.wire, compositor.id().protocol_id(), 0, &[surface_id]);
let lock_surface = self.id();
request(&mut self.wire, 2, 1, &[lock_surface, surface_id, output_id]);
fixture.dispatch();
fixture
.state
.session_lock
.surfaces
.get(output)
.expect("lock surface on requested output")
.clone()
}
}
fn request(wire: &mut UnixStream, object: u32, opcode: u32, args: &[u32]) {
for word in [object, (((args.len() + 2) * 4) as u32) << 16 | opcode]
.into_iter()
.chain(args.iter().copied())
{
wire.write_all(&word.to_ne_bytes()).unwrap();
}
}
#[test]
fn lock_confirms_without_outputs() {
let Some(mut fixture) = Fixture::new("session_lock::tests::lock_confirms_without_outputs") else {
return;
};
let epoch = fixture.state.capture_epoch;
let mut locker = fixture.acquire();
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Locked(_)));
assert_ne!(fixture.state.capture_epoch, epoch, "lock revokes deferred snapshots");
locker.expect_locked();
}
#[test]
fn lock_waits_for_each_output_before_confirmation() {
let Some(mut fixture) = Fixture::new("session_lock::tests::lock_waits_for_each_output_before_confirmation") else {
return;
};
let first = fixture.output("first");
let second = fixture.output("second");
let mut locker = fixture.acquire();
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Acquiring(_)));
fixture.state.lock_frame_presented(&first);
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Acquiring(_)));
assert!(fixture.state.session_lock.idle_timer.is_none());
fixture.state.lock_frame_presented(&second);
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Locked(_)));
fixture.dispatch();
locker.expect_locked();
}
#[test]
fn live_owner_rejects_competitor_unlock() {
let Some(mut fixture) = Fixture::new("session_lock::tests::live_owner_rejects_competitor_unlock") else {
return;
};
let _owner_wire = fixture.acquire();
let owner = fixture.state.session_lock.lifecycle.owner().cloned();
let mut competitor = fixture.acquire();
assert_eq!(fixture.state.session_lock.lifecycle.owner(), owner.as_ref());
request(&mut competitor.wire, 2, 2, &[]);
fixture.dispatch();
assert_eq!(fixture.state.session_lock.lifecycle.owner(), owner.as_ref());
assert!(fixture.state.session_lock.active());
}
#[test]
fn pending_owner_cannot_unlock_before_confirmation() {
let Some(mut fixture) = Fixture::new("session_lock::tests::pending_owner_cannot_unlock_before_confirmation") else {
return;
};
fixture.output("pending");
let mut pending = fixture.acquire();
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Acquiring(_)));
request(&mut pending.wire, 2, 2, &[]);
fixture.dispatch();
assert!(fixture.state.session_lock.active());
fixture.dispatch();
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Orphaned));
}
#[test]
fn owner_disconnect_stays_locked_and_allows_replacement() {
let Some(mut fixture) = Fixture::new("session_lock::tests::owner_disconnect_stays_locked_and_allows_replacement")
else {
return;
};
let owner_wire = fixture.acquire();
let owner = fixture.state.session_lock.lifecycle.owner().cloned();
drop(owner_wire);
fixture.dispatch();
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Orphaned));
assert!(fixture.state.session_lock.active());
let mut replacement = fixture.acquire();
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Locked(_)));
assert_ne!(fixture.state.session_lock.lifecycle.owner(), owner.as_ref());
replacement.expect_locked();
}
#[test]
fn removing_last_output_confirms_pending_lock() {
let Some(mut fixture) = Fixture::new("session_lock::tests::removing_last_output_confirms_pending_lock") else {
return;
};
let output = fixture.output("last");
let mut pending = fixture.acquire();
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Acquiring(_)));
fixture.state.unregister_output(&output);
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Locked(_)));
fixture.dispatch();
pending.expect_locked();
}
#[test]
fn refresh_after_removal_confirms_and_starts_idle_timer() {
let Some(mut fixture) = Fixture::new("session_lock::tests::refresh_after_removal_confirms_and_starts_idle_timer")
else {
return;
};
let output = fixture.output("last");
let mut pending = fixture.acquire();
fixture.state.space.unmap_output(&output);
fixture.state.session_lock.output_removed(&output);
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Acquiring(_)));
fixture.state.refresh_lock_outputs();
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Locked(_)));
assert!(fixture.state.session_lock.idle_timer.is_some());
fixture.dispatch();
pending.expect_locked();
}
#[test]
fn activity_reuses_timer_and_expired_timer_rechecks_latest_activity() {
let Some(mut fixture) =
Fixture::new("session_lock::tests::activity_reuses_timer_and_expired_timer_rechecks_latest_activity")
else {
return;
};
fixture.state.session_lock.lifecycle = Lifecycle::Orphaned;
fixture.state.lock_idle = IdleSettings {
dim_after_seconds: 1,
sleep_after_seconds: 2,
};
fixture.state.session_lock.idle_since = Some(Instant::now() - Duration::from_millis(999));
fixture.state.refresh_lock_idle_policy();
let timer = fixture.state.session_lock.idle_timer.unwrap();
for _ in 0..1000 {
fixture.state.lock_input_activity();
assert_eq!(
fixture.state.session_lock.idle_timer,
Some(timer),
"input replaced the source"
);
}
let limit = Instant::now() + Duration::from_secs(1);
while fixture.state.session_lock.idle_timer.unwrap().1 == timer.1 {
assert!(Instant::now() < limit, "idle timer did not recheck");
fixture
.events
.dispatch(Duration::from_millis(10), &mut fixture.state)
.unwrap();
}
let rechecked = fixture.state.session_lock.idle_timer.unwrap();
assert_eq!(timer.0, rechecked.0, "timer callback replaced the source");
assert!(rechecked.1 > timer.1);
assert_eq!(fixture.state.session_lock.idle_opacity, 0.0);
assert!(!fixture.state.session_lock.sleeping);
}
#[test]
fn earlier_policy_replaces_timer_and_sleeping_activity_arms_one() {
let Some(mut fixture) =
Fixture::new("session_lock::tests::earlier_policy_replaces_timer_and_sleeping_activity_arms_one")
else {
return;
};
fixture.state.session_lock.lifecycle = Lifecycle::Orphaned;
fixture.state.lock_input_activity();
let first = fixture.state.session_lock.idle_timer.unwrap();
fixture.state.lock_idle.dim_after_seconds = 60;
fixture.state.refresh_lock_idle_policy();
assert_eq!(fixture.state.session_lock.idle_timer, Some(first));
fixture.state.lock_idle.dim_after_seconds = 1;
fixture.state.refresh_lock_idle_policy();
let earlier = fixture.state.session_lock.idle_timer.unwrap();
assert_ne!(first.0, earlier.0);
assert!(earlier.1 < first.1);
fixture.state.session_lock.idle_since = Some(Instant::now() - Duration::from_secs(121));
fixture.state.refresh_lock_idle_policy();
assert!(fixture.state.session_lock.sleeping);
assert!(fixture.state.session_lock.idle_timer.is_none());
fixture.state.lock_input_activity();
assert!(fixture.state.session_lock.idle_timer.is_some());
assert!(!fixture.state.session_lock.sleeping);
assert_eq!(fixture.state.session_lock.idle_opacity, 0.0);
assert!(fixture.state.session_lock.active());
}
#[test]
fn dead_preferred_surface_uses_live_fallback_in_stable_output_order() {
let Some(mut fixture) =
Fixture::new("session_lock::tests::dead_preferred_surface_uses_live_fallback_in_stable_output_order")
else {
return;
};
let preferred = fixture.output("preferred");
let second = fixture.output("second");
let third = fixture.output("third");
let mut locker = fixture.acquire();
let dead = locker.surface(&mut fixture, &preferred);
let fallback = locker.surface(&mut fixture, &second);
let later = locker.surface(&mut fixture, &third);
let preferred_id = fixture.state.output_id(&preferred).unwrap();
fixture.state.output_workspaces.focus_output(preferred_id).unwrap();
request(&mut locker.wire, dead.wl_surface().id().protocol_id(), 0, &[]);
fixture.dispatch();
assert!(!dead.alive());
// Exercise the defensive path even though destruction normally removes it.
fixture.state.session_lock.surfaces.insert(preferred.clone(), dead);
for _ in 0..5 {
fixture.state.session_lock.surfaces.remove(&second);
fixture
.state
.session_lock
.surfaces
.insert(second.clone(), fallback.clone());
fixture.state.session_lock.surfaces.insert(third.clone(), later.clone());
fixture.state.focus_lock_surface();
assert_eq!(
fixture.state.seat.get_keyboard().unwrap().current_focus(),
Some(fallback.wl_surface().clone())
);
}
}
#[test]
fn output_removed_clears_all_lock_resources() {
let Some(mut fixture) = Fixture::new("session_lock::tests::output_removed_clears_all_lock_resources") else {
return;
};
let output = fixture.output("removed");
let mut locker = fixture.acquire();
locker.surface(&mut fixture, &output);
let background =
smithay::backend::renderer::element::solid::SolidColorBuffer::new((800, 600), [0.0, 0.0, 0.0, 1.0]);
fixture
.state
.session_lock
.backgrounds
.insert(output.clone(), background.clone());
fixture
.state
.session_lock
.idle_overlays
.insert(output.clone(), background);
fixture.state.session_lock.presented.insert(output.clone());
fixture.state.session_lock.output_removed(&output);
let lock = &fixture.state.session_lock;
assert!(!lock.surfaces.contains_key(&output));
assert!(!lock.backgrounds.contains_key(&output));
assert!(!lock.idle_overlays.contains_key(&output));
assert!(!lock.presented.contains(&output));
assert!(lock.active(), "output cleanup must not unlock");
}
#[test]
fn confirmed_owner_can_unlock_and_cancel_idle_timer() {
let Some(mut fixture) = Fixture::new("session_lock::tests::confirmed_owner_can_unlock_and_cancel_idle_timer")
else {
return;
};
let mut locker = fixture.acquire();
locker.expect_locked();
assert!(fixture.state.session_lock.idle_timer.is_some());
request(&mut locker.wire, 2, 2, &[]);
fixture.dispatch();
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Unlocked));
assert!(fixture.state.session_lock.idle_timer.is_none());
}
#[test]
fn acquiring_owner_disconnect_keeps_lock_and_replacement_waits_for_frame() {
let Some(mut fixture) =
Fixture::new("session_lock::tests::acquiring_owner_disconnect_keeps_lock_and_replacement_waits_for_frame")
else {
return;
};
let output = fixture.output("pending");
let pending = fixture.acquire();
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Acquiring(_)));
drop(pending);
fixture.dispatch();
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Orphaned));
assert!(fixture.state.session_lock.active());
let mut replacement = fixture.acquire();
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Acquiring(_)));
fixture.state.lock_frame_presented(&output);
fixture.dispatch();
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Locked(_)));
replacement.expect_locked();
}
+253
View File
@@ -0,0 +1,253 @@
use std::io::{Read, Write};
use std::os::unix::net::UnixStream;
use std::os::unix::process::ExitStatusExt;
use std::process::{Command, Stdio};
use std::sync::Arc;
use std::time::{Duration, Instant};
use calloop::EventLoop;
use calloop::signals::{Signal, Signals};
use smithay::input::SeatHandler;
use smithay::input::pointer::{CursorImageStatus, MotionEvent};
use smithay::reexports::wayland_server::protocol::{wl_compositor::WlCompositor, wl_surface::WlSurface};
use smithay::reexports::wayland_server::{Display, Resource};
use smithay::utils::SERIAL_COUNTER;
use crate::{Ferese, RedrawRequest, after_dispatch, after_dispatch_with_redraw};
// Each state needs its own sockets and environment. A subprocess also bounds
// the cursor deadlock test without changing environment variables across tests.
pub(crate) fn private_runtime(test: &str) -> bool {
const CHILD: &str = "FERESE_STARTUP_TEST_CHILD";
if std::env::var_os(CHILD).is_some_and(|value| value == test) {
return true;
}
let directory = tempfile::tempdir().unwrap();
let log_path = directory.path().join("test.log");
let log = std::fs::File::create(&log_path).unwrap();
let mut child = Command::new(std::env::current_exe().unwrap())
.args(["--exact", test, "--nocapture"])
.env(CHILD, test)
.env("XDG_RUNTIME_DIR", directory.path())
.env("XDG_CONFIG_HOME", directory.path())
.env("XDG_STATE_HOME", directory.path())
.env("XDG_CACHE_HOME", directory.path())
.env_remove("FERESE_SOCKET")
.stdout(Stdio::from(log.try_clone().unwrap()))
.stderr(Stdio::from(log))
.spawn()
.unwrap();
let deadline = Instant::now() + Duration::from_secs(20);
let status = loop {
if let Some(status) = child.try_wait().unwrap() {
break status;
}
if Instant::now() >= deadline {
child.kill().unwrap();
child.wait().unwrap();
panic!("{test} did not complete; possible startup deadlock");
}
std::thread::sleep(Duration::from_millis(10));
};
assert!(status.success(), "{}", std::fs::read_to_string(log_path).unwrap());
false
}
pub(crate) fn state(events: &mut EventLoop<'static, Ferese>) -> Ferese {
let mut config = crate::config::Config::default().runtime_config().unwrap();
config.wallpaper.path = None;
Ferese::new(events, Display::new().unwrap(), config).unwrap()
}
#[test]
fn launched_clients_unblock_termination_signals_and_keep_private_sockets() {
if !private_runtime("startup_tests::launched_clients_unblock_termination_signals_and_keep_private_sockets") {
return;
}
let mut events = EventLoop::try_new().unwrap();
let mut state = state(&mut events);
let signals = Signals::new(&[Signal::SIGINT, Signal::SIGTERM, Signal::SIGUSR1]).unwrap();
let mut private = crate::private_client::ClientCapabilities::EFFECTS;
private.insert(crate::private_client::ClientCapabilities::SHELL_CONTROL);
for capabilities in [crate::private_client::ClientCapabilities::default(), private] {
let mut child = crate::process::spawn_client(&mut state, ["/bin/sleep", "30"], capabilities).unwrap();
let status = std::fs::read_to_string(format!("/proc/{}/status", child.id())).unwrap();
let blocked = status.lines().find(|line| line.starts_with("SigBlk:")).unwrap();
let blocked = u64::from_str_radix(blocked.split_whitespace().nth(1).unwrap(), 16).unwrap();
assert_eq!(blocked & ((1 << (libc::SIGINT - 1)) | (1 << (libc::SIGTERM - 1))), 0);
assert_ne!(blocked & (1 << (libc::SIGUSR1 - 1)), 0, "unrelated mask was lost");
if !capabilities.is_empty() {
let environment = std::fs::read(format!("/proc/{}/environ", child.id())).unwrap();
for name in ["WAYLAND_SOCKET=", "FERESE_SHELL_CONTROL_SOCKET="] {
let variable = environment
.split(|byte| *byte == 0)
.filter_map(|value| std::str::from_utf8(value).ok())
.find_map(|value| value.strip_prefix(name))
.unwrap();
let fd = std::fs::read_link(format!("/proc/{}/fd/{variable}", child.id())).unwrap();
assert!(fd.to_string_lossy().starts_with("socket:["));
}
}
crate::process::terminate_child(&mut child);
assert_eq!(
child.wait().unwrap().signal(),
Some(libc::SIGTERM),
"child needed SIGKILL"
);
}
// Child setup must not unblock the compositor's own signalfd mask.
let mut mask = unsafe { std::mem::zeroed() };
assert_eq!(
unsafe { libc::pthread_sigmask(libc::SIG_BLOCK, std::ptr::null(), &mut mask) },
0
);
assert_eq!(unsafe { libc::sigismember(&mask, libc::SIGINT) }, 1);
assert_eq!(unsafe { libc::sigismember(&mask, libc::SIGTERM) }, 1);
drop(signals);
}
#[test]
fn explicit_client_exit_is_reaped_without_stopping_the_compositor() {
if !private_runtime("startup_tests::explicit_client_exit_is_reaped_without_stopping_the_compositor") {
return;
}
let mut events = EventLoop::try_new().unwrap();
let mut state = state(&mut events);
let child = crate::process::spawn_client(
&mut state,
["/bin/sh", "-c", "exit 7"],
crate::private_client::ClientCapabilities::default(),
)
.unwrap();
let pid = child.id();
let watched = crate::process::watch_client_exit(&events.handle(), child);
let deadline = Instant::now() + Duration::from_secs(2);
while watched.borrow().is_some() {
assert!(Instant::now() < deadline, "client was not reaped");
events.dispatch(Duration::from_millis(10), &mut state).unwrap();
after_dispatch(&mut state);
}
let mut status = 0;
assert_eq!(unsafe { libc::waitpid(pid as i32, &mut status, libc::WNOHANG) }, -1);
assert_eq!(std::io::Error::last_os_error().raw_os_error(), Some(libc::ECHILD));
// Exit watching removes itself; subsequent event-loop dispatch remains usable.
events.dispatch(Duration::ZERO, &mut state).unwrap();
}
#[test]
fn check_config_rejects_an_otherwise_valid_oversized_file() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("config.kdl");
let valid = format!("//{}\n", "x".repeat(60 * 1024 - 3));
std::fs::write(&path, &valid).unwrap();
crate::check_config(&path).unwrap();
assert!(crate::theme::prepare(&(valid.clone() + "\n"), directory.path()).is_ok());
std::fs::write(&path, valid + "\n").unwrap();
let error = crate::check_config(&path).unwrap_err();
assert!(error.contains("exceeds 60 KiB"), "{error}");
assert_eq!(crate::theme::read_source(&path).unwrap_err(), error);
}
#[test]
fn deferred_cursor_redraw_runs_after_input_lock_is_released() {
if !private_runtime("startup_tests::deferred_cursor_redraw_runs_after_input_lock_is_released") {
return;
}
let mut events = EventLoop::try_new().unwrap();
let mut state = state(&mut events);
let (server, mut client_wire) = UnixStream::pair().unwrap();
let client = state
.display_handle
.insert_client(server, Arc::new(crate::state::ClientState::default()))
.unwrap();
let compositor = client
.create_resource::<WlCompositor, (), Ferese>(&state.display_handle, 6, ())
.unwrap();
// wl_compositor.create_surface(new_id=2).
for word in [compositor.id().protocol_id(), 12u32 << 16, 2] {
client_wire.write_all(&word.to_ne_bytes()).unwrap();
}
events.dispatch(Duration::from_millis(10), &mut state).unwrap();
let surface = client
.object_from_protocol_id::<WlSurface>(&state.display_handle, 2)
.unwrap();
let pointer = state.seat.get_pointer().unwrap();
let seat = state.seat.clone();
state.cursor_image(&seat, CursorImageStatus::Hidden);
after_dispatch_with_redraw(&mut state, |_, _| {});
events.handle().insert_idle(move |state| {
for _ in 0..2 {
let event = MotionEvent {
location: (10.0, 20.0).into(),
serial: SERIAL_COUNTER.next_serial(),
time: 0,
};
pointer.motion(state, Some((surface.clone(), (0.0, 0.0).into())), &event);
// Smithay invokes Ferese::cursor_image while holding its real pointer lock.
pointer.motion(state, None, &event);
assert!(state.cursor_redraw_pending);
}
});
let signal = events.get_signal();
let mut redraws = 0;
events
.run(Some(Duration::ZERO), &mut state, |state| {
after_dispatch_with_redraw(state, |state, request| {
assert!(matches!(request, RedrawRequest::Cursor));
assert_eq!(
state.seat.get_pointer().unwrap().current_location(),
(10.0, 20.0).into()
);
redraws += 1;
});
assert!(!state.cursor_redraw_pending);
signal.stop();
})
.unwrap();
assert_eq!(redraws, 1);
after_dispatch_with_redraw(&mut state, |_, _| panic!("redraw was not consumed"));
}
#[test]
fn wayland_roundtrip_completes_without_a_rendered_frame() {
if !private_runtime("startup_tests::wayland_roundtrip_completes_without_a_rendered_frame") {
return;
}
let mut events = EventLoop::try_new().unwrap();
let mut state = state(&mut events);
let (server, mut client) = UnixStream::pair().unwrap();
state
.display_handle
.insert_client(server, Arc::new(crate::state::ClientState::default()))
.unwrap();
// wl_display.sync(new_id=2), using native-endian Wayland wire format.
for word in [1u32, 12u32 << 16, 2u32] {
client.write_all(&word.to_ne_bytes()).unwrap();
}
events.dispatch(Duration::from_millis(10), &mut state).unwrap();
client.set_read_timeout(Some(Duration::from_millis(20))).unwrap();
let mut reply = [0u8; 12];
assert!(
client.read_exact(&mut reply).is_err(),
"dispatch flushed replies before after_dispatch"
);
after_dispatch(&mut state);
client.set_read_timeout(Some(Duration::from_secs(1))).unwrap();
client.read_exact(&mut reply).unwrap();
assert_eq!(u32::from_ne_bytes(reply[0..4].try_into().unwrap()), 2);
assert_eq!(u32::from_ne_bytes(reply[4..8].try_into().unwrap()) & 0xffff, 0);
}
+4
View File
@@ -7,6 +7,8 @@ mod layout;
mod navigation;
mod outputs;
mod reconciliation;
#[cfg(test)]
mod window_lifecycle_tests;
pub(crate) use reconciliation::{DesktopChanges, DesktopOutput, DesktopTransition};
mod prediction;
mod presentation_dependencies;
@@ -478,6 +480,7 @@ pub struct Ferese {
pub direct_backend: Option<DirectBackendState>,
_ipc_socket: Option<IpcSocketGuard>,
pending_dmabuf_imports: Vec<(Dmabuf, ImportNotifier)>,
pub(crate) activation_inputs: crate::handlers::activation::InputHistory,
pub(crate) pending_screencopies: Vec<PendingScreencopy>,
// Screenshot requests outlive the readback that filled them: the
// coordinator owns them until encoding finishes and the caller is
@@ -721,6 +724,7 @@ impl Ferese {
direct_backend: None,
_ipc_socket: None,
pending_dmabuf_imports: Vec::new(),
activation_inputs: Default::default(),
pending_screencopies: Vec::new(),
screenshot: Coordinator::new(),
screenshot_parts: None,
+1 -1
View File
@@ -138,4 +138,4 @@ impl Ferese {
#[cfg(test)]
#[path = "capture_privacy_tests.rs"]
mod tests;
pub(super) mod tests;
@@ -22,7 +22,7 @@ use smithay::wayland::shell::xdg::{XdgShellHandler, XdgToplevelSurfaceData, XdgW
use crate::Ferese;
use crate::state::{ClientState, DesktopOutput};
fn request(wire: &mut UnixStream, object: u32, opcode: u32, args: &[u32], fd: Option<RawFd>) {
pub(crate) fn request(wire: &mut UnixStream, object: u32, opcode: u32, args: &[u32], fd: Option<RawFd>) {
let bytes = [object, (((args.len() + 2) * 4) as u32) << 16 | opcode]
.into_iter()
.chain(args.iter().copied())
@@ -54,12 +54,12 @@ fn request(wire: &mut UnixStream, object: u32, opcode: u32, args: &[u32], fd: Op
}
}
fn dispatch(events: &mut EventLoop<'static, Ferese>, state: &mut Ferese) {
pub(crate) fn dispatch(events: &mut EventLoop<'static, Ferese>, state: &mut Ferese) {
events.dispatch(Duration::from_millis(1), state).unwrap();
state.display_handle.flush_clients().unwrap();
}
fn ack_configure(wire: &mut UnixStream, xdg: u32) -> u32 {
pub(crate) fn ack_configure(wire: &mut UnixStream, xdg: u32) -> u32 {
wire.set_nonblocking(true).unwrap();
let mut bytes = Vec::new();
let _ = wire.read_to_end(&mut bytes);
@@ -79,7 +79,7 @@ fn ack_configure(wire: &mut UnixStream, xdg: u32) -> u32 {
serial.unwrap()
}
fn window(state: &mut Ferese, events: &mut EventLoop<'static, Ferese>, color: u32) -> (Window, UnixStream) {
pub(crate) fn window(state: &mut Ferese, events: &mut EventLoop<'static, Ferese>, color: u32) -> (Window, UnixStream) {
let (server, mut wire) = UnixStream::pair().unwrap();
let client = state
.display_handle
@@ -208,7 +208,7 @@ fn pixels(
Rectangle::from_size((320, 240).into()),
));
let mut target = Some(texture.clone());
crate::backends::direct::capture::capture_output(state, renderer, &mut target, output, &scene).unwrap();
crate::backends::direct::capture::capture_output(state, renderer, &mut target, output, &scene);
let mut result = receiver.try_recv().unwrap().result.unwrap().pixels;
for pixel in result.chunks_exact_mut(4) {
pixel.swap(0, 2);
@@ -424,8 +424,7 @@ fn capture_privacy_pixels_and_policy_transitions() {
&mut Some(capture_texture.clone()),
&output,
&scene,
)
.unwrap();
);
let region = receiver.try_recv().unwrap().result.unwrap();
assert_eq!((region.width, region.height), (160, 120));
assert!(
+4 -3
View File
@@ -246,12 +246,14 @@ impl Ferese {
let is_maximized =
self.windows.record(*id).is_some_and(|w| w.maximized) && workspace_fullscreen != Some(*id);
let placement = self.workspaces.placement(*id);
let is_floating = matches!(placement, Some(WindowPlacement::Floating { .. }));
let rect = if workspace_fullscreen == Some(*id) {
fullscreen_bounds
} else if is_maximized && !is_scrolling_layout {
} else if is_maximized && (!is_scrolling_layout || is_floating) {
maximized_rect(bounds, self.gap_config.outer)
} else {
match self.workspaces.placement(*id) {
match placement {
Some(WindowPlacement::Tiled) => {
let Some(rect) = layout.geometry.get(id).copied() else {
continue;
@@ -271,7 +273,6 @@ impl Ferese {
};
let is_fullscreen = workspace_fullscreen == Some(*id);
let is_floating = matches!(self.workspaces.placement(*id), Some(WindowPlacement::Floating { .. }));
visible.insert(*id);
let scrolling = if !is_fullscreen && !is_floating && (!is_maximized || is_scrolling_layout) {
-2
View File
@@ -111,8 +111,6 @@ impl Ferese {
true
}
});
self.session_lock.surfaces.remove(output);
self.session_lock.backgrounds.remove(output);
self.session_lock.output_removed(output);
}
@@ -0,0 +1,281 @@
use super::capture_privacy::tests::{ack_configure, dispatch, request, window};
use super::*;
use smithay::input::pointer::{Focus, GrabStartData, MotionEvent};
use smithay::reexports::wayland_server::Resource;
use smithay::utils::SERIAL_COUNTER;
fn fixture() -> (smithay::reexports::calloop::EventLoop<'static, Ferese>, Ferese, Output) {
let mut events = smithay::reexports::calloop::EventLoop::try_new().unwrap();
let mut state = crate::startup_tests::state(&mut events);
state.animations_enabled = false;
let output = Output::new(
"test".into(),
smithay::output::PhysicalProperties {
size: (0, 0).into(),
subpixel: smithay::output::Subpixel::Unknown,
make: "test".into(),
model: "test".into(),
},
);
output.change_current_state(
Some(smithay::output::Mode {
size: (800, 600).into(),
refresh: 60_000,
}),
None,
None,
None,
);
state.space.map_output(&output, (0, 0));
state.register_output(&output, "test".into());
(events, state, output)
}
#[test]
fn hidden_null_commit_removes_membership_and_remaps_without_workspace_switch() {
if !crate::startup_tests::private_runtime(
"state::window_lifecycle_tests::hidden_null_commit_removes_membership_and_remaps_without_workspace_switch",
) {
return;
}
let (mut events, mut state, _) = fixture();
let (window, mut wire) = window(&mut state, &mut events, 0xff112233);
let id = state.windows.ids()[&window];
state.switch_workspace(2);
let active = state.workspaces.active_id();
assert!(state.space.element_location(&window).is_none());
assert_eq!(
state.window_for_surface(window.toplevel().unwrap().wl_surface()),
Some(window.clone())
);
request(&mut wire, 2, 1, &[0, 0, 0], None);
request(&mut wire, 2, 6, &[], None);
dispatch(&mut events, &mut state);
assert!(!state.windows.ids().contains_key(&window));
assert!(state.workspaces.workspace_for_window(id).is_none());
request(&mut wire, 2, 6, &[], None);
dispatch(&mut events, &mut state);
ack_configure(&mut wire, 3);
request(&mut wire, 2, 1, &[6, 0, 0], None);
request(&mut wire, 2, 6, &[], None);
dispatch(&mut events, &mut state);
let remapped = state.windows.ids()[&window];
assert_ne!(id, remapped);
assert_eq!(state.workspaces.workspace_for_window(remapped), Some(active));
assert_eq!(state.workspaces.active_id(), active);
}
#[test]
fn hidden_commits_update_client_geometry_and_release_resize_transactions() {
if !crate::startup_tests::private_runtime(
"state::window_lifecycle_tests::hidden_commits_update_client_geometry_and_release_resize_transactions",
) {
return;
}
let (mut events, mut state, _) = fixture();
let (window, mut wire) = window(&mut state, &mut events, 0xff112233);
let id = state.windows.ids()[&window];
state.switch_workspace(2);
assert!(state.space.element_location(&window).is_none());
let toplevel = window.toplevel().unwrap();
toplevel.with_pending_state(|pending| pending.size = Some((80, 48).into()));
let serial = toplevel.send_configure();
state.windows.set_transaction(
id,
crate::resize_transaction::ResizeTransaction::new(serial, state.presentation_now()),
);
state.display_handle.flush_clients().unwrap();
assert_eq!(ack_configure(&mut wire, 3), u32::from(serial));
request(&mut wire, 3, 3, &[0, 0, 32, 48], None);
request(&mut wire, 2, 1, &[6, 0, 0], None);
request(&mut wire, 2, 6, &[], None);
dispatch(&mut events, &mut state);
assert_eq!(window.geometry().size, (32, 48).into());
assert!(state.windows.transaction(&id).is_none());
assert_eq!(
state.windows.geometry(&id).unwrap().client.committed_size,
Some(ClientSize { width: 32, height: 48 })
);
assert!(state.space.element_location(&window).is_none());
}
#[test]
fn floating_maximize_and_restore_work_in_both_layouts() {
if !crate::startup_tests::private_runtime(
"state::window_lifecycle_tests::floating_maximize_and_restore_work_in_both_layouts",
) {
return;
}
let (mut events, mut state, _) = fixture();
let (window, mut wire) = window(&mut state, &mut events, 0xff112233);
let id = state.windows.ids()[&window];
state.focused_window = Some(id);
state.toggle_focused_floating();
state.set_floating_window_geometry(&window, (120, 100).into(), (64, 48).into());
state.display_handle.flush_clients().unwrap();
ack_configure(&mut wire, 3);
request(&mut wire, 2, 6, &[], None);
dispatch(&mut events, &mut state);
let normal = match state.workspaces.placement(id).unwrap() {
WindowPlacement::Floating { rect } => rect,
_ => panic!("expected floating window"),
};
let mut next_object = 7;
for mode in [ferese_core::LayoutMode::Scrolling, ferese_core::LayoutMode::Tree] {
let bounds = state.output_bounds().unwrap();
state.workspaces.set_active_layout_mode(mode, bounds).unwrap();
state.relayout();
state.set_window_maximized(id, true);
let maximized = maximized_rect(bounds, state.gap_config.outer);
assert_eq!(state.windows.geometry(&id).unwrap().logical, maximized, "{mode:?}");
assert_eq!(
state.workspaces.placement(id),
Some(WindowPlacement::Floating { rect: normal })
);
state.display_handle.flush_clients().unwrap();
ack_configure(&mut wire, 3);
// Commit the configured raster instead of keeping the original tiny buffer.
use smithay::reexports::wayland_server::protocol::wl_shm::WlShm;
use std::io::Write;
use std::os::fd::AsRawFd;
let client = window.toplevel().unwrap().wl_surface().client().unwrap();
let shm = client
.create_resource::<WlShm, (), Ferese>(&state.display_handle, 1, ())
.unwrap();
let (width, height) = (maximized.width.round() as u32, maximized.height.round() as u32);
let mut file = tempfile::tempfile().unwrap();
file.write_all(&0xff112233u32.to_ne_bytes().repeat((width * height) as usize))
.unwrap();
request(
&mut wire,
shm.id().protocol_id(),
0,
&[next_object, width * height * 4],
Some(file.as_raw_fd()),
);
request(
&mut wire,
next_object,
0,
&[next_object + 1, 0, width, height, width * 4, 0],
None,
);
request(&mut wire, 3, 3, &[0, 0, width, height], None);
request(&mut wire, 2, 1, &[next_object + 1, 0, 0], None);
next_object += 2;
request(&mut wire, 2, 6, &[], None);
dispatch(&mut events, &mut state);
assert!(
window
.toplevel()
.unwrap()
.current_state()
.states
.contains(xdg_toplevel::State::Maximized)
);
state.set_window_maximized(id, false);
assert_eq!(state.windows.geometry(&id).unwrap().logical, normal, "{mode:?}");
state.display_handle.flush_clients().unwrap();
ack_configure(&mut wire, 3);
request(&mut wire, 3, 3, &[0, 0, 64, 48], None);
request(&mut wire, 2, 1, &[6, 0, 0], None);
request(&mut wire, 2, 6, &[], None);
dispatch(&mut events, &mut state);
assert!(
!window
.toplevel()
.unwrap()
.current_state()
.states
.contains(xdg_toplevel::State::Maximized)
);
assert_eq!(
state.workspaces.placement(id),
Some(WindowPlacement::Floating { rect: normal })
);
}
}
#[test]
fn malformed_resize_limits_disconnect_only_the_client_and_motion_survives() {
use std::io::Read;
if !crate::startup_tests::private_runtime(
"state::window_lifecycle_tests::malformed_resize_limits_disconnect_only_the_client_and_motion_survives",
) {
return;
}
let (mut events, mut state, _) = fixture();
for (minimum, maximum) in [
((800, 0), (400, 0)),
((0, 800), (0, 400)),
((-1, 0), (0, 0)),
((0, 0), (-1, 0)),
((0, -1), (0, 0)),
((0, 0), (0, -1)),
] {
let (window, mut wire) = window(&mut state, &mut events, 0xff112233);
let id = state.windows.ids()[&window];
state.focused_window = Some(id);
state.toggle_focused_floating();
let pointer = state.seat.get_pointer().unwrap();
pointer.set_grab(
&mut state,
crate::grabs::ResizeSurfaceGrab::new(
GrabStartData {
focus: None,
button: 0x110,
location: (100.0, 100.0).into(),
},
window,
xdg_toplevel::ResizeEdge::BottomRight.into(),
Rectangle::new((100, 100).into(), (64, 48).into()),
),
SERIAL_COUNTER.next_serial(),
Focus::Clear,
);
request(&mut wire, 4, 8, &[minimum.0 as u32, minimum.1 as u32], None);
request(&mut wire, 4, 7, &[maximum.0 as u32, maximum.1 as u32], None);
request(&mut wire, 2, 6, &[], None);
dispatch(&mut events, &mut state);
pointer.motion(
&mut state,
None,
&MotionEvent {
location: (120.0, 130.0).into(),
serial: SERIAL_COUNTER.next_serial(),
time: 1,
},
);
pointer.unset_grab(&mut state, SERIAL_COUNTER.next_serial(), 2);
wire.set_nonblocking(true).unwrap();
let mut messages = Vec::new();
let _ = wire.read_to_end(&mut messages);
let mut offset = 0;
let mut rejected = false;
while offset + 8 <= messages.len() {
let object = u32::from_ne_bytes(messages[offset..offset + 4].try_into().unwrap());
let header = u32::from_ne_bytes(messages[offset + 4..offset + 8].try_into().unwrap());
if object == 1 && header & 0xffff == 0 {
assert_eq!(
u32::from_ne_bytes(messages[offset + 12..offset + 16].try_into().unwrap()),
xdg_toplevel::Error::InvalidSize as u32
);
rejected = true;
}
offset += (header >> 16) as usize;
}
assert!(rejected, "invalid limits {minimum:?}/{maximum:?} were not rejected");
}
let (healthy, _wire) = window(&mut state, &mut events, 0xff112233);
assert!(state.windows.ids().contains_key(&healthy));
}
+18 -11
View File
@@ -1,6 +1,20 @@
use super::*;
impl Ferese {
pub(crate) fn window_for_surface(&self, surface: &WlSurface) -> Option<Window> {
self.windows
.id_for_surface(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)
})
})
.cloned()
}
pub(crate) fn restore_initial_floating_size(&mut self, window: &Window) {
let Some(toplevel) = window.toplevel() else { return };
let (app_id, title, transient) = with_states(toplevel.wl_surface(), |states| {
@@ -449,17 +463,10 @@ impl Ferese {
}
pub(crate) fn capture_resize_before_commit(&mut self, surface: &WlSurface) {
let Some((window, id)) = self
.windows
.ids()
.iter()
.find(|(window, _)| {
window
.toplevel()
.is_some_and(|toplevel| toplevel.wl_surface() == surface)
})
.map(|(window, id)| (window.clone(), *id))
else {
let Some(id) = self.windows.id_for_surface(surface) else {
return;
};
let Some(window) = self.windows.window(id).cloned() else {
return;
};
+4 -5
View File
@@ -97,7 +97,7 @@ static SYSTEM_APPEARANCE: std::sync::OnceLock<Mutex<Option<ferese_config::theme:
fn auto_context() -> ferese_config::theme::AutoContext {
let appearance = SYSTEM_APPEARANCE.get_or_init(|| {
let value = std::process::Command::new("gsettings")
let value = crate::process::command("gsettings")
.args(["get", "org.gnome.desktop.interface", "color-scheme"])
.output()
.ok()
@@ -323,7 +323,7 @@ pub(crate) fn init(
.spawn(move || {
use std::io::BufRead;
use std::os::unix::process::CommandExt;
let mut command = std::process::Command::new("gsettings");
let mut command = crate::process::command("gsettings");
unsafe {
command.pre_exec(|| {
if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM) == -1 {
@@ -338,7 +338,7 @@ pub(crate) fn init(
.stderr(std::process::Stdio::null())
.spawn();
if let Ok(mut monitor) = monitor {
if let Ok(output) = std::process::Command::new("gsettings")
if let Ok(output) = crate::process::command("gsettings")
.args(["get", "org.gnome.desktop.interface", "color-scheme"])
.output()
{
@@ -430,8 +430,7 @@ impl Ferese {
self.poll_theme();
return;
}
let wallpaper = (self.wallpaper.configuration() != &wallpaper)
.then(|| WallpaperState::with_wakeup(wallpaper, Some(self.loop_signal.clone())));
let wallpaper = (self.wallpaper.configuration() != &wallpaper).then(|| self.wallpaper.replacement(wallpaper));
self.theme_engine.pending = Some(Pending {
candidate,
wallpaper,
+102 -32
View File
@@ -18,8 +18,11 @@ use smithay::utils::{Buffer, Physical, Rectangle, Size};
use crate::presentation::NativeTextureElement;
mod decoder;
mod pixels;
use decoder::Decoder;
use pixels::Pixels;
use std::sync::Arc;
const UPLOAD_RETRY_DELAY: Duration = Duration::from_secs(5);
@@ -59,6 +62,8 @@ struct WallpaperTexture {
pub(crate) struct WallpaperState {
config: WallpaperConfig,
pending: Option<WallpaperConfig>,
loading: Option<WallpaperConfig>,
decoder: Arc<Decoder>,
commit: CommitCounter,
owned: bool,
mode: WallpaperMode,
@@ -78,26 +83,35 @@ impl WallpaperState {
}
pub fn with_wakeup(config: WallpaperConfig, wakeup: Option<smithay::reexports::calloop::LoopSignal>) -> Self {
Self::with_decoder(config, wakeup, Decoder::new(pixels::load))
}
pub(crate) fn replacement(&self, config: WallpaperConfig) -> Self {
Self::with_decoder(config, self.wakeup.clone(), self.decoder.clone())
}
fn with_decoder(
config: WallpaperConfig,
wakeup: Option<smithay::reexports::calloop::LoopSignal>,
decoder: Arc<Decoder>,
) -> Self {
let retained = config.clone();
let (sender, receiver) = mpsc::channel();
let owned = config.path.as_ref().is_some_and(|path| path.is_file());
if owned {
let path = config.path.unwrap();
let wakeup = wakeup.clone();
std::thread::spawn(move || {
let _ = sender.send(pixels::load(&path));
if let Some(wakeup) = wakeup {
wakeup.wakeup();
}
});
}
let receiver = if owned {
Some(decoder.submit(config.path.clone().unwrap(), wakeup.clone()))
} else {
decoder.cancel_pending();
None
};
Self {
config: retained,
pending: None,
loading: None,
decoder,
commit: CommitCounter::default(),
owned,
mode: config.mode,
receiver: owned.then_some(receiver),
receiver,
pixels: None,
textures: HashMap::new(),
upload_retries: HashMap::new(),
@@ -121,8 +135,22 @@ impl WallpaperState {
}
pub fn reload(&mut self, config: WallpaperConfig) {
if self.config.path != config.path && config.path.as_ref().is_some_and(|path| !path.is_file()) {
tracing::warn!("new wallpaper is unavailable; retaining previous image");
return;
}
if self.receiver.is_some() {
self.pending = (self.config != config).then_some(config);
if self.loading.is_some() && self.config == config {
self.receiver = None;
self.loading = None;
self.pending = None;
self.decoder.cancel_pending();
return;
}
let requested = self.loading.as_ref().unwrap_or(&self.config);
self.pending = (requested != &config).then_some(config);
return;
}
if self.config == config {
@@ -134,19 +162,18 @@ impl WallpaperState {
self.commit.increment();
return;
}
if config.path.as_ref().is_some_and(|path| !path.is_file()) {
tracing::warn!("new wallpaper is unavailable; retaining previous image");
return;
}
let mut replacement = Self::with_wakeup(config, self.wakeup.clone());
self.config = replacement.config;
self.mode = replacement.mode;
self.owned = replacement.owned;
self.receiver = replacement.receiver.take();
if self.config.path.is_none() {
if let Some(path) = config.path.clone() {
self.receiver = Some(self.decoder.submit(path, self.wakeup.clone()));
self.loading = Some(config);
} else {
self.decoder.cancel_pending();
self.config = config;
self.mode = self.config.mode;
self.owned = false;
self.pixels = None;
self.textures.clear();
self.upload_retries.clear();
self.commit.increment();
}
}
@@ -205,16 +232,15 @@ impl WallpaperState {
match receiver.try_recv() {
Ok(result) => {
self.receiver = None;
if let Some(pending) = self.pending.take() {
if pending.path != self.config.path {
self.reload(pending);
return true;
}
self.config = pending;
self.mode = self.config.mode;
}
let loading = self.loading.take();
match result {
Ok(pixels) => {
if let Some(config) = loading {
self.config = config;
self.mode = self.config.mode;
self.owned = true;
}
tracing::info!(
width = pixels.width(),
height = pixels.height(),
@@ -227,13 +253,17 @@ impl WallpaperState {
self.commit.increment();
}
Err(error) => {
tracing::warn!(%error, "wallpaper unavailable; using output clear color")
tracing::warn!(%error, "wallpaper decode failed; retaining previous image")
}
}
if let Some(pending) = self.pending.take() {
self.reload(pending);
}
true
}
Err(mpsc::TryRecvError::Disconnected) => {
self.receiver = None;
self.loading = None;
false
}
Err(mpsc::TryRecvError::Empty) => false,
@@ -323,6 +353,46 @@ fn image_geometry(
mod tests {
use super::*;
#[test]
fn failed_reload_preserves_accepted_config_and_unavailable_pending_keeps_success() {
let directory = tempfile::tempdir().unwrap();
let mut state = WallpaperState::new(WallpaperConfig {
path: None,
mode: WallpaperMode::Fill,
});
let accepted = state.config.clone();
state.pixels = Some(pixels::from_rgba(image::RgbaImage::new(2, 2)).unwrap());
let bad = directory.path().join("bad-image");
std::fs::write(&bad, b"not an image").unwrap();
state.reload(WallpaperConfig {
path: Some(bad),
mode: WallpaperMode::Fit,
});
let deadline = Instant::now() + Duration::from_secs(2);
while !state.poll() {
assert!(Instant::now() < deadline);
std::thread::sleep(Duration::from_millis(1));
}
assert_eq!(state.config, accepted);
assert_eq!(state.pixels.as_ref().unwrap().dimensions(), (2, 2));
let (sender, receiver) = mpsc::channel();
state.receiver = Some(receiver);
state.reload(WallpaperConfig {
path: Some(directory.path().join("missing")),
mode: WallpaperMode::Fit,
});
sender
.send(Ok(pixels::from_rgba(image::RgbaImage::new(3, 4)).unwrap()))
.unwrap();
assert!(state.poll());
assert_eq!(state.pixels.as_ref().unwrap().dimensions(), (3, 4));
assert_eq!(state.config, accepted);
let replacement = state.replacement(accepted);
assert!(Arc::ptr_eq(&state.decoder, &replacement.decoder));
}
#[test]
fn portal_wallpaper_decodes_without_a_filename_extension() {
let directory = tempfile::tempdir().unwrap();
+114
View File
@@ -0,0 +1,114 @@
//! One in-flight decode and one replaceable request across wallpaper states.
use std::path::{Path, PathBuf};
use std::sync::{Arc, Condvar, Mutex, mpsc};
use super::pixels::Pixels;
struct Request {
path: PathBuf,
result: mpsc::Sender<Result<Pixels, String>>,
wakeup: Option<smithay::reexports::calloop::LoopSignal>,
}
#[derive(Default)]
struct Queue {
pending: Option<Request>,
closed: bool,
}
pub(super) struct Decoder {
shared: Arc<(Mutex<Queue>, Condvar)>,
}
impl Decoder {
pub fn new(load: impl Fn(&Path) -> Result<Pixels, String> + Send + 'static) -> Arc<Self> {
let shared = Arc::new((Mutex::new(Queue::default()), Condvar::new()));
let worker = shared.clone();
std::thread::Builder::new()
.name("ferese-wallpaper".into())
.spawn(move || {
loop {
let (queue, ready) = &*worker;
let mut queue = ready
.wait_while(queue.lock().unwrap(), |queue| !queue.closed && queue.pending.is_none())
.unwrap();
if queue.closed {
break;
}
let request = queue.pending.take().unwrap();
drop(queue);
let _ = request.result.send(load(&request.path));
if let Some(wakeup) = request.wakeup {
wakeup.wakeup();
}
}
})
.expect("start wallpaper decoder");
Arc::new(Self { shared })
}
pub fn submit(
&self,
path: PathBuf,
wakeup: Option<smithay::reexports::calloop::LoopSignal>,
) -> mpsc::Receiver<Result<Pixels, String>> {
let (result, receiver) = mpsc::channel();
let (queue, ready) = &*self.shared;
queue.lock().unwrap().pending = Some(Request { path, result, wakeup });
ready.notify_one();
receiver
}
pub fn cancel_pending(&self) {
self.shared.0.lock().unwrap().pending = None;
}
}
impl Drop for Decoder {
fn drop(&mut self) {
let (queue, ready) = &*self.shared;
let mut queue = queue.lock().unwrap();
queue.closed = true;
queue.pending = None;
ready.notify_one();
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn replacements_share_one_decode_and_only_the_latest_pending_starts() {
let (started, events) = mpsc::channel();
let (release, held) = mpsc::channel();
let decoder = Decoder::new(move |path| {
started.send(path.to_owned()).unwrap();
if path == Path::new("first") {
held.recv_timeout(Duration::from_secs(2)).unwrap();
}
Err("test decode".into())
});
let first = decoder.submit("first".into(), None);
assert_eq!(
events.recv_timeout(Duration::from_secs(2)).unwrap(),
PathBuf::from("first")
);
let dropped = decoder.submit("second".into(), None);
let dropped_too = decoder.submit("third".into(), None);
let latest = decoder.submit("latest".into(), None);
assert!(events.try_recv().is_err());
assert!(matches!(dropped.try_recv(), Err(mpsc::TryRecvError::Disconnected)));
assert!(matches!(dropped_too.try_recv(), Err(mpsc::TryRecvError::Disconnected)));
release.send(()).unwrap();
assert!(first.recv_timeout(Duration::from_secs(2)).unwrap().is_err());
assert_eq!(
events.recv_timeout(Duration::from_secs(2)).unwrap(),
PathBuf::from("latest")
);
assert!(latest.recv_timeout(Duration::from_secs(2)).unwrap().is_err());
assert!(events.try_recv().is_err());
}
}
+1 -1
View File
@@ -155,7 +155,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
&mut capture_texture,
&output,
&scene,
)?;
);
capture_changed_binding = true;
}
if capture_changed_binding {