2020//! [`connect`](WlrForeignToplevelSource::connect) returns `None` there and the
2121//! caller falls through to the next backend candidate.
2222//!
23+ //! ## Dispatch model
24+ //!
2325//! The protocol is event-driven, but the [`super::FrontmostSource`] contract is
24- //! a synchronous poll (~1 Hz from `openlogi-gui::app_watcher`). To bridge that,
25- //! the connection, event queue, and accumulated state are kept alive in the
26- //! backend and a `roundtrip` is performed on each poll to drain pending events
27- //! (focus changes, new / closed windows) before reading the active toplevel.
26+ //! a synchronous poll (~1 Hz from `openlogi-gui::app_watcher`). Two primitives
27+ //! bridge that gap:
28+ //!
29+ //! - **`drain_events`** (poll path) β flushes pending writes, then attempts a
30+ //! non-blocking `prepare_read` + `read` with a short 25 ms `poll(2)` cap.
31+ //! If nothing arrives in time the last known state is returned unchanged;
32+ //! millisecond-stale frontmost data is acceptable by design.
33+ //!
34+ //! - **`timed_roundtrip`** (init path) β sends `wl_display.sync`, then loops
35+ //! `flush` β `poll(2)` β `read` β `dispatch_pending` until the sync callback
36+ //! fires or `INIT_TIMEOUT` (5 s) expires. If the deadline is hit the candidate
37+ //! returns `None` so backend selection falls through β the same contract as
38+ //! every other backend.
39+ //!
40+ //! Both helpers use `poll(2)` via the `libc` crate (already a Linux dependency)
41+ //! with `Instant`-based remaining-time accounting and `EINTR` retry.
2842
2943use std:: collections:: HashMap ;
44+ use std:: os:: unix:: io:: AsRawFd ;
3045use std:: sync:: Mutex ;
46+ use std:: time:: { Duration , Instant } ;
3147
3248use tracing:: { debug, info, warn} ;
3349use wayland_client:: backend:: ObjectId ;
50+ use wayland_client:: protocol:: wl_callback;
3451use wayland_client:: protocol:: wl_registry:: { self , WlRegistry } ;
3552use wayland_client:: { Connection , Dispatch , EventQueue , Proxy , QueueHandle , event_created_child} ;
3653use wayland_protocols_wlr:: foreign_toplevel:: v1:: client:: zwlr_foreign_toplevel_handle_v1:: {
@@ -47,6 +64,15 @@ use super::FrontmostSource;
4764/// here to stay within what `wayland-protocols-wlr` generates.
4865const MANAGER_MAX_VERSION : u32 = 3 ;
4966
67+ /// Deadline for the two `wl_display.sync` round-trips in `Session::open`.
68+ /// Mirrors `gnome_shell::METHOD_TIMEOUT`: both guard the `FRONTMOST_SOURCE`
69+ /// `LazyLock` initializer against a stalled compositor socket.
70+ const INIT_TIMEOUT : Duration = Duration :: from_secs ( 5 ) ;
71+
72+ /// Maximum time the poll-path drain will wait for new Wayland events. Stale
73+ /// frontmost data within this window is acceptable by design.
74+ const POLL_CAP_MS : u64 = 25 ;
75+
5076/// Accumulated per-toplevel data. wlr sends individual property events and then
5177/// a `done` marking a consistent snapshot, so updates are staged in `pending_*`
5278/// and committed on `done`.
@@ -66,6 +92,9 @@ struct State {
6692 /// Set when the compositor sends `finished`; triggers a reconnect on the
6793 /// next poll instead of permanently disabling the backend.
6894 finished : bool ,
95+ /// Flipped to `true` by the `wl_callback::Done` handler; used by
96+ /// `timed_roundtrip` to detect that the sync echo arrived.
97+ sync_done : bool ,
6998}
7099
71100impl Dispatch < WlRegistry , ( ) > for State {
@@ -175,6 +204,21 @@ impl Dispatch<ZwlrForeignToplevelHandleV1, ()> for State {
175204 }
176205}
177206
207+ impl Dispatch < wl_callback:: WlCallback , ( ) > for State {
208+ fn event (
209+ state : & mut Self ,
210+ _: & wl_callback:: WlCallback ,
211+ event : wl_callback:: Event ,
212+ ( ) : & ( ) ,
213+ _: & Connection ,
214+ _: & QueueHandle < Self > ,
215+ ) {
216+ if let wl_callback:: Event :: Done { .. } = event {
217+ state. sync_done = true ;
218+ }
219+ }
220+ }
221+
178222/// The `state` event carries a `wl_array` of native-endian `u32` state values.
179223/// A toplevel is frontmost iff the `activated` value is present in that set.
180224fn is_activated ( states : & [ u8 ] ) -> bool {
@@ -186,6 +230,116 @@ fn is_activated(states: &[u8]) -> bool {
186230 } )
187231}
188232
233+ /// Returns the milliseconds remaining until `deadline`, clamped to `[0, i32::MAX]`
234+ /// for use as a `libc::poll` timeout. Returns 0 when the deadline has passed.
235+ fn millis_until ( deadline : Instant ) -> i32 {
236+ i32:: try_from (
237+ deadline
238+ . saturating_duration_since ( Instant :: now ( ) )
239+ . as_millis ( )
240+ . min ( i32:: MAX as u128 ) ,
241+ )
242+ . unwrap_or ( i32:: MAX )
243+ }
244+
245+ /// Calls `poll(2)` on `fd` (waiting for `POLLIN | POLLERR`) with a deadline.
246+ /// Retries on `EINTR` with the remaining time. Returns `true` if the fd became
247+ /// readable, `false` on timeout or error.
248+ fn poll_fd ( fd : libc:: c_int , deadline : Instant ) -> bool {
249+ let mut pfd = libc:: pollfd {
250+ fd,
251+ events : libc:: POLLIN | libc:: POLLERR ,
252+ revents : 0 ,
253+ } ;
254+ loop {
255+ let timeout_ms = millis_until ( deadline) ;
256+ if timeout_ms == 0 {
257+ return false ;
258+ }
259+ let r = unsafe { libc:: poll ( & raw mut pfd, 1 , timeout_ms) } ;
260+ if r > 0 {
261+ return true ;
262+ }
263+ if r == 0 {
264+ return false ;
265+ }
266+ // r < 0 β check errno
267+ let e = unsafe { * libc:: __errno_location ( ) } ;
268+ if e != libc:: EINTR {
269+ return false ;
270+ }
271+ // EINTR: retry with remaining deadline
272+ }
273+ }
274+
275+ /// Sends `wl_display.sync` and spins `flush β poll β read β dispatch_pending`
276+ /// until the sync callback fires or `deadline` is reached. Returns `true` on
277+ /// success, `false` on timeout or connection error.
278+ fn timed_roundtrip (
279+ conn : & Connection ,
280+ queue : & mut EventQueue < State > ,
281+ state : & mut State ,
282+ deadline : Instant ,
283+ ) -> bool {
284+ state. sync_done = false ;
285+ let qh = queue. handle ( ) ;
286+ conn. display ( ) . sync ( & qh, ( ) ) ;
287+
288+ loop {
289+ if queue. flush ( ) . is_err ( ) {
290+ return false ;
291+ }
292+ if queue. dispatch_pending ( state) . is_err ( ) {
293+ return false ;
294+ }
295+ if state. sync_done {
296+ return true ;
297+ }
298+ if millis_until ( deadline) == 0 {
299+ return false ;
300+ }
301+
302+ match queue. prepare_read ( ) {
303+ None => {
304+ // Events are already buffered; loop back to dispatch.
305+ }
306+ Some ( guard) => {
307+ let fd = guard. connection_fd ( ) . as_raw_fd ( ) ;
308+ if !poll_fd ( fd, deadline) {
309+ // Timed out or error β candidate falls through.
310+ return false ;
311+ }
312+ if guard. read ( ) . is_err ( ) {
313+ return false ;
314+ }
315+ }
316+ }
317+ }
318+ }
319+
320+ /// Drains pending compositor events without blocking longer than `POLL_CAP_MS`.
321+ /// Used on every frontmost poll. Stale data within the cap is acceptable by
322+ /// design; errors are silently ignored so the last known state is returned.
323+ fn drain_events ( queue : & mut EventQueue < State > , state : & mut State ) {
324+ let _ = queue. flush ( ) ;
325+ let _ = queue. dispatch_pending ( state) ;
326+
327+ let deadline = Instant :: now ( ) + Duration :: from_millis ( POLL_CAP_MS ) ;
328+ match queue. prepare_read ( ) {
329+ None => {
330+ // Already had buffered events; dispatch_pending above handled them.
331+ }
332+ Some ( guard) => {
333+ let fd = guard. connection_fd ( ) . as_raw_fd ( ) ;
334+ if poll_fd ( fd, deadline) {
335+ let _ = guard. read ( ) ;
336+ let _ = queue. dispatch_pending ( state) ;
337+ }
338+ // If poll timed out, guard is dropped here and we return stale state.
339+ }
340+ }
341+ }
342+
189343/// One live Wayland session: connection + event queue + dispatch state.
190344///
191345/// Grouping all three behind a single mutex means the whole session can be
@@ -200,35 +354,36 @@ struct Session {
200354
201355impl Session {
202356 /// Open a fresh connection, bind the manager, and do the initial two
203- /// round-trips to populate the toplevel list. Returns `None` when the
204- /// compositor doesn't advertise the protocol or the connection fails.
357+ /// timed round-trips to populate the toplevel list. Returns `None` when
358+ /// the compositor doesn't advertise the protocol, the connection fails,
359+ /// or either round-trip exceeds `INIT_TIMEOUT`.
205360 fn open ( ) -> Option < Self > {
206361 let conn = Connection :: connect_to_env ( )
207362 . map_err ( |e| debug ! ( "wlr-foreign-toplevel: no Wayland connection: {e}" ) )
208363 . ok ( ) ?;
209364 let mut queue = conn. new_event_queue ( ) ;
210365 let qh = queue. handle ( ) ;
211366
212- // Registering the registry triggers `global` events on the next
213- // round-trip, where the manager is bound if the compositor advertises
214- // it. The registry handle is only needed for that round-trip.
367+ // Registering the registry triggers `global` events on the first
368+ // round-trip, where the manager is bound if the compositor advertises it.
215369 let _registry = conn. display ( ) . get_registry ( & qh, ( ) ) ;
216370 let mut state = State :: default ( ) ;
217- queue
218- . roundtrip ( & mut state)
219- . map_err ( |e| debug ! ( "wlr-foreign-toplevel: registry roundtrip failed: {e}" ) )
220- . ok ( ) ?;
371+
372+ if !timed_roundtrip ( & conn, & mut queue, & mut state, Instant :: now ( ) + INIT_TIMEOUT ) {
373+ debug ! ( "wlr-foreign-toplevel: registry round-trip timed out or failed" ) ;
374+ return None ;
375+ }
221376 if state. manager . is_none ( ) {
222377 debug ! ( "wlr-foreign-toplevel: compositor does not advertise the protocol" ) ;
223378 return None ;
224379 }
225380
226381 // Second round-trip: receive the initial toplevel list and properties,
227382 // so the first poll already has the active window.
228- queue
229- . roundtrip ( & mut state )
230- . map_err ( |e| debug ! ( "wlr-foreign-toplevel: initial roundtrip failed: {e}" ) )
231- . ok ( ) ? ;
383+ if ! timed_roundtrip ( & conn , & mut queue, & mut state , Instant :: now ( ) + INIT_TIMEOUT ) {
384+ debug ! ( "wlr-foreign-toplevel: initial toplevel round-trip timed out or failed" ) ;
385+ return None ;
386+ }
232387
233388 Some ( Self {
234389 _conn : conn,
@@ -261,19 +416,20 @@ impl FrontmostSource for WlrForeignToplevelSource {
261416
262417 // Reconnect when the compositor sent `Finished` (compositor reload /
263418 // restart) or when a prior reconnect attempt failed.
264- let needs_reconnect = guard. as_ref ( ) . map_or ( true , |s| s. state . finished ) ;
419+ let needs_reconnect = guard. as_ref ( ) . is_none_or ( |s| s. state . finished ) ;
265420 if needs_reconnect {
266421 * guard = Session :: open ( ) ;
267- match & * guard {
268- Some ( _) => info ! ( "wlr-foreign-toplevel: reconnected" ) ,
269- None => debug ! ( "wlr-foreign-toplevel: reconnect pending, retrying next poll" ) ,
422+ if guard. is_some ( ) {
423+ info ! ( "wlr-foreign-toplevel: reconnected" ) ;
424+ } else {
425+ debug ! ( "wlr-foreign-toplevel: reconnect pending, retrying next poll" ) ;
270426 }
271427 }
272428
273429 let Session { queue, state, .. } = guard. as_mut ( ) ?;
274- queue . roundtrip ( state) . ok ( ) ? ;
430+ drain_events ( queue , state) ;
275431 if state. finished {
276- // `Finished` arrived during this poll ; reconnect on the next call.
432+ // `Finished` arrived during this drain ; reconnect on the next call.
277433 return None ;
278434 }
279435
@@ -293,3 +449,26 @@ impl FrontmostSource for WlrForeignToplevelSource {
293449pub ( super ) fn candidate ( ) -> Option < Box < dyn FrontmostSource > > {
294450 WlrForeignToplevelSource :: connect ( ) . map ( |s| Box :: new ( s) as Box < dyn FrontmostSource > )
295451}
452+
453+ #[ cfg( test) ]
454+ mod tests {
455+ use std:: time:: { Duration , Instant } ;
456+
457+ use super :: millis_until;
458+
459+ #[ test]
460+ fn millis_until_elapsed_deadline_is_zero ( ) {
461+ // `deadline` is captured before the call; by the time `millis_until`
462+ // reads `Instant::now()` the deadline is at or before now, so
463+ // `saturating_duration_since` returns `Duration::ZERO` β 0 ms.
464+ let deadline = Instant :: now ( ) ;
465+ assert_eq ! ( millis_until( deadline) , 0 ) ;
466+ }
467+
468+ #[ test]
469+ fn millis_until_future_deadline_is_positive ( ) {
470+ let future = Instant :: now ( ) + Duration :: from_secs ( 10 ) ;
471+ let ms = millis_until ( future) ;
472+ assert ! ( ms > 0 && ms <= 10_000 ) ;
473+ }
474+ }
0 commit comments