use crate::error::{CaError, CaResult};
use crate::runtime::log::errlog_printf;
use crate::server::device_support::DeviceSupport;
use crate::server::record::Record;
use crate::types::EpicsValue;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum StdioStream {
Stdout,
Stderr,
Errlog,
}
impl StdioStream {
fn from_name(name: &str) -> Option<StdioStream> {
match name {
"stdout" => Some(StdioStream::Stdout),
"stderr" => Some(StdioStream::Stderr),
"errlog" => Some(StdioStream::Errlog),
_ => None,
}
}
fn print_line(self, line: &str) {
match self {
StdioStream::Stdout => println!("{line}"),
StdioStream::Stderr => eprintln!("{line}"),
StdioStream::Errlog => errlog_printf(line),
}
}
}
pub struct StdioDeviceSupport {
stream: Option<StdioStream>,
}
impl StdioDeviceSupport {
pub fn new(out: &str) -> Self {
let name = out.strip_prefix('@').unwrap_or(out).trim();
Self {
stream: StdioStream::from_name(name),
}
}
}
impl DeviceSupport for StdioDeviceSupport {
fn dtyp(&self) -> &str {
"stdio"
}
fn init(&mut self, record: &mut dyn Record) -> CaResult<()> {
let rt = record.record_type();
if !matches!(rt, "lso" | "printf" | "stringout") {
return Err(CaError::InvalidValue(format!(
"DTYP='stdio': unsupported record type '{rt}' (use lso, printf, or stringout)"
)));
}
Ok(())
}
fn write(&mut self, record: &mut dyn Record) -> CaResult<()> {
let Some(stream) = self.stream else {
return Ok(());
};
if let Some(line) = val_string(record) {
stream.print_line(&line);
}
Ok(())
}
}
fn val_string(record: &dyn Record) -> Option<String> {
match record.get_field("VAL")? {
EpicsValue::String(s) => Some(s.as_str_lossy().into_owned()),
EpicsValue::CharArray(bytes) => {
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
Some(String::from_utf8_lossy(&bytes[..end]).into_owned())
}
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::server::records::lso::LsoRecord;
use crate::server::records::stringin::StringinRecord;
use crate::server::records::stringout::StringoutRecord;
#[test]
fn from_name_resolves_the_three_streams() {
assert_eq!(StdioStream::from_name("stdout"), Some(StdioStream::Stdout));
assert_eq!(StdioStream::from_name("stderr"), Some(StdioStream::Stderr));
assert_eq!(StdioStream::from_name("errlog"), Some(StdioStream::Errlog));
assert_eq!(StdioStream::from_name("bogus"), None);
assert_eq!(StdioStream::from_name(""), None);
}
#[test]
fn new_strips_inst_io_at_and_resolves_stream() {
assert_eq!(
StdioDeviceSupport::new("@stdout").stream,
Some(StdioStream::Stdout)
);
assert_eq!(
StdioDeviceSupport::new("stderr").stream,
Some(StdioStream::Stderr)
);
assert_eq!(
StdioDeviceSupport::new("@errlog").stream,
Some(StdioStream::Errlog)
);
assert_eq!(StdioDeviceSupport::new("@nope").stream, None);
}
#[test]
fn init_rejects_non_stdio_record_type() {
let mut dev = StdioDeviceSupport::new("@stdout");
let mut rec = StringinRecord::new("");
assert!(dev.init(&mut rec).is_err());
}
#[test]
fn init_accepts_stringout() {
let mut dev = StdioDeviceSupport::new("@stdout");
let mut rec = StringoutRecord::new("hello");
assert!(dev.init(&mut rec).is_ok());
}
#[test]
fn write_is_noop_on_unresolved_stream() {
let mut dev = StdioDeviceSupport::new("@bogus");
let mut rec = StringoutRecord::new("ignored");
assert!(dev.write(&mut rec).is_ok());
}
#[test]
fn write_to_resolved_stream_succeeds() {
let mut dev = StdioDeviceSupport::new("@stderr");
let mut rec = StringoutRecord::new("hello stdio");
assert!(dev.write(&mut rec).is_ok());
}
#[test]
fn val_string_reads_string_and_char_array_vals() {
let so = StringoutRecord::new("from stringout");
assert_eq!(val_string(&so).as_deref(), Some("from stringout"));
let lso = LsoRecord::new("from lso");
assert_eq!(val_string(&lso).as_deref(), Some("from lso"));
}
}