use std::sync::mpsc;
use std::thread;
use std::time::Duration;
use tokio::sync::{broadcast, oneshot};
use rs_teststand::Engine;
use crate::event::{MessageEvent, PayloadPolicy};
use crate::Error;
const MESSAGE_BACKLOG: usize = 1024;
const IDLE_POLL_INTERVAL: Duration = Duration::from_millis(20);
const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10);
type Job = Box<dyn FnOnce(&Engine) + Send>;
#[derive(Debug)]
pub struct EngineHost {
jobs: mpsc::Sender<Job>,
messages: broadcast::Sender<MessageEvent>,
worker: Option<thread::JoinHandle<()>>,
}
impl EngineHost {
pub fn start() -> Result<Self, Error> {
let (job_sender, job_receiver) = mpsc::channel::<Job>();
let (message_sender, _) = broadcast::channel(MESSAGE_BACKLOG);
let (ready_sender, ready_receiver) = mpsc::channel::<Result<(), Error>>();
let messages = message_sender.clone();
let worker = thread::Builder::new()
.name("rs-teststand-engine".to_owned())
.spawn(move || Self::run(&job_receiver, &messages, &ready_sender))
.map_err(|source| Error::ThreadNotStarted {
reason: source.to_string(),
})?;
match ready_receiver.recv() {
Ok(Ok(())) => Ok(Self {
jobs: job_sender,
messages: message_sender,
worker: Some(worker),
}),
Ok(Err(error)) => Err(error),
Err(_) => Err(Error::HostStopped),
}
}
pub async fn with_engine<F, T>(&self, work: F) -> Result<T, Error>
where
F: FnOnce(&Engine) -> T + Send + 'static,
T: Send + 'static,
{
let (result_sender, result_receiver) = oneshot::channel();
let job: Job = Box::new(move |engine| {
let _ = result_sender.send(work(engine));
});
self.jobs.send(job).map_err(|_| Error::HostStopped)?;
result_receiver.await.map_err(|_| Error::ResultLost)
}
#[must_use]
pub fn subscribe(&self) -> broadcast::Receiver<MessageEvent> {
self.messages.subscribe()
}
#[must_use]
pub fn subscriber_count(&self) -> usize {
self.messages.receiver_count()
}
fn run(
jobs: &mpsc::Receiver<Job>,
messages: &broadcast::Sender<MessageEvent>,
ready: &mpsc::Sender<Result<(), Error>>,
) {
let engine = match Engine::new() {
Ok(engine) => engine,
Err(error) => {
let _ = ready.send(Err(error.into()));
return;
}
};
if let Err(error) = engine.set_ui_message_polling_enabled(true) {
let _ = ready.send(Err(error.into()));
return;
}
if ready.send(Ok(())).is_err() {
return;
}
loop {
if rs_teststand_sys::pump_thread_messages() {
Self::close_apartment(engine);
return;
}
Self::drain_messages(&engine, messages);
match jobs.recv_timeout(IDLE_POLL_INTERVAL) {
Ok(job) => job(&engine),
Err(mpsc::RecvTimeoutError::Timeout) => {}
Err(mpsc::RecvTimeoutError::Disconnected) => {
Self::drain_messages(&engine, messages);
Self::close_apartment(engine);
return;
}
}
}
}
fn close_apartment(engine: Engine) {
let _ = engine.close(SHUTDOWN_TIMEOUT);
}
fn drain_messages(engine: &Engine, messages: &broadcast::Sender<MessageEvent>) {
while matches!(engine.is_ui_message_queue_empty(), Ok(false)) {
let Ok(message) = engine.get_ui_message() else {
return;
};
if let Ok(event) = MessageEvent::from_ui_message(&message, PayloadPolicy::default()) {
let _ = messages.send(event);
}
let _ = message.acknowledge();
}
}
}
impl Drop for EngineHost {
fn drop(&mut self) {
if let Some(worker) = self.worker.take() {
let _ = worker.join();
}
}
}
#[cfg(test)]
mod tests {
use rs_teststand::UIMessageCode;
use crate::event::MessageEvent;
fn event(code: i32) -> MessageEvent {
MessageEvent {
code,
numeric: 0.0,
text: String::new(),
payload: None,
synchronous: false,
execution_id: None,
}
}
#[test]
fn a_sequence_posted_event_is_distinguishable_from_an_engine_one() {
assert!(event(UIMessageCode::USER_MESSAGE_BASE + 1).is_from_sequence());
assert!(!event(UIMessageCode::EndExecution.bits()).is_from_sequence());
}
#[test]
fn an_engine_event_resolves_to_its_name() {
assert_eq!(
event(UIMessageCode::EndExecution.bits()).engine_code(),
Some(UIMessageCode::EndExecution)
);
assert_eq!(event(UIMessageCode::USER_MESSAGE_BASE).engine_code(), None);
}
}