use std::{
io::{self, Error, ErrorKind, Read},
sync::{
Arc,
atomic::{AtomicBool, Ordering},
mpsc::{self, Receiver, RecvTimeoutError, Sender},
},
thread::{self, JoinHandle},
time::{Duration, Instant},
};
use anyhow::{Context, Result};
use serialport::SerialPort;
use crate::{
console::{Console, Inject},
probe,
};
const RECONNECT_EVERY: Duration = Duration::from_secs(1);
const TICK: Duration = Duration::from_millis(100);
const WRITE_STALL_LIMIT: Duration = Duration::from_secs(5);
struct WriteReq {
bytes: Vec<u8>,
resp: Option<Reply>,
}
type Reply = tokio::sync::oneshot::Sender<Result<(), String>>;
struct Connection {
writer_tx: Sender<WriteReq>,
stop_tx: Sender<()>,
reader: JoinHandle<()>,
writer: JoinHandle<()>,
}
pub struct Runner {
stop: Arc<AtomicBool>,
thread: JoinHandle<()>,
}
impl Runner {
pub fn start(console: Arc<Console>, injects: Receiver<Inject>, require_open: bool) -> Result<Runner> {
let (deaths_tx, deaths_rx) = mpsc::channel::<String>();
let mut conn = None;
if require_open {
conn = Some(connect(&console, &deaths_tx)?);
console.set_connected(true);
}
let stop = Arc::new(AtomicBool::new(false));
let thread_stop = Arc::clone(&stop);
let thread = thread::spawn(move || {
run(&console, injects, &deaths_tx, &deaths_rx, conn, &thread_stop);
});
Ok(Runner { stop, thread })
}
pub fn stop(self) {
self.stop.store(true, Ordering::Relaxed);
match self.thread.join() {
Ok(()) => {}
Err(_) => eprintln!("smon: console thread panicked"),
}
}
}
fn run(
console: &Arc<Console>,
injects: Receiver<Inject>,
deaths_tx: &Sender<String>,
deaths_rx: &Receiver<String>,
mut conn: Option<Connection>,
stop: &AtomicBool,
) {
let mut last_attempt: Option<Instant> = None;
let mut opened_before = conn.is_some();
loop {
match injects.recv_timeout(TICK) {
Ok(inject) => write(console, &conn, inject),
Err(RecvTimeoutError::Timeout) => {}
Err(RecvTimeoutError::Disconnected) => break,
}
if stop.load(Ordering::Relaxed) {
break;
}
if console.released() {
if let Some(c) = conn.take() {
teardown(c);
console.set_connected(false);
console.push_system("released, another program has the device");
}
continue;
}
while let Ok(reason) = deaths_rx.try_recv() {
if let Some(c) = conn.take() {
teardown(c);
console.set_connected(false);
console.push_system(&format!("{reason}, reconnecting"));
last_attempt = Some(Instant::now());
}
}
if conn.is_none()
&& !console.released()
&& last_attempt.is_none_or(|at| at.elapsed() >= RECONNECT_EVERY)
{
last_attempt = Some(Instant::now());
match connect(console, deaths_tx) {
Ok(c) => {
conn = Some(c);
console.set_connected(true);
console.push_system(if opened_before { "reconnected" } else { "connected" });
opened_before = true;
}
Err(_) => continue,
}
}
}
if let Some(c) = conn {
teardown(c);
console.set_connected(false);
}
}
fn write(console: &Arc<Console>, conn: &Option<Connection>, inject: Inject) {
let Inject {
bytes,
echo,
origin,
resp,
} = inject;
let Some(c) = conn else {
if !reply(resp, Err("port disconnected".to_string())) {
console.push_system("port disconnected, input not sent");
}
return;
};
console.push_echo(origin, &bytes, &echo);
if let Err(back) = c.writer_tx.send(WriteReq { bytes, resp }) {
let WriteReq { resp, .. } = back.0;
if !reply(resp, Err("port disconnected".to_string())) {
console.push_system("port disconnected, input not sent");
}
}
}
fn reply(resp: Option<Reply>, outcome: Result<(), String>) -> bool {
match resp {
Some(tx) => tx.send(outcome).is_ok(),
None => false,
}
}
fn connect(console: &Arc<Console>, deaths: &Sender<String>) -> Result<Connection> {
let name = console.device();
let baud = console.baud();
let opened = probe::hold(|| serialport::new(name, baud).timeout(Duration::from_millis(50)).open());
let port = opened.with_context(|| format!("opening {name} @ {baud}"))?;
let reader_port = port.try_clone().context("cloning serial port for reader thread")?;
let (stop_tx, stop_rx) = mpsc::channel::<()>();
let (writer_tx, writer_rx) = mpsc::channel::<WriteReq>();
let reader_console = Arc::clone(console);
let reader_deaths = deaths.clone();
let reader = thread::spawn(move || reader_loop(reader_port, &reader_console, &stop_rx, &reader_deaths));
let writer_deaths = deaths.clone();
let writer = thread::spawn(move || writer_loop(port, &writer_rx, &writer_deaths));
Ok(Connection {
writer_tx,
stop_tx,
reader,
writer,
})
}
fn teardown(conn: Connection) {
let sent = conn.stop_tx.send(());
drop(conn.writer_tx);
let reader = conn.reader.join();
let writer = conn.writer.join();
if sent.is_err() || reader.is_err() || writer.is_err() {
eprintln!("smon: a port thread ended badly");
}
}
fn reader_loop(
mut port: Box<dyn SerialPort>,
console: &Arc<Console>,
stop_rx: &Receiver<()>,
deaths: &Sender<String>,
) {
let mut buf = [0u8; 4096];
loop {
if matches!(stop_rx.try_recv(), Ok(()) | Err(mpsc::TryRecvError::Disconnected)) {
return;
}
match port.read(&mut buf) {
Ok(0) => {}
Ok(n) => console.push_rx(&buf[..n]),
Err(e) if e.kind() == ErrorKind::TimedOut => {}
Err(e) => {
report(deaths, format!("read error: {e}"));
return;
}
}
}
}
fn writer_loop(mut port: Box<dyn SerialPort>, reqs: &Receiver<WriteReq>, deaths: &Sender<String>) {
while let Ok(req) = reqs.recv() {
match write_with_retry(port.as_mut(), &req.bytes) {
Ok(()) => {
reply(req.resp, Ok(()));
}
Err(e) => {
let msg = format!("write error: {e}");
reply(req.resp, Err(msg.clone()));
report(deaths, msg);
return;
}
}
}
}
fn report(deaths: &Sender<String>, reason: String) -> bool {
deaths.send(reason).is_ok()
}
fn write_with_retry(port: &mut dyn SerialPort, bytes: &[u8]) -> io::Result<()> {
let mut written = 0;
let mut deadline = Instant::now() + WRITE_STALL_LIMIT;
while written < bytes.len() {
match port.write(&bytes[written..]) {
Ok(0) => return Err(Error::new(ErrorKind::WriteZero, "wrote zero bytes")),
Ok(n) => {
written += n;
deadline = Instant::now() + WRITE_STALL_LIMIT;
}
Err(e) if e.kind() == ErrorKind::TimedOut => {
if Instant::now() >= deadline {
return Err(Error::new(ErrorKind::TimedOut, "write stalled"));
}
}
Err(e) if e.kind() == ErrorKind::Interrupted => {}
Err(e) => return Err(e),
}
}
Ok(())
}