use parking_lot::RwLock;
use rama_core::error::{ErrorContext, OpaqueError};
use std::{
collections::{HashMap, hash_map::Entry},
fs::OpenOptions,
io::Write,
path::{Component, Path, PathBuf},
sync::OnceLock,
};
pub fn new_key_log_file_handle(path: String) -> Result<KeyLogFileHandle, OpaqueError> {
let path: PathBuf = path
.parse()
.with_context(|| format!("parse path str as Path: {path}"))?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("create parent dir(s) at {parent:?} for key log file"))?;
}
let path = normalize_path(&path);
let mapping = GLOBAL_KEY_LOG_FILE_MAPPING.get_or_init(Default::default);
if let Some(handle) = mapping.read().get(&path).cloned() {
return Ok(handle);
}
let mut mut_mapping = mapping.write();
match mut_mapping.entry(path.clone()) {
Entry::Occupied(entry) => Ok(entry.get().clone()),
Entry::Vacant(entry) => {
let handle = try_init_key_log_file_handle(path)?;
entry.insert(handle.clone());
Ok(handle)
}
}
}
pub fn normalize_path(path: &Path) -> PathBuf {
let mut components = path.components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
components.next();
PathBuf::from(c.as_os_str())
} else {
PathBuf::new()
};
for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
ret.push(component.as_os_str());
}
Component::CurDir => {}
Component::ParentDir => {
ret.pop();
}
Component::Normal(c) => {
ret.push(c);
}
}
}
ret
}
fn try_init_key_log_file_handle(path: PathBuf) -> Result<KeyLogFileHandle, OpaqueError> {
tracing::trace!(
file = ?path,
"KeyLogFileHandle: try to create a new handle",
);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).context("create parent dir(s) of key log file")?;
}
let mut file = OpenOptions::new()
.append(true)
.create(true)
.open(&path)
.with_context(|| format!("create key log file {path:?}"))?;
let (tx, rx) = flume::unbounded::<String>();
let path_name = path.clone();
std::thread::spawn(move || {
tracing::trace!(
file = ?path_name,
"KeyLogFileHandle[rx]: receiver task up and running",
);
while let Ok(line) = rx.recv() {
if let Err(err) = file.write_all(line.as_bytes()) {
tracing::error!(
file = ?path_name,
error = %err,
"KeyLogFileHandle[rx]: failed to write file",
);
}
}
});
Ok(KeyLogFileHandle { path, sender: tx })
}
static GLOBAL_KEY_LOG_FILE_MAPPING: OnceLock<RwLock<HashMap<PathBuf, KeyLogFileHandle>>> =
OnceLock::new();
#[derive(Debug, Clone)]
pub struct KeyLogFileHandle {
path: PathBuf,
sender: flume::Sender<String>,
}
impl KeyLogFileHandle {
pub fn write_log_line(&self, line: String) {
if let Err(err) = self.sender.send(line) {
tracing::error!(
file = ?self.path,
error = %err,
"KeyLogFileHandle[tx]: failed to send log line for writing",
);
}
}
}