use std::any::Any;
use std::any::type_name;
use std::num::NonZeroU32;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::Weak;
use bevy::prelude::Component;
use bevy::prelude::Reflect;
use bevy::prelude::World;
use thiserror::Error;
use crate::ApplyPermit;
use crate::AttemptId;
use crate::AttemptProgress;
use crate::DeviceAccessError;
use crate::DeviceEndpoint;
use crate::DeviceScan;
use crate::LastKnownGoodConfiguration;
pub trait DeviceReporter: Send + Sync + 'static {
fn discover(&mut self) -> DiscoveryWork;
}
pub enum DiscoveryWork {
Immediate(MainThreadDiscoveryJob),
Background(DiscoveryJob),
}
pub struct MainThreadDiscoveryJob(Box<dyn FnOnce(&mut World) -> DeviceScan + 'static>);
impl MainThreadDiscoveryJob {
#[must_use]
pub fn new(run: impl FnOnce(&mut World) -> DeviceScan + 'static) -> Self { Self(Box::new(run)) }
pub(crate) fn run(self, world: &mut World) -> DeviceScan { self.0(world) }
}
pub struct DiscoveryJob(
Mutex<Box<dyn FnOnce(DiscoveryProgressSender) -> DeviceScan + Send + 'static>>,
);
impl DiscoveryJob {
#[must_use]
pub fn new(run: impl FnOnce(DiscoveryProgressSender) -> DeviceScan + Send + 'static) -> Self {
Self(Mutex::new(Box::new(run)))
}
pub(crate) fn run(self, discovery_progress_sender: DiscoveryProgressSender) -> DeviceScan {
let run = self
.0
.into_inner()
.unwrap_or_else(std::sync::PoisonError::into_inner);
run(discovery_progress_sender)
}
}
#[derive(Clone)]
pub struct DiscoveryProgressSender(Weak<DiscoveryProgressMailbox>);
struct DiscoveryProgressMailbox {
pending: Mutex<PendingDiscoveryProgress>,
}
pub(crate) struct DiscoveryProgressReceiver(Arc<DiscoveryProgressMailbox>);
pub(crate) enum PendingDiscoveryProgress {
NoUpdate,
Latest(DiscoveryProgress),
}
impl DiscoveryProgressSender {
pub(crate) fn scheduler_mailbox() -> (Self, DiscoveryProgressReceiver) {
let discovery_progress_mailbox = Arc::new(DiscoveryProgressMailbox {
pending: Mutex::new(PendingDiscoveryProgress::NoUpdate),
});
(
Self(Arc::downgrade(&discovery_progress_mailbox)),
DiscoveryProgressReceiver(discovery_progress_mailbox),
)
}
pub fn send(
&self,
discovery_progress: DiscoveryProgress,
) -> Result<(), DiscoveryProgressSendError> {
let discovery_progress_mailbox = self
.0
.upgrade()
.ok_or(DiscoveryProgressSendError::SchedulerStopped)?;
*discovery_progress_mailbox
.pending
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) =
PendingDiscoveryProgress::Latest(discovery_progress);
Ok(())
}
}
impl DiscoveryProgressReceiver {
pub(crate) fn take_latest(&self) -> PendingDiscoveryProgress {
let mut pending = self
.0
.pending
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
std::mem::replace(&mut *pending, PendingDiscoveryProgress::NoUpdate)
}
}
#[derive(Clone, PartialEq, Eq, Debug, Reflect)]
pub enum DiscoveryProgress {
Indeterminate,
Measured {
completed: u32,
total: NonZeroU32,
},
}
#[derive(Debug, Error)]
pub enum DiscoveryProgressSendError {
#[error("discovery scheduler stopped receiving background progress")]
SchedulerStopped,
}
pub trait EndpointDriver: Send + Sync + 'static {
type Configuration: Reflect + Component;
fn capture(
&mut self,
world: &mut World,
endpoint: &DeviceEndpoint,
) -> CaptureOutcome<Self::Configuration>;
fn start_apply(
&mut self,
world: &mut World,
endpoint: &DeviceEndpoint,
configuration: &Self::Configuration,
attempt: AttemptId,
permit: ApplyPermit,
);
fn poll(&mut self, world: &mut World, attempt: AttemptId) -> AttemptProgress;
}
pub enum CaptureOutcome<Configuration> {
Read(Configuration),
NotReadable,
ReadFailed(DeviceAccessError),
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum DriverContractError {
#[error("endpoint driver `{driver_id:?}` is not registered")]
DriverNotRegistered {
driver_id: crate::DriverId,
},
#[error("endpoint driver registry entry expected concrete driver `{expected_driver}`")]
DriverTypeMismatch {
expected_driver: &'static str,
},
#[error(
"endpoint driver expected configuration `{expected_configuration}` but received `{received_configuration}`"
)]
ConfigurationTypeMismatch {
expected_configuration: &'static str,
received_configuration: String,
},
#[error("state-issued apply request for role `{role}` has no last-known-good configuration")]
LastKnownGoodConfigurationUnavailable {
role: crate::RoleKey,
},
}
type ErasedDriver = dyn Any + Send + Sync;
type CaptureFunction =
fn(
&mut ErasedDriver,
&mut World,
&DeviceEndpoint,
) -> Result<CaptureOutcome<LastKnownGoodConfiguration>, DriverContractError>;
type StartApplyFunction = fn(
&mut ErasedDriver,
&mut World,
&DeviceEndpoint,
&dyn Reflect,
AttemptId,
ApplyPermit,
) -> Result<(), DriverContractError>;
type PollFunction =
fn(&mut ErasedDriver, &mut World, AttemptId) -> Result<AttemptProgress, DriverContractError>;
pub(crate) struct DriverEntry {
driver: Box<ErasedDriver>,
capture: CaptureFunction,
start_apply: StartApplyFunction,
poll: PollFunction,
}
impl DriverEntry {
pub(crate) fn new<Driver>(driver: Driver) -> Self
where
Driver: EndpointDriver,
{
Self {
driver: Box::new(driver),
capture: capture_driver::<Driver>,
start_apply: start_apply_driver::<Driver>,
poll: poll_driver::<Driver>,
}
}
pub(crate) fn capture(
&mut self,
world: &mut World,
endpoint: &DeviceEndpoint,
) -> Result<CaptureOutcome<LastKnownGoodConfiguration>, DriverContractError> {
(self.capture)(self.driver.as_mut(), world, endpoint)
}
pub(crate) fn start_apply(
&mut self,
world: &mut World,
endpoint: &DeviceEndpoint,
configuration: &dyn Reflect,
attempt: AttemptId,
permit: ApplyPermit,
) -> Result<(), DriverContractError> {
(self.start_apply)(
self.driver.as_mut(),
world,
endpoint,
configuration,
attempt,
permit,
)
}
pub(crate) fn poll(
&mut self,
world: &mut World,
attempt: AttemptId,
) -> Result<AttemptProgress, DriverContractError> {
(self.poll)(self.driver.as_mut(), world, attempt)
}
}
fn capture_driver<Driver>(
driver: &mut ErasedDriver,
world: &mut World,
endpoint: &DeviceEndpoint,
) -> Result<CaptureOutcome<LastKnownGoodConfiguration>, DriverContractError>
where
Driver: EndpointDriver,
{
let driver = typed_driver_mut::<Driver>(driver)?;
Ok(match driver.capture(world, endpoint) {
CaptureOutcome::Read(configuration) => {
CaptureOutcome::Read(LastKnownGoodConfiguration::known(configuration))
},
CaptureOutcome::NotReadable => CaptureOutcome::NotReadable,
CaptureOutcome::ReadFailed(error) => CaptureOutcome::ReadFailed(error),
})
}
fn start_apply_driver<Driver>(
driver: &mut ErasedDriver,
world: &mut World,
endpoint: &DeviceEndpoint,
configuration: &dyn Reflect,
attempt: AttemptId,
permit: ApplyPermit,
) -> Result<(), DriverContractError>
where
Driver: EndpointDriver,
{
let driver = typed_driver_mut::<Driver>(driver)?;
let Some(configuration) = configuration
.as_any()
.downcast_ref::<Driver::Configuration>()
else {
return Err(DriverContractError::ConfigurationTypeMismatch {
expected_configuration: type_name::<Driver::Configuration>(),
received_configuration: configuration.reflect_type_path().to_owned(),
});
};
driver.start_apply(world, endpoint, configuration, attempt, permit);
Ok(())
}
fn poll_driver<Driver>(
driver: &mut ErasedDriver,
world: &mut World,
attempt: AttemptId,
) -> Result<AttemptProgress, DriverContractError>
where
Driver: EndpointDriver,
{
Ok(typed_driver_mut::<Driver>(driver)?.poll(world, attempt))
}
fn typed_driver_mut<Driver>(driver: &mut ErasedDriver) -> Result<&mut Driver, DriverContractError>
where
Driver: EndpointDriver,
{
driver
.downcast_mut::<Driver>()
.ok_or_else(|| DriverContractError::DriverTypeMismatch {
expected_driver: type_name::<Driver>(),
})
}
#[cfg(test)]
mod tests {
use std::any::TypeId;
use std::any::type_name;
use std::error::Error;
use std::num::NonZeroU32;
use std::rc::Rc;
use bevy::app::App;
use bevy::ecs::reflect::AppTypeRegistry;
use bevy::ecs::reflect::ReflectComponent;
use bevy::prelude::Component;
use bevy::prelude::Reflect;
use bevy::prelude::Resource;
use bevy::prelude::World;
use super::CaptureOutcome;
use super::DiscoveryProgressSender;
use super::DriverContractError;
use super::DriverEntry;
use super::EndpointDriver;
use super::MainThreadDiscoveryJob;
use super::PendingDiscoveryProgress;
use crate::ApplyPermit;
use crate::AttemptId;
use crate::AttemptProgress;
use crate::DeviceEndpoint;
use crate::DeviceIdSource;
use crate::DeviceKey;
use crate::DeviceKind;
use crate::DeviceScan;
use crate::DiscoveryProgress;
use crate::DiscoveryProgressSendError;
use crate::EndpointId;
use crate::LastKnownGoodConfiguration;
use crate::ReportedId;
use crate::SchemeName;
#[derive(Component, Reflect)]
#[reflect(Component)]
struct TestConfiguration;
#[derive(Component, Reflect)]
#[reflect(Component)]
struct OtherConfiguration;
#[derive(Resource)]
struct Running;
struct MainThreadDiscoverySource {
name: Rc<str>,
}
struct TestDriver;
impl EndpointDriver for TestDriver {
type Configuration = TestConfiguration;
fn capture(
&mut self,
_: &mut World,
_: &DeviceEndpoint,
) -> CaptureOutcome<Self::Configuration> {
CaptureOutcome::NotReadable
}
fn start_apply(
&mut self,
_: &mut World,
_: &DeviceEndpoint,
_: &Self::Configuration,
_: AttemptId,
_: ApplyPermit,
) {
}
fn poll(&mut self, _: &mut World, _: AttemptId) -> AttemptProgress {
AttemptProgress::Pending
}
}
#[test]
fn main_thread_discovery_job_reads_non_send_resource_and_returns_owned_whole_set() {
let mut world = World::new();
world.insert_non_send(MainThreadDiscoverySource {
name: Rc::from("monitor-api"),
});
let main_thread_discovery_job = MainThreadDiscoveryJob::new(|world| {
let main_thread_discovery_source = world.non_send::<MainThreadDiscoverySource>();
assert_eq!(main_thread_discovery_source.name.as_ref(), "monitor-api");
DeviceScan::Complete(Vec::new())
});
let device_scan = main_thread_discovery_job.run(&mut world);
assert!(matches!(
device_scan,
DeviceScan::Complete(device_records) if device_records.is_empty()
));
}
#[test]
fn progress_mailbox_coalesces_to_latest_measured_and_indeterminate_updates() {
const UPDATE_COUNT: u32 = 1_000;
let (discovery_progress_sender, discovery_progress_receiver) =
DiscoveryProgressSender::scheduler_mailbox();
let total = NonZeroU32::new(UPDATE_COUNT).unwrap_or(NonZeroU32::MIN);
for completed in 0..UPDATE_COUNT {
assert!(
discovery_progress_sender
.send(DiscoveryProgress::Measured { completed, total })
.is_ok(),
"scheduler must retain the progress receiver"
);
}
assert!(matches!(
discovery_progress_receiver.take_latest(),
PendingDiscoveryProgress::Latest(DiscoveryProgress::Measured {
completed,
total,
}) if completed == UPDATE_COUNT - 1 && total.get() == UPDATE_COUNT
));
assert!(matches!(
discovery_progress_receiver.take_latest(),
PendingDiscoveryProgress::NoUpdate
));
for completed in 0..UPDATE_COUNT {
assert!(
discovery_progress_sender
.send(DiscoveryProgress::Measured { completed, total })
.is_ok(),
"scheduler must retain the progress receiver"
);
}
assert!(
discovery_progress_sender
.send(DiscoveryProgress::Indeterminate)
.is_ok(),
"scheduler must retain the progress receiver"
);
assert!(matches!(
discovery_progress_receiver.take_latest(),
PendingDiscoveryProgress::Latest(DiscoveryProgress::Indeterminate)
));
}
#[test]
fn progress_sender_reports_stopped_after_scheduler_releases_receiver() {
let (discovery_progress_sender, discovery_progress_receiver) =
DiscoveryProgressSender::scheduler_mailbox();
drop(discovery_progress_receiver);
assert!(matches!(
discovery_progress_sender.send(DiscoveryProgress::Indeterminate),
Err(DiscoveryProgressSendError::SchedulerStopped)
));
}
#[test]
fn wrong_configuration_type_returns_contract_error_without_stopping_the_app()
-> Result<(), Box<dyn Error>> {
let mut app = App::new();
let mut driver_entry = DriverEntry::new(TestDriver);
let configuration = LastKnownGoodConfiguration::known(OtherConfiguration);
let result = driver_entry.start_apply(
app.world_mut(),
&display_endpoint()?,
configuration
.as_reflect()
.map_err(|_| "missing configuration")?,
AttemptId::default(),
ApplyPermit::restore_only(),
);
assert!(matches!(
result,
Err(DriverContractError::ConfigurationTypeMismatch { .. })
));
app.insert_resource(Running);
assert!(app.world().contains_resource::<Running>());
Ok(())
}
#[test]
fn wrong_driver_type_returns_contract_error_without_stopping_the_app() {
let mut app = App::new();
let mut driver_entry = DriverEntry::new(TestDriver);
driver_entry.driver = Box::new(());
let result = driver_entry.poll(app.world_mut(), AttemptId::default());
assert!(matches!(
result,
Err(DriverContractError::DriverTypeMismatch { expected_driver })
if expected_driver == type_name::<TestDriver>()
));
app.insert_resource(Running);
assert!(app.world().contains_resource::<Running>());
}
#[test]
fn driver_configuration_registers_reflect_component_metadata() {
let app = App::new();
let type_registry = app.world().resource::<AppTypeRegistry>().read();
let type_id = TypeId::of::<TestConfiguration>();
assert!(type_registry.contains(type_id));
assert!(
type_registry
.get_type_data::<ReflectComponent>(type_id)
.is_some()
);
drop(type_registry);
}
fn display_endpoint() -> Result<DeviceEndpoint, Box<dyn Error>> {
Ok(DeviceEndpoint {
device: DeviceKey {
kind: DeviceKind::Display,
id: DeviceIdSource::Reported {
scheme: SchemeName::new("edid-serial")?,
value: ReportedId::new("DELL-U2723QE-9J4K2H3")?,
},
},
id: EndpointId::Whole,
})
}
}