use std::path::{Path, PathBuf};
use chrono::Local;
use parking_lot::Mutex as SyncMutex;
use tokio::fs::{File, OpenOptions};
use tokio::io::AsyncWriteExt;
use tokio::sync::Mutex as AsyncMutex;
use crate::procserv::error::{ProcServError, ProcServResult};
pub struct LogFile {
file: AsyncMutex<File>,
time_format: String,
in_line: SyncMutex<bool>,
}
impl LogFile {
pub async fn open(path: &Path, time_format: impl Into<String>) -> ProcServResult<Self> {
let file = OpenOptions::new()
.create(true)
.append(true)
.open(path)
.await
.map_err(ProcServError::Io)?;
Ok(Self {
file: AsyncMutex::new(file),
time_format: time_format.into(),
in_line: SyncMutex::new(false),
})
}
pub async fn write_chunk(&self, chunk: &[u8]) -> ProcServResult<()> {
let out: Vec<u8> = {
let stamp = self.format_stamp();
let mut buf: Vec<u8> = Vec::with_capacity(chunk.len() + 32);
let mut in_line = self.in_line.lock();
let mut prev = 0usize;
for (i, &b) in chunk.iter().enumerate() {
if !*in_line {
buf.extend_from_slice(stamp.as_bytes());
*in_line = true;
}
if b == b'\n' {
buf.extend_from_slice(&chunk[prev..=i]);
prev = i + 1;
*in_line = false;
}
}
if prev < chunk.len() {
buf.extend_from_slice(&chunk[prev..]);
}
buf
};
let mut file = self.file.lock().await;
file.write_all(&out).await.map_err(ProcServError::Io)?;
file.flush().await.map_err(ProcServError::Io)?;
Ok(())
}
fn format_stamp(&self) -> String {
let now = Local::now();
format!("[{}] ", now.format(&self.time_format))
}
}
pub fn write_pid_file(path: &Path, pid: i32) -> ProcServResult<()> {
let parent = path.parent().unwrap_or_else(|| Path::new("."));
let tmp = parent.join(format!(
".{}.tmp",
path.file_name()
.and_then(|s| s.to_str())
.unwrap_or("procserv.pid")
));
std::fs::write(&tmp, format!("{pid}\n")).map_err(ProcServError::Io)?;
std::fs::rename(&tmp, path).map_err(ProcServError::Io)?;
Ok(())
}
pub fn remove_pid_file(path: &Path) {
if let Err(e) = std::fs::remove_file(path) {
tracing::warn!(path = %path.display(), error = %e, "procserv-rs: failed to remove pid file");
}
}
pub fn write_info_file(path: &Path, info: &InfoSnapshot) -> ProcServResult<()> {
let parent = path.parent().unwrap_or_else(|| Path::new("."));
let tmp = parent.join(format!(
".{}.tmp",
path.file_name()
.and_then(|s| s.to_str())
.unwrap_or("procserv.info")
));
let body = render_procserv_info(info);
std::fs::write(&tmp, body).map_err(ProcServError::Io)?;
std::fs::rename(&tmp, path).map_err(ProcServError::Io)?;
Ok(())
}
#[derive(Debug, Clone)]
pub struct InfoSnapshot {
pub procserv_pid: i32,
pub child_pid: Option<i32>,
pub child_exe: PathBuf,
pub child_args: Vec<String>,
}
pub fn render_procserv_info(info: &InfoSnapshot) -> String {
let mut out = String::new();
out.push_str(&format!("procservpid={}\n", info.procserv_pid));
if let Some(p) = info.child_pid {
out.push_str(&format!("childpid={p}\n"));
}
out.push_str(&format!("childexe={}\n", info.child_exe.display()));
out.push_str(&format!("childargs={}\n", info.child_args.join(" ")));
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn render_info_keys_match_c_procserv_convention() {
let info = InfoSnapshot {
procserv_pid: 1234,
child_pid: Some(1235),
child_exe: PathBuf::from("/usr/bin/softIoc"),
child_args: vec!["-d".into(), "test.db".into()],
};
let rendered = render_procserv_info(&info);
assert!(rendered.contains("procservpid=1234"));
assert!(rendered.contains("childpid=1235"));
assert!(rendered.contains("childexe=/usr/bin/softIoc"));
assert!(rendered.contains("childargs=-d test.db"));
}
#[tokio::test]
async fn log_file_prefixes_each_line_with_timestamp() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.log");
let log = LogFile::open(&path, "%Y-%m-%dT%H:%M:%S".to_string())
.await
.unwrap();
log.write_chunk(b"line1\nline2\n").await.unwrap();
log.write_chunk(b"partial").await.unwrap();
log.write_chunk(b" continued\n").await.unwrap();
let content = std::fs::read_to_string(&path).unwrap();
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines.len(), 3);
for line in &lines {
assert!(line.starts_with('['), "no stamp on: {line}");
}
assert!(lines[0].ends_with("line1"));
assert!(lines[1].ends_with("line2"));
assert!(lines[2].ends_with("partial continued"));
}
#[test]
fn pid_file_atomic_write() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.pid");
write_pid_file(&path, 12345).unwrap();
let content = std::fs::read_to_string(&path).unwrap();
assert_eq!(content.trim(), "12345");
}
}