use std::future::Future;
use std::time::Duration;
use bacnet_client::client::BACnetClient;
use bacnet_transport::port::TransportPort;
use bacnet_transport::sc::ScEvent;
use tokio::sync::broadcast;
use tokio::sync::broadcast::error::RecvError;
use tokio::sync::mpsc;
use super::EventSink;
use crate::tui::app::sc::{describe, Severity};
use crate::tui::message::WorkerEvent;
use crate::tui::sc::{CertSummary, LinkEvent, LinkSnapshot, ScReport};
const STATUS_EVERY: Duration = Duration::from_secs(1);
pub(crate) async fn run<T: TransportPort + 'static>(
client: &BACnetClient<T>,
sink: &EventSink,
stop: mpsc::Receiver<()>,
early: Option<broadcast::Receiver<ScEvent>>,
) {
#[cfg(feature = "sc-tls")]
{
use std::any::Any;
type Sc = bacnet_transport::sc::ScTransport<bacnet_transport::sc_tls::TlsWebSocket>;
if let Some(sc) = (client.transport() as &dyn Any).downcast_ref::<Sc>() {
const CHAIN_WAIT: Duration = Duration::from_secs(2);
let events = early.unwrap_or_else(|| sc.connection_events());
let status = || sc.connection_status().into();
let chain = || async {
let info = tokio::time::timeout(CHAIN_WAIT, sc.hub_peer_info()).await;
info.ok()
.flatten()
.map(|info| crate::tui::sc::summarize(info.tls().certificates()))
};
pump(events, status, chain, sink, stop).await;
}
}
#[cfg(not(feature = "sc-tls"))]
let _ = (client, sink, stop, early);
}
fn log(event: &LinkEvent) {
let (severity, text) = describe(event);
match severity {
Severity::Info => tracing::info!("SC: {text}"),
Severity::Warn => tracing::warn!("SC: {text}"),
Severity::Error => tracing::error!("SC: {text}"),
}
}
pub(crate) async fn pump<S, C, F>(
mut events: broadcast::Receiver<ScEvent>,
status: S,
chain: C,
sink: &EventSink,
mut stop: mpsc::Receiver<()>,
) where
S: Fn() -> LinkSnapshot,
C: Fn() -> F,
F: Future<Output = Option<Vec<CertSummary>>>,
{
let snapshot = || {
sink.offer(WorkerEvent::Sc(ScReport::Status(status())));
};
let certificates = async |stop: &mut mpsc::Receiver<()>| -> bool {
let read = tokio::select! {
biased;
_ = stop.recv() => return true,
read = chain() => read,
};
if let Some(chain) = read {
sink.deliver(WorkerEvent::Sc(ScReport::Chain(chain))).await;
}
false
};
snapshot();
if certificates(&mut stop).await {
return;
}
let mut poll = tokio::time::interval(STATUS_EVERY);
poll.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
let mut open = true;
loop {
tokio::select! {
biased;
() = sink.cancelled() => break,
_ = stop.recv() => break,
received = events.recv(), if open => match received {
Ok(event) => {
let Some(event) = LinkEvent::from_event(event) else { continue };
log(&event);
let connected = matches!(event, LinkEvent::Connected { .. });
sink.deliver(WorkerEvent::Sc(ScReport::Event(event))).await;
if connected && certificates(&mut stop).await {
break;
}
}
Err(RecvError::Lagged(missed)) => {
tracing::warn!("SC: {missed} hub-connector events missed; resyncing");
sink.add_dropped(missed);
sink.deliver(WorkerEvent::Sc(ScReport::Gap { missed })).await;
snapshot();
if certificates(&mut stop).await {
break;
}
}
Err(RecvError::Closed) => open = false,
},
_ = poll.tick() => snapshot(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use bacnet_transport::sc::ScConnectionState;
fn snapshot() -> LinkSnapshot {
LinkSnapshot {
state: ScConnectionState::Connecting,
hub: Some(bacnet_transport::sc::ScActiveHub::Failover),
hub_vmac: None,
heartbeat_age: None,
heartbeat_rtt: None,
}
}
#[tokio::test(start_paused = true)]
async fn a_stalled_chain_read_does_not_hold_the_stop() {
let (_tx, events) = broadcast::channel(2);
let (sink, _ui) = EventSink::channel(64);
let (stop_tx, stop) = mpsc::channel(1);
drop(stop_tx);
let stalled = || std::future::pending::<Option<Vec<CertSummary>>>();
tokio::time::timeout(
Duration::from_secs(60),
pump(events, snapshot, stalled, &sink, stop),
)
.await
.expect("the stop ends the worker");
}
#[tokio::test(start_paused = true)]
async fn a_lagged_stream_reports_a_gap_and_resyncs() {
let (tx, events) = broadcast::channel(2);
for _ in 0..5 {
tx.send(ScEvent::FailoverEngaged).unwrap();
}
let (sink, mut ui) = EventSink::channel(64);
let (stop_tx, stop) = mpsc::channel(1);
let pumped = pump(events, snapshot, || async { None }, &sink, stop);
let reports = async {
let mut seen = Vec::new();
while seen.len() < 5 {
match ui.recv().await {
Some(WorkerEvent::Sc(report)) => seen.push(report),
other => panic!("unexpected {other:?}"),
}
}
drop(stop_tx);
seen
};
let ((), seen) = tokio::join!(pumped, reports);
assert_eq!(
seen,
[
ScReport::Status(snapshot()),
ScReport::Gap { missed: 3 },
ScReport::Status(snapshot()),
ScReport::Event(LinkEvent::FailoverEngaged),
ScReport::Event(LinkEvent::FailoverEngaged),
]
);
assert_eq!(
sink.dropped_counter()
.load(std::sync::atomic::Ordering::Relaxed),
3
);
}
}