use std::cell::RefCell;
use std::collections::HashMap;
use std::os::fd::{AsFd as _, AsRawFd as _, RawFd};
use std::path::Path;
use std::rc::Rc;
use std::time::{Duration, Instant};
use crate::SessionError;
use crate::session::{Backend, DeviceId, RawEvent};
pub const DEFAULT_ENABLE_TIMEOUT: Duration = Duration::from_secs(3);
const DISPATCH_SLICE_MS: u128 = 100;
type Inbox = Rc<RefCell<Vec<RawEvent>>>;
#[derive(Debug)]
pub struct LibseatBackend {
seat: libseat::Seat,
devices: HashMap<i32, libseat::Device>,
next_id: i32,
inbox: Inbox,
poll_fd: Option<RawFd>,
}
impl LibseatBackend {
pub fn open() -> Result<Self, SessionError> {
Self::open_within(Some(DEFAULT_ENABLE_TIMEOUT))
}
pub fn open_within(timeout: Option<Duration>) -> Result<Self, SessionError> {
let inbox: Inbox = Rc::new(RefCell::new(Vec::new()));
let listener = {
let inbox = Rc::clone(&inbox);
move |_: &mut libseat::SeatRef, event: libseat::SeatEvent| {
inbox.borrow_mut().push(match event {
libseat::SeatEvent::Enable => RawEvent::Enable,
libseat::SeatEvent::Disable => RawEvent::Disable,
});
}
};
let mut seat = libseat::Seat::open(listener).map_err(|errno| {
drmkit_log::log_debug!("no seat: {errno}");
SessionError::NoBackend
})?;
let poll_fd = seat.get_fd().ok().map(|fd| fd.as_raw_fd());
let mut backend = Self {
seat,
devices: HashMap::new(),
next_id: 1,
inbox,
poll_fd,
};
backend.wait_for_activation(timeout)?;
Ok(backend)
}
pub fn name(&mut self) -> &str {
self.seat.name()
}
fn wait_for_activation(&mut self, timeout: Option<Duration>) -> Result<(), SessionError> {
let Self { seat, inbox, .. } = self;
wait_until_active(
|timeout_ms| {
seat.dispatch(timeout_ms)
.map(|_| ())
.map_err(|errno| SessionError::Seat(format!("dispatch: {errno}")))
},
inbox,
timeout,
)?;
drmkit_log::log_info!("seat {} is ours", self.seat.name());
Ok(())
}
}
fn wait_until_active(
mut dispatch: impl FnMut(i32) -> Result<(), SessionError>,
inbox: &Inbox,
timeout: Option<Duration>,
) -> Result<(), SessionError> {
let deadline = timeout.map(|timeout| Instant::now() + timeout);
loop {
let timeout_ms = match deadline {
None => -1,
Some(deadline) => {
let left = deadline.saturating_duration_since(Instant::now());
if left.is_zero() {
drmkit_log::log_warn!(
"seat not enabled within {} ms; open devices directly instead",
timeout.unwrap_or_default().as_millis()
);
return Err(SessionError::NoBackend);
}
i32::try_from(left.as_millis().min(DISPATCH_SLICE_MS)).unwrap_or(0)
}
};
dispatch(timeout_ms)?;
if drain_through_activation(&mut inbox.borrow_mut()) {
return Ok(());
}
}
}
fn drain_through_activation(events: &mut Vec<RawEvent>) -> bool {
if let Some(enable) = events.iter().position(|event| *event == RawEvent::Enable) {
events.drain(..=enable);
true
} else {
events.clear();
false
}
}
impl Drop for LibseatBackend {
fn drop(&mut self) {
for (_, device) in self.devices.drain() {
let _ = self.seat.close_device(device);
}
}
}
impl Backend for LibseatBackend {
fn open_device(&mut self, path: &Path) -> Result<(DeviceId, RawFd), SessionError> {
if path.to_str().is_none_or(|path| path.contains('\0')) {
return Err(SessionError::Seat(format!(
"{} is not a path libseat can take",
path.display()
)));
}
let device = self
.seat
.open_device(&path)
.map_err(|errno| SessionError::Seat(format!("open {}: {errno}", path.display())))?;
let fd = device.as_fd().as_raw_fd();
let id = self.next_id;
self.next_id += 1;
self.devices.insert(id, device);
Ok((DeviceId(id), fd))
}
fn close_device(&mut self, id: DeviceId) -> Result<(), SessionError> {
let Some(device) = self.devices.remove(&id.0) else {
return Ok(());
};
self.seat
.close_device(device)
.map_err(|errno| SessionError::Seat(format!("close: {errno}")))
}
fn acknowledge_pause(&mut self) -> Result<(), SessionError> {
self.seat
.disable()
.map_err(|errno| SessionError::Seat(format!("disable: {errno}")))
}
fn switch_session(&mut self, session: i32) -> Result<(), SessionError> {
self.seat
.switch_session(session)
.map_err(|errno| SessionError::Seat(format!("switch to {session}: {errno}")))
}
fn poll_fd(&self) -> Option<RawFd> {
self.poll_fd
}
fn dispatch(
&mut self,
timeout_ms: i32,
events: &mut Vec<RawEvent>,
) -> Result<(), SessionError> {
self.seat
.dispatch(timeout_ms)
.map_err(|errno| SessionError::Seat(format!("dispatch: {errno}")))?;
events.append(&mut self.inbox.borrow_mut());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn activation_swallows_the_enable_that_announced_it() {
let mut events = vec![RawEvent::Enable];
assert!(drain_through_activation(&mut events));
assert!(events.is_empty());
}
#[test]
fn a_switch_away_during_startup_still_reaches_the_session() {
let mut events = vec![RawEvent::Enable, RawEvent::Disable];
assert!(drain_through_activation(&mut events));
assert_eq!(events, vec![RawEvent::Disable]);
}
#[test]
fn without_an_enable_the_seat_is_not_ours_yet() {
let mut events = vec![RawEvent::Disable];
assert!(!drain_through_activation(&mut events));
assert!(events.is_empty(), "and nothing is left to act on");
}
#[test]
fn a_seat_that_never_enables_times_out_as_no_seat() {
let inbox: Inbox = Rc::new(RefCell::new(Vec::new()));
let mut waits = Vec::new();
let started = Instant::now();
let result = wait_until_active(
|timeout_ms| {
waits.push(timeout_ms);
std::thread::sleep(Duration::from_millis(
u64::try_from(timeout_ms.max(0)).unwrap_or(0),
));
Ok(())
},
&inbox,
Some(Duration::from_millis(50)),
);
assert_eq!(result, Err(SessionError::NoBackend));
assert!(started.elapsed() < Duration::from_secs(2));
assert!(!waits.is_empty());
assert!(
waits.iter().all(|ms| (0..=50).contains(ms)),
"no dispatch may block past the deadline: {waits:?}"
);
}
#[test]
fn the_first_enable_ends_the_wait() {
let inbox: Inbox = Rc::new(RefCell::new(Vec::new()));
let mut calls = 0;
let result = wait_until_active(
|_| {
calls += 1;
if calls == 3 {
inbox
.borrow_mut()
.extend([RawEvent::Enable, RawEvent::Disable]);
}
Ok(())
},
&inbox,
Some(DEFAULT_ENABLE_TIMEOUT),
);
assert_eq!(result, Ok(()));
assert_eq!(calls, 3);
assert_eq!(*inbox.borrow(), [RawEvent::Disable]);
}
#[test]
fn an_unbounded_wait_blocks_in_dispatch() {
let inbox: Inbox = Rc::new(RefCell::new(Vec::new()));
let mut waits = Vec::new();
let result = wait_until_active(
|timeout_ms| {
waits.push(timeout_ms);
inbox.borrow_mut().push(RawEvent::Enable);
Ok(())
},
&inbox,
None,
);
assert_eq!(result, Ok(()));
assert_eq!(waits, [-1]);
}
#[test]
fn a_failed_dispatch_is_reported() {
let inbox: Inbox = Rc::new(RefCell::new(Vec::new()));
let result = wait_until_active(
|_| Err(SessionError::Seat("dispatch: EIO".to_owned())),
&inbox,
Some(DEFAULT_ENABLE_TIMEOUT),
);
assert_eq!(result, Err(SessionError::Seat("dispatch: EIO".to_owned())));
}
}