use std::sync::mpsc;
use std::thread;
use std::time::Duration;
use tokio::sync::{broadcast, oneshot};
use rs_teststand::{Engine, UIMessage, UIMessageCode};
use rs_teststand_serde::PropertyObjectValue as _;
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);
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MessageEvent {
pub code: i32,
pub numeric: f64,
pub text: String,
pub payload: Option<String>,
pub synchronous: bool,
pub execution_id: Option<i32>,
}
impl MessageEvent {
#[must_use]
pub const fn is_from_sequence(&self) -> bool {
UIMessageCode::is_user_message(self.code)
}
#[must_use]
pub fn engine_code(&self) -> Option<UIMessageCode> {
UIMessageCode::from_bits(self.code).ok()
}
pub fn from_ui_message(message: &UIMessage, policy: PayloadPolicy) -> Result<Self, Error> {
let code = message.event()?;
let payload = match message.activex_data()? {
Some(container) if policy.admits(code) => {
Some(serde_json::to_string(&container.to_value()?)?)
}
Some(_) | None => None,
};
Ok(Self {
code,
numeric: message.numeric_data()?,
text: message.string_data()?,
payload,
synchronous: message.is_synchronous()?,
execution_id: message
.execution()?
.map(|execution| execution.id())
.transpose()?,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PayloadPolicy {
#[default]
SequenceMessagesOnly,
Everything,
Never,
}
impl PayloadPolicy {
#[must_use]
pub const fn admits(self, code: i32) -> bool {
match self {
Self::Everything => true,
Self::Never => false,
Self::SequenceMessagesOnly => UIMessageCode::is_user_message(code),
}
}
}
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 super::{MessageEvent, UIMessageCode};
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);
}
}