1use parking_lot::RwLock;
8use rama_core::error::{ErrorContext, OpaqueError};
9use std::{
10 collections::{HashMap, hash_map::Entry},
11 fs::OpenOptions,
12 io::Write,
13 path::{Component, Path, PathBuf},
14 sync::OnceLock,
15};
16
17pub fn new_key_log_file_handle(path: String) -> Result<KeyLogFileHandle, OpaqueError> {
25 let path: PathBuf = path
26 .parse()
27 .with_context(|| format!("parse path str as Path: {path}"))?;
28
29 if let Some(parent) = path.parent() {
30 std::fs::create_dir_all(parent)
31 .with_context(|| format!("create parent dir(s) at {parent:?} for key log file"))?;
32 }
33
34 let path = normalize_path(&path);
35
36 let mapping = GLOBAL_KEY_LOG_FILE_MAPPING.get_or_init(Default::default);
37 if let Some(handle) = mapping.read().get(&path).cloned() {
38 return Ok(handle);
39 }
40 let mut mut_mapping = mapping.write();
41 match mut_mapping.entry(path.clone()) {
42 Entry::Occupied(entry) => Ok(entry.get().clone()),
43 Entry::Vacant(entry) => {
44 let handle = try_init_key_log_file_handle(path)?;
45 entry.insert(handle.clone());
46 Ok(handle)
47 }
48 }
49}
50
51pub fn normalize_path(path: &Path) -> PathBuf {
54 let mut components = path.components().peekable();
55 let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
56 components.next();
57 PathBuf::from(c.as_os_str())
58 } else {
59 PathBuf::new()
60 };
61
62 for component in components {
63 match component {
64 Component::Prefix(..) => unreachable!(),
65 Component::RootDir => {
66 ret.push(component.as_os_str());
67 }
68 Component::CurDir => {}
69 Component::ParentDir => {
70 ret.pop();
71 }
72 Component::Normal(c) => {
73 ret.push(c);
74 }
75 }
76 }
77 ret
78}
79
80fn try_init_key_log_file_handle(path: PathBuf) -> Result<KeyLogFileHandle, OpaqueError> {
81 tracing::trace!(
82 file = ?path,
83 "KeyLogFileHandle: try to create a new handle",
84 );
85
86 if let Some(parent) = path.parent() {
87 std::fs::create_dir_all(parent).context("create parent dir(s) of key log file")?;
88 }
89
90 let mut file = OpenOptions::new()
91 .append(true)
92 .create(true)
93 .open(&path)
94 .with_context(|| format!("create key log file {path:?}"))?;
95
96 let (tx, rx) = flume::unbounded::<String>();
97
98 let path_name = path.clone();
99 std::thread::spawn(move || {
100 tracing::trace!(
101 file = ?path_name,
102 "KeyLogFileHandle[rx]: receiver task up and running",
103 );
104 while let Ok(line) = rx.recv() {
105 if let Err(err) = file.write_all(line.as_bytes()) {
106 tracing::error!(
107 file = ?path_name,
108 error = %err,
109 "KeyLogFileHandle[rx]: failed to write file",
110 );
111 }
112 }
113 });
114
115 Ok(KeyLogFileHandle { path, sender: tx })
116}
117
118static GLOBAL_KEY_LOG_FILE_MAPPING: OnceLock<RwLock<HashMap<PathBuf, KeyLogFileHandle>>> =
119 OnceLock::new();
120
121#[derive(Debug, Clone)]
122pub struct KeyLogFileHandle {
127 path: PathBuf,
128 sender: flume::Sender<String>,
129}
130
131impl KeyLogFileHandle {
132 pub fn write_log_line(&self, line: String) {
134 if let Err(err) = self.sender.send(line) {
135 tracing::error!(
136 file = ?self.path,
137 error = %err,
138 "KeyLogFileHandle[tx]: failed to send log line for writing",
139 );
140 }
141 }
142}