use std::sync::{Arc, Mutex, RwLock, mpsc::channel};
use crate::{
console::{Console, ConsoleSpec},
log::ConsoleLog,
runner::Runner,
settings::eol_bytes,
};
pub struct Adopt {
pub device: String,
pub label: Option<String>,
pub baud: u32,
pub eol: String,
pub ring_kb: usize,
}
pub struct Registry {
consoles: RwLock<Vec<Arc<Console>>>,
runners: Mutex<Vec<Runner>>,
retention: i64,
}
impl Registry {
pub fn new(consoles: Vec<Arc<Console>>, retention: i64) -> Arc<Self> {
Arc::new(Self {
consoles: RwLock::new(consoles),
runners: Mutex::new(Vec::new()),
retention,
})
}
pub fn all(&self) -> Vec<Arc<Console>> {
self.consoles.read().unwrap().clone()
}
pub fn adopt(&self, wanted: Adopt) -> Result<Arc<Console>, String> {
if let Some(existing) = self.consoles.read().unwrap().iter().find(|c| c.matches(&wanted.device)) {
return Err(format!(
"{} is already open here as {}",
wanted.device,
existing.name()
));
}
let eol = eol_bytes(&wanted.eol).map_err(|e| e.to_string())?;
let name = wanted.label.clone().unwrap_or_else(|| wanted.device.clone());
let log = ConsoleLog::open(&name, self.retention, None).map_err(|e| format!("{e:#}"))?;
let (inject_tx, inject_rx) = channel();
let console = Console::new(
ConsoleSpec {
device: wanted.device,
label: wanted.label,
baud: wanted.baud,
eol,
ring_cap: wanted.ring_kb.saturating_mul(1024),
bridge: None,
},
log,
inject_tx,
);
let runner = Runner::start(Arc::clone(&console), inject_rx, true).map_err(|e| format!("{e:#}"))?;
self.runners.lock().unwrap().push(runner);
self.consoles.write().unwrap().push(Arc::clone(&console));
Ok(console)
}
pub fn resolve(&self, query: Option<&str>) -> Result<Arc<Console>, String> {
let consoles = self.consoles.read().unwrap();
let Some(query) = query else {
return match consoles.as_slice() {
[only] => Ok(Arc::clone(only)),
[] => Err("no consoles are open".to_string()),
_ => Err(format!("name a console, one of {}", self.names())),
};
};
let mut found = consoles.iter().filter(|c| c.matches(query));
let Some(first) = found.next() else {
return Err(format!("no console '{query}', have {}", self.names()));
};
if found.next().is_some() {
return Err(format!(
"'{query}' matches more than one console of {}",
self.names()
));
}
Ok(Arc::clone(first))
}
fn names(&self) -> String {
self.consoles
.read()
.unwrap()
.iter()
.map(|c| c.name().to_string())
.collect::<Vec<_>>()
.join(", ")
}
}
#[cfg(test)]
mod tests {
use std::{env, fs, path::PathBuf, sync::mpsc::channel};
use super::*;
use crate::{console::ConsoleSpec, log::ConsoleLog, ring::DEFAULT_RING_CAP};
struct Logs {
dir: PathBuf,
}
impl Logs {
fn new(name: &str) -> Logs {
let dir = env::temp_dir().join(format!("smon-registry-test-{name}"));
if dir.exists() {
fs::remove_dir_all(&dir).unwrap();
}
Logs { dir }
}
fn console(&self, device: &str, label: Option<&str>) -> Arc<Console> {
let log = ConsoleLog::open_in(self.dir.clone(), device, 0, None).unwrap();
Console::new(
ConsoleSpec {
device: device.to_string(),
label: label.map(str::to_string),
baud: 115_200,
eol: b"\r\n".to_vec(),
ring_cap: DEFAULT_RING_CAP,
bridge: None,
},
log,
channel().0,
)
}
}
impl Drop for Logs {
fn drop(&mut self) {
if let Err(e) = fs::remove_dir_all(&self.dir) {
eprintln!("could not clean {}: {e}", self.dir.display());
}
}
}
#[test]
fn one_console_needs_no_name() {
let logs = Logs::new("one");
let registry = Registry::new(vec![logs.console("/dev/ttyUSB0", None)], 0);
assert_eq!(registry.resolve(None).unwrap().device(), "/dev/ttyUSB0");
}
#[test]
fn several_consoles_refuse_to_guess() {
let logs = Logs::new("guess");
let registry = Registry::new(
vec![
logs.console("/dev/ttyUSB0", Some("first")),
logs.console("/dev/ttyUSB2", Some("second")),
],
0,
);
let error = registry.resolve(None).err().unwrap();
assert!(error.contains("first"), "{error}");
assert!(error.contains("second"), "{error}");
}
#[test]
fn a_console_answers_to_its_label_and_its_device() {
let logs = Logs::new("address");
let registry = Registry::new(
vec![
logs.console("/dev/ttyUSB0", Some("first")),
logs.console("/dev/ttyUSB2", Some("second")),
],
0,
);
assert_eq!(registry.resolve(Some("second")).unwrap().device(), "/dev/ttyUSB2");
assert_eq!(
registry.resolve(Some("/dev/ttyUSB0")).unwrap().device(),
"/dev/ttyUSB0"
);
assert_eq!(
registry.resolve(Some("ttyUSB2")).unwrap().device(),
"/dev/ttyUSB2"
);
}
#[test]
fn an_unknown_name_lists_what_there_is() {
let logs = Logs::new("unknown");
let registry = Registry::new(vec![logs.console("/dev/ttyUSB0", Some("first"))], 0);
let error = registry.resolve(Some("nope")).err().unwrap();
assert!(error.contains("nope"), "{error}");
assert!(error.contains("first"), "{error}");
}
#[test]
fn a_device_is_only_adopted_once() {
let logs = Logs::new("adopt");
let registry = Registry::new(vec![logs.console("/dev/ttyUSB0", Some("first"))], 0);
let error = registry
.adopt(Adopt {
device: "/dev/ttyUSB0".to_string(),
label: Some("again".to_string()),
baud: 115_200,
eol: "crlf".to_string(),
ring_kb: 64,
})
.err()
.unwrap();
assert!(error.contains("already open"), "{error}");
assert!(error.contains("first"), "{error}");
assert_eq!(registry.all().len(), 1);
}
#[test]
fn adopting_a_device_that_is_not_there_fails() {
let logs = Logs::new("adopt-missing");
let registry = Registry::new(vec![logs.console("/dev/ttyUSB0", Some("first"))], 0);
let error = registry
.adopt(Adopt {
device: "/dev/does-not-exist".to_string(),
label: None,
baud: 115_200,
eol: "crlf".to_string(),
ring_kb: 64,
})
.err()
.unwrap();
assert!(error.contains("does-not-exist"), "{error}");
assert_eq!(registry.all().len(), 1, "a failed adopt must add nothing");
}
#[test]
fn an_ambiguous_name_is_refused() {
let logs = Logs::new("ambiguous");
let registry = Registry::new(
vec![
logs.console("/dev/serial/by-path/a/ttyUSB0", Some("first")),
logs.console("/dev/serial/by-path/b/ttyUSB0", Some("second")),
],
0,
);
let error = registry.resolve(Some("ttyUSB0")).err().unwrap();
assert!(error.contains("more than one"), "{error}");
}
}