use std::cmp::Ordering;
use std::fs::File;
use std::io::Write;
use std::os::raw::c_uint;
use std::{fs, io};
use lazy_static::lazy_static;
use regex::Regex;
use thiserror::Error;
use tracing::instrument;
use crate::prelude::*;
use crate::{ensure_logs_setup, ffi};
const DEVICE_CARDS_DIR: &str = "/dev/dri";
const REMOVE_ALL_FILE: &str = "/sys/devices/evdi/remove_all";
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct DeviceNode {
pub(crate) id: i32,
}
impl DeviceNode {
pub fn get() -> Option<Self> {
if let Ok(mut devices) = Self::list_available() {
devices.pop()
} else {
None
}
}
pub fn status(&self) -> DeviceNodeStatus {
ensure_logs_setup();
let sys = unsafe { ffi::evdi_check_device(self.id) };
DeviceNodeStatus::from(sys)
}
pub unsafe fn open(&self) -> Result<UnconnectedHandle, OpenDeviceError> {
ensure_logs_setup();
match check_kernel_mod() {
KernelModStatus::NotInstalled => return Err(OpenDeviceError::KernelModuleNotInstalled),
KernelModStatus::Outdated => return Err(OpenDeviceError::KernelModuleOutdated),
KernelModStatus::Compatible => (),
}
match self.status() {
DeviceNodeStatus::Unrecognized => Err(OpenDeviceError::NotEvdiDevice),
DeviceNodeStatus::NotPresent => Err(OpenDeviceError::NonexistentDevice),
DeviceNodeStatus::Available => {
let sys = unsafe { ffi::evdi_open(self.id) };
if !sys.is_null() {
info!("Opened device {}", self.id);
Ok(UnconnectedHandle::new(self.clone(), sys))
} else {
Err(OpenDeviceError::Unknown)
}
}
}
}
#[instrument]
pub fn list_available() -> io::Result<Vec<Self>> {
lazy_static! {
static ref RE: Regex = Regex::new(r"^card([0-9]+)$").unwrap();
}
let mut devices: Vec<DeviceNode> = vec![];
for entry in fs::read_dir(DEVICE_CARDS_DIR)? {
let name_os = entry?.file_name();
let name = name_os.to_string_lossy();
let id = RE
.captures(&name)
.and_then(|caps| caps.get(1))
.and_then(|id| id.as_str().parse::<i32>().ok());
if let Some(id) = id {
devices.push(DeviceNode::new(id))
}
}
let mut available: Vec<DeviceNode> = devices
.into_iter()
.filter(|device| device.status() == DeviceNodeStatus::Available)
.collect();
available.sort();
Ok(available)
}
#[instrument]
pub fn add() -> bool {
ensure_logs_setup();
let status = unsafe { ffi::evdi_add_device() };
status > 0
}
#[instrument]
pub fn remove_all() -> io::Result<()> {
let mut f = File::options().write(true).open(REMOVE_ALL_FILE)?;
f.write_all("1".as_ref())?;
f.flush()?;
Ok(())
}
pub fn new(id: i32) -> Self {
DeviceNode { id }
}
}
impl Ord for DeviceNode {
fn cmp(&self, other: &Self) -> Ordering {
self.id.cmp(&other.id)
}
}
impl PartialOrd for DeviceNode {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[derive(Debug, PartialEq)]
#[cfg_attr(
feature = "serde",
derive(serde_crate::Serialize, serde_crate::Deserialize),
serde(crate = "serde_crate")
)]
pub enum DeviceNodeStatus {
Available,
Unrecognized,
NotPresent,
}
#[derive(Debug, Error)]
pub enum OpenDeviceError {
#[error("The kernel module evdi is not installed")]
KernelModuleNotInstalled,
#[error("The kernel module is too outdated to use with this library version")]
KernelModuleOutdated,
#[error("The device node does not exist")]
NonexistentDevice,
#[error("The device node is not an evdi device node")]
NotEvdiDevice,
#[error("The call to the c library failed. Maybe the device node is an incompatible version?")]
Unknown,
}
impl DeviceNodeStatus {
fn from(sys: c_uint) -> Self {
match sys {
ffi::EVDI_STATUS_AVAILABLE => DeviceNodeStatus::Available,
ffi::EVDI_STATUS_UNRECOGNIZED => DeviceNodeStatus::Unrecognized,
ffi::EVDI_STATUS_NOT_PRESENT => DeviceNodeStatus::NotPresent,
_ => panic!("Invalid device status {}", sys),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_common::*;
#[ltest]
fn default_device_is_not_evdi() {
let status = DeviceNode::new(0).status();
assert_eq!(status, DeviceNodeStatus::Unrecognized);
}
#[ltest]
fn nonexistent_device_has_proper_status() {
let status = DeviceNode::new(4200).status();
assert_eq!(status, DeviceNodeStatus::NotPresent);
}
#[ltest]
fn add_fails_without_superuser() {
let result = DeviceNode::add();
assert_eq!(result, false);
}
#[ltest]
fn remove_all_fails_without_superuser() {
let result = DeviceNode::remove_all();
assert!(result.is_err())
}
#[ltest]
fn list_available_contains_at_least_one_device() {
let results = DeviceNode::list_available().unwrap();
assert!(!results.is_empty(), "No available devices. Have you added at least one device by running the binary add_device at least once?");
}
#[ltest]
fn get_returns_a_device() {
let result = DeviceNode::get();
assert!(result.is_some());
}
#[ltest]
fn can_open() {
let device = DeviceNode::get().unwrap();
device.open().unwrap();
}
#[ltest]
fn opening_nonexistent_device_fails() {
let device = DeviceNode::new(4200);
match device.open() {
Err(OpenDeviceError::NonexistentDevice) => (),
_ => panic!(),
}
}
#[ltest]
fn opening_non_evdi_device_fails() {
let device = DeviceNode::new(0);
match device.open() {
Err(OpenDeviceError::NotEvdiDevice) => (),
_ => panic!(),
}
}
}