use std::io;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc;
use std::thread::{self, JoinHandle};
use std::time::Duration;
use crate::engine::RawFrame;
use super::{DeviceHandle, FrameSender, WriterCmd};
pub trait DeviceFactory: Send + 'static {
fn open(&mut self) -> io::Result<DeviceHandle>;
fn name(&self) -> &str {
"device"
}
}
impl<F> DeviceFactory for F
where
F: FnMut() -> io::Result<DeviceHandle> + Send + 'static,
{
fn open(&mut self) -> io::Result<DeviceHandle> {
(self)()
}
}
pub const INITIAL_BACKOFF: Duration = Duration::from_millis(500);
pub const MAX_BACKOFF: Duration = Duration::from_secs(30);
pub struct Supervisor {
pub frames_rx: mpsc::Receiver<RawFrame>,
sender: FrameSender,
stop: Arc<AtomicBool>,
join: Option<JoinHandle<()>>,
}
impl Supervisor {
pub fn new<F: DeviceFactory>(factory: F) -> Self {
let (frames_tx, frames_rx) = mpsc::channel::<RawFrame>();
let (cmd_tx, cmd_rx) = mpsc::channel::<WriterCmd>();
let stop = Arc::new(AtomicBool::new(false));
let stop_for_thread = stop.clone();
let join = thread::Builder::new()
.name("device-supervisor".into())
.spawn(move || supervisor_loop(factory, frames_tx, cmd_rx, stop_for_thread))
.expect("spawn device supervisor");
Self {
frames_rx,
sender: FrameSender::from_cmd_tx(cmd_tx),
stop,
join: Some(join),
}
}
pub fn frame_sender(&self) -> FrameSender {
self.sender.clone()
}
pub fn shutdown(&self) {
self.stop.store(true, Ordering::Relaxed);
}
}
impl Drop for Supervisor {
fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
if let Some(j) = self.join.take() {
let _ = j.join();
}
}
}
fn supervisor_loop<F: DeviceFactory>(
mut factory: F,
frames_tx: mpsc::Sender<RawFrame>,
cmd_rx: mpsc::Receiver<WriterCmd>,
stop: Arc<AtomicBool>,
) {
let mut backoff = INITIAL_BACKOFF;
let mut held_cmd_rx = Some(cmd_rx);
while !stop.load(Ordering::Relaxed) {
let handle = match factory.open() {
Ok(h) => {
log::info!("{}: device session opened", factory.name());
backoff = INITIAL_BACKOFF;
h
}
Err(e) => {
log::warn!(
"{}: open failed: {e}; retrying in {:?}",
factory.name(),
backoff
);
sleep_interruptible(backoff, &stop);
backoff = (backoff * 2).min(MAX_BACKOFF);
continue;
}
};
let cmd_rx = held_cmd_rx
.take()
.expect("held_cmd_rx is always Some at session start");
held_cmd_rx = Some(run_session(
factory.name(),
handle,
&frames_tx,
cmd_rx,
&stop,
));
if stop.load(Ordering::Relaxed) {
break;
}
log::warn!("{}: session ended; will reconnect", factory.name());
sleep_interruptible(Duration::from_millis(500), &stop);
}
log::info!("{}: supervisor stopped", factory.name());
}
fn sleep_interruptible(duration: Duration, stop: &Arc<AtomicBool>) {
let step = Duration::from_millis(50);
let mut remaining = duration;
while remaining > Duration::ZERO {
if stop.load(Ordering::Relaxed) {
return;
}
let s = step.min(remaining);
thread::sleep(s);
remaining = remaining.saturating_sub(s);
}
}
fn run_session(
name: &str,
handle: DeviceHandle,
frames_tx: &mpsc::Sender<RawFrame>,
cmd_rx: mpsc::Receiver<WriterCmd>,
stop: &Arc<AtomicBool>,
) -> mpsc::Receiver<WriterCmd> {
let device_sender = handle.frame_sender();
let stop_for_fwd = stop.clone();
let session_done = Arc::new(AtomicBool::new(false));
let session_done_for_fwd = session_done.clone();
let (return_tx, return_rx) = mpsc::channel::<mpsc::Receiver<WriterCmd>>();
let fwd_name = format!("{name}-cmd-fwd");
let cmd_join = thread::Builder::new()
.name(fwd_name)
.spawn(move || {
loop {
if stop_for_fwd.load(Ordering::Relaxed)
|| session_done_for_fwd.load(Ordering::Relaxed)
{
break;
}
match cmd_rx.recv_timeout(Duration::from_millis(250)) {
Ok(WriterCmd::Frame(f)) => {
if device_sender.send_frame(f).is_err() {
break;
}
}
Ok(WriterCmd::Bytes(_)) => {
}
Err(mpsc::RecvTimeoutError::Timeout) => continue,
Err(mpsc::RecvTimeoutError::Disconnected) => break,
}
}
let _ = return_tx.send(cmd_rx);
})
.expect("spawn supervisor cmd forwarder");
'pump: while let Ok(frame) = handle.frames_rx.recv() {
if stop.load(Ordering::Relaxed) {
break;
}
if frames_tx.send(frame).is_err() {
break;
}
loop {
match handle.frames_rx.try_recv() {
Ok(frame) => {
if frames_tx.send(frame).is_err() {
break 'pump;
}
}
Err(mpsc::TryRecvError::Empty) => break,
Err(mpsc::TryRecvError::Disconnected) => break 'pump,
}
}
}
session_done.store(true, Ordering::Relaxed);
drop(handle);
let _ = cmd_join.join();
return_rx
.recv()
.expect("cmd forwarder always returns cmd_rx")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::io::device::{DeviceDecoder, DeviceEncoder, DeviceEvent, run as run_device};
use std::io::{self, Read, Write};
use std::sync::Mutex;
use std::sync::atomic::AtomicU32;
struct TestDecoder;
impl DeviceDecoder for TestDecoder {
fn decode(&mut self, bytes: &[u8], events: &mut Vec<DeviceEvent>) {
for &b in bytes {
events.push(DeviceEvent::Frame(RawFrame::new(
None,
0,
b as u32,
0,
0,
std::iter::empty(),
)));
}
}
}
struct TestEncoder;
impl DeviceEncoder for TestEncoder {
fn encode_frame(&self, _frame: &RawFrame) -> Option<Vec<u8>> {
None
}
}
struct OneShotReader(Option<Vec<u8>>);
impl Read for OneShotReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self.0.take() {
Some(data) => {
let n = data.len().min(buf.len());
buf[..n].copy_from_slice(&data[..n]);
Ok(n)
}
None => Ok(0),
}
}
}
struct SinkWriter;
impl Write for SinkWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
struct ReconnectFactory {
sessions: Arc<AtomicU32>,
bytes_per_session: Arc<Mutex<Vec<Vec<u8>>>>,
}
impl DeviceFactory for ReconnectFactory {
fn open(&mut self) -> io::Result<DeviceHandle> {
let n = self.sessions.fetch_add(1, Ordering::Relaxed) as usize;
let payload = {
let table = self.bytes_per_session.lock().unwrap();
table.get(n).cloned().unwrap_or_default()
};
if payload.is_empty() {
return Err(io::Error::other("no payload for session"));
}
let reader: Box<dyn Read + Send> = Box::new(OneShotReader(Some(payload)));
let writer: Box<dyn Write + Send> = Box::new(SinkWriter);
Ok(run_device(TestDecoder, TestEncoder, reader, writer))
}
}
#[test]
fn supervisor_reconnects_across_eof() {
let sessions = Arc::new(AtomicU32::new(0));
let payloads = Arc::new(Mutex::new(vec![
vec![1u8, 2, 3],
vec![], vec![10u8, 20],
vec![100u8, 101],
]));
let factory = ReconnectFactory {
sessions: sessions.clone(),
bytes_per_session: payloads,
};
let sup = Supervisor::new(factory);
let mut got: Vec<u32> = Vec::new();
let deadline = std::time::Instant::now() + Duration::from_secs(60);
while got.len() < 7 && std::time::Instant::now() < deadline {
match sup.frames_rx.recv_timeout(Duration::from_millis(500)) {
Ok(f) => got.push(f.pgn),
Err(_) => continue,
}
}
sup.shutdown();
assert_eq!(
got,
vec![1, 2, 3, 10, 20, 100, 101],
"expected all 3 sessions' bytes in order"
);
assert!(
sessions.load(Ordering::Relaxed) >= 4,
"expected ≥ 4 open attempts (3 successful + 1 transient failure)"
);
}
}