use std::{
collections::VecDeque,
panic::{AssertUnwindSafe, catch_unwind},
sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
mpsc::{self, RecvTimeoutError},
},
time::Duration,
};
use egui::Context;
use crate::{
diagnostics::DevMcpConfigError,
registry::lock,
types::{FixtureCall, FixtureError, FixtureResult, FixtureSpec},
};
pub type RuntimeFixtureHandler = Arc<dyn Fn(&FixtureCall) -> FixtureResult + Send + Sync>;
pub type UiFixtureHandler =
Arc<Mutex<Box<dyn FnMut(&Context, &FixtureCall) -> FixtureResult + Send>>>;
#[derive(Clone)]
pub enum FixtureHandler {
Runtime(RuntimeFixtureHandler),
Ui(UiFixtureHandler),
}
struct UiFixtureRequest {
call: FixtureCall,
handler: UiFixtureHandler,
sender: mpsc::Sender<FixtureResult>,
cancelled: Arc<AtomicBool>,
}
pub struct UiFixtureReceiver {
name: String,
receiver: mpsc::Receiver<FixtureResult>,
cancelled: Arc<AtomicBool>,
}
impl UiFixtureReceiver {
pub fn recv_timeout(self, timeout: Duration) -> FixtureResult {
match self.receiver.recv_timeout(timeout) {
Ok(result) => result,
Err(RecvTimeoutError::Timeout) => {
self.cancelled.store(true, Ordering::Release);
Err(FixtureError::new(
"timeout",
format!("fixture handler {:?} timed out", self.name),
))
}
Err(RecvTimeoutError::Disconnected) => {
self.cancelled.store(true, Ordering::Release);
Err(FixtureError::new(
"internal",
format!("fixture handler {:?} did not return a result", self.name),
))
}
}
}
}
pub enum FixtureExecution {
Ready(FixtureResult),
Queued(UiFixtureReceiver),
}
pub struct FixtureManager {
fixtures: Mutex<Vec<FixtureSpec>>,
handler: Mutex<Option<FixtureHandler>>,
pending_ui: Mutex<VecDeque<UiFixtureRequest>>,
}
impl FixtureManager {
pub fn new() -> Self {
Self {
fixtures: Mutex::new(Vec::new()),
handler: Mutex::new(None),
pending_ui: Mutex::new(VecDeque::new()),
}
}
pub fn set_fixtures(&self, fixtures: Vec<FixtureSpec>) {
for fixture in &fixtures {
if let Err(error) = fixture.validate(true) {
panic!("invalid fixture {}: {error}", fixture.name);
}
}
let mut stored = lock(&self.fixtures, "fixtures lock");
*stored = fixtures;
}
pub fn fixtures(&self) -> Vec<FixtureSpec> {
lock(&self.fixtures, "fixtures lock").clone()
}
pub fn fixtures_sorted(&self) -> Vec<FixtureSpec> {
let mut fixtures = self.fixtures();
fixtures.sort_by(|a, b| a.name.cmp(&b.name));
fixtures
}
pub fn has_fixture(&self, name: &str) -> bool {
self.fixture(name).is_some()
}
pub fn fixture(&self, name: &str) -> Option<FixtureSpec> {
lock(&self.fixtures, "fixtures lock")
.iter()
.find(|fixture| fixture.name == name)
.cloned()
}
pub fn set_handler(&self, handler: FixtureHandler) -> Result<(), DevMcpConfigError> {
let mut stored = lock(&self.handler, "fixture handler lock");
if stored.is_some() {
return Err(DevMcpConfigError::new(
"duplicate_fixture_handler",
"fixture handler is already registered",
));
}
*stored = Some(handler);
Ok(())
}
pub fn start_fixture(&self, call: FixtureCall) -> FixtureExecution {
let handler = lock(&self.handler, "fixture handler lock").clone();
match handler {
Some(FixtureHandler::Runtime(handler)) => {
FixtureExecution::Ready(run_runtime_handler(&handler, &call))
}
Some(FixtureHandler::Ui(handler)) => {
let (sender, receiver) = mpsc::channel();
let name = call.name.clone();
let cancelled = Arc::new(AtomicBool::new(false));
lock(&self.pending_ui, "pending fixture lock").push_back(UiFixtureRequest {
call,
handler,
sender,
cancelled: Arc::clone(&cancelled),
});
FixtureExecution::Queued(UiFixtureReceiver {
name,
receiver,
cancelled,
})
}
None => FixtureExecution::Ready(Err(FixtureError::new(
"no_handler",
"no fixture handler registered",
))),
}
}
pub fn drain_ui(&self, ctx: &Context) {
let request = loop {
let request = {
let mut pending = lock(&self.pending_ui, "pending fixture lock");
pending.pop_front()
};
match request {
Some(request) if request.cancelled.load(Ordering::Acquire) => {}
request => break request,
}
};
if let Some(request) = request {
let result = run_ui_handler(&request.handler, ctx, &request.call);
drop(request.sender.send(result));
}
}
}
fn run_runtime_handler(handler: &RuntimeFixtureHandler, call: &FixtureCall) -> FixtureResult {
catch_unwind(AssertUnwindSafe(|| handler(call)))
.unwrap_or_else(|panic| Err(FixtureError::handler_panic(&call.name, panic.as_ref())))
}
fn run_ui_handler(handler: &UiFixtureHandler, ctx: &Context, call: &FixtureCall) -> FixtureResult {
catch_unwind(AssertUnwindSafe(|| {
let mut handler = lock(handler, "UI fixture handler lock");
handler(ctx, call)
}))
.unwrap_or_else(|panic| Err(FixtureError::handler_panic(&call.name, panic.as_ref())))
}
#[cfg(test)]
mod tests {
use std::{
collections::BTreeMap,
sync::{
Arc, Mutex,
atomic::{AtomicUsize, Ordering},
},
time::Duration,
};
use egui::Context;
use super::{FixtureExecution, FixtureHandler, FixtureManager};
use crate::types::{FixtureCall, FixtureResponse, FixtureSpec, WidgetValue};
fn empty_call(name: &str) -> FixtureCall {
FixtureCall {
name: name.to_string(),
params: FixtureSpec::new(name, "Test fixture")
.validate_params(BTreeMap::new())
.expect("params"),
}
}
#[test]
fn ui_fixture_drains_queued_call() {
let manager = FixtureManager::new();
let called = Arc::new(Mutex::new(None));
let called_clone = Arc::clone(&called);
manager
.set_handler(FixtureHandler::Ui(Arc::new(Mutex::new(Box::new(
move |_ctx, call| {
*called_clone.lock().expect("called lock") = Some(call.name.clone());
Ok(FixtureResponse::new().value("done", true))
},
)))))
.expect("handler");
let receiver = match manager.start_fixture(empty_call("ui.ready")) {
FixtureExecution::Queued(receiver) => receiver,
FixtureExecution::Ready(_) => panic!("expected queued UI fixture"),
};
manager.drain_ui(&Context::default());
let response = receiver
.recv_timeout(Duration::from_millis(1))
.expect("fixture response");
assert_eq!(
called.lock().expect("called lock").as_deref(),
Some("ui.ready")
);
assert_eq!(response.values.get("done"), Some(&WidgetValue::Bool(true)));
}
#[test]
fn timed_out_ui_fixture_is_not_applied_later() {
let manager = FixtureManager::new();
let calls = Arc::new(AtomicUsize::new(0));
let calls_clone = Arc::clone(&calls);
manager
.set_handler(FixtureHandler::Ui(Arc::new(Mutex::new(Box::new(
move |_ctx, _call| {
calls_clone.fetch_add(1, Ordering::Relaxed);
Ok(FixtureResponse::new())
},
)))))
.expect("handler");
let receiver = match manager.start_fixture(empty_call("ui.timeout")) {
FixtureExecution::Queued(receiver) => receiver,
FixtureExecution::Ready(_) => panic!("expected queued UI fixture"),
};
let error = receiver
.recv_timeout(Duration::ZERO)
.expect_err("fixture should time out");
assert_eq!(error.code, "timeout");
manager.drain_ui(&Context::default());
assert_eq!(calls.load(Ordering::Relaxed), 0);
}
}