use std::fs::{File, OpenOptions};
use std::io::{BufWriter, Write};
use std::path::{Path, PathBuf};
use std::sync::mpsc::{sync_channel, SyncSender, TrySendError};
use std::thread::JoinHandle;
use std::time::{SystemTime, UNIX_EPOCH};
const QUEUE: usize = 4_096;
pub(crate) const HEADER: &str = "unix_ms,device,object,property,value,status_flags,source,network";
pub(crate) struct CsvLog {
tx: Option<SyncSender<String>>,
thread: Option<JoinHandle<()>>,
pub(crate) path: PathBuf,
}
pub(crate) fn field(text: &str) -> String {
if text.contains([',', '"', '\n', '\r']) {
format!("\"{}\"", text.replace('"', "\"\""))
} else {
text.to_string()
}
}
pub(crate) fn unix_ms() -> u128 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |since| since.as_millis())
}
impl CsvLog {
pub(crate) fn open(dir: &Path) -> Result<Self, String> {
let path = dir.join(format!("watch-{}.csv", unix_ms()));
let file = OpenOptions::new()
.write(true)
.create_new(true)
.open(&path)
.map_err(|e| format!("cannot create {}: {e}", path.display()))?;
let (tx, rx) = sync_channel::<String>(QUEUE);
let thread = std::thread::Builder::new()
.name("bacnet-watch-csv".into())
.spawn(move || write_lines(file, rx.iter()))
.map_err(|e| format!("cannot start the CSV writer: {e}"))?;
let log = Self {
tx: Some(tx),
thread: Some(thread),
path,
};
log.line(HEADER.to_string());
Ok(log)
}
pub(crate) fn line(&self, line: String) -> bool {
match self.tx.as_ref().map(|tx| tx.try_send(line)) {
Some(Ok(())) => true,
Some(Err(TrySendError::Full(_) | TrySendError::Disconnected(_))) | None => false,
}
}
pub(crate) async fn close(mut self) {
drop(self.tx.take());
if let Some(thread) = self.thread.take() {
let _ = tokio::task::spawn_blocking(move || thread.join()).await;
}
}
}
impl Drop for CsvLog {
fn drop(&mut self) {
drop(self.tx.take());
if let Some(thread) = self.thread.take() {
let _ = thread.join();
}
}
}
fn write_lines(file: File, lines: impl Iterator<Item = String>) {
let mut out = BufWriter::new(file);
for line in lines {
if writeln!(out, "{line}").is_err() {
break;
}
}
let _ = out.flush();
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn lines_reach_the_file_with_awkward_fields_quoted() {
let dir = tempfile::tempdir().unwrap();
let log = CsvLog::open(dir.path()).unwrap();
assert!(log.line(format!("1,100,{},x", field("a \"b\", c"))));
let path = log.path.clone();
log.close().await;
let text = std::fs::read_to_string(path).unwrap();
assert_eq!(text, format!("{HEADER}\n1,100,\"a \"\"b\"\", c\",x\n"));
}
}