use crate::paths;
use crate::php;
use crate::services;
use crate::state::State;
use anyhow::{bail, Context, Result};
use std::fs::File;
use std::io::{BufRead, BufReader, Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use std::thread;
use std::time::Duration;
pub struct LogTarget {
pub label: String,
pub path: PathBuf,
}
pub fn targets(state: &State) -> Result<Vec<LogTarget>> {
let dir = paths::logs_dir()?;
let mut out = Vec::new();
for s in &state.servers {
out.push(LogTarget {
label: format!("server-{}", s.name),
path: dir.join(format!("server-{}.log", s.name)),
});
}
for p in &state.php_versions {
out.push(LogTarget {
label: format!("php-{}", p.version),
path: php::launchd_log(&p.version)?,
});
}
for svc in &state.services {
let id = services::service_id(svc.kind);
out.push(LogTarget {
path: dir.join(format!("{id}.log")),
label: id,
});
}
out.push(LogTarget {
label: "dnsmasq".into(),
path: dir.join("dnsmasq.log"),
});
Ok(out)
}
pub fn resolve(state: &State, target: &str) -> Result<PathBuf> {
let known = targets(state)?;
if let Some(t) = known.iter().find(|t| t.label.eq_ignore_ascii_case(target)) {
return Ok(t.path.clone());
}
let dir = paths::logs_dir()?;
for cand in [
dir.join(target),
dir.join(format!("{target}.log")),
dir.join(format!("server-{target}.log")),
] {
if cand.exists() {
return Ok(cand);
}
}
let labels = known
.iter()
.map(|t| t.label.as_str())
.collect::<Vec<_>>()
.join(", ");
bail!("Unknown log target '{target}'. Available: {labels}");
}
pub fn tail(path: &Path, lines: usize) -> Result<()> {
if !path.exists() {
println!("(no log yet at {})", path.display());
return Ok(());
}
let content = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read {}", path.display()))?;
for line in last_lines(&content, lines) {
println!("{line}");
}
Ok(())
}
pub fn follow(path: &Path, lines: usize) -> Result<()> {
tail(path, lines)?;
let mut file = loop {
match File::open(path) {
Ok(f) => break f,
Err(_) => thread::sleep(Duration::from_millis(500)),
}
};
let mut pos = file.seek(SeekFrom::End(0))?;
let mut reader = BufReader::new(file.try_clone()?);
loop {
let len = file.metadata()?.len();
if len < pos {
pos = 0;
reader.seek(SeekFrom::Start(0))?;
}
let mut line = String::new();
let n = reader.read_line(&mut line)?;
if n == 0 {
thread::sleep(Duration::from_millis(400));
} else {
print!("{line}");
pos += n as u64;
}
}
}
fn last_lines(content: &str, n: usize) -> Vec<&str> {
let all: Vec<&str> = content.lines().collect();
let start = all.len().saturating_sub(n);
all[start..].to_vec()
}
pub fn tail_lines(path: &Path, n: usize) -> Vec<String> {
let mut buf = String::new();
if File::open(path)
.and_then(|mut f| f.read_to_string(&mut buf))
.is_err()
{
return Vec::new();
}
last_lines(&buf, n).into_iter().map(String::from).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn last_lines_takes_tail() {
let s = "a\nb\nc\nd\ne";
assert_eq!(last_lines(s, 2), vec!["d", "e"]);
assert_eq!(last_lines(s, 10), vec!["a", "b", "c", "d", "e"]);
assert_eq!(last_lines("", 3), Vec::<&str>::new());
}
}