use std::fs;
use std::io::{BufRead, BufReader, Read};
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use anyhow::{Context, Result};
use astrid_core::PrincipalId;
use astrid_core::dirs::AstridHome;
use astrid_core::identity::PrincipalUid;
use astrid_core::kernel_api::{AdminRequestKind, AdminResponseBody};
use clap::Args;
use crate::context;
use crate::theme::Theme;
#[derive(Args, Debug, Clone)]
pub(crate) struct LogsArgs {
pub capsule: Option<String>,
#[arg(short, long)]
pub agent: Option<String>,
#[arg(short, long)]
pub follow: bool,
#[arg(short = 'n', long = "lines", default_value = "100")]
pub lines: usize,
}
fn most_recent_log(dir: &Path) -> Result<Option<PathBuf>> {
if !dir.exists() {
return Ok(None);
}
let mut newest: Option<(std::time::SystemTime, PathBuf)> = None;
for entry in fs::read_dir(dir).with_context(|| format!("Failed to read {}", dir.display()))? {
let entry = entry?;
let path = entry.path();
if !path.is_file() {
continue;
}
let modified = entry.metadata()?.modified()?;
if newest.as_ref().is_none_or(|(t, _)| *t < modified) {
newest = Some((modified, path));
}
}
Ok(newest.map(|(_, p)| p))
}
async fn resolve_principal_uid(principal: &PrincipalId) -> Result<PrincipalUid> {
let mut client = crate::admin_client::connect_as_active_agent().await?;
let body = client.request(AdminRequestKind::AgentList).await?;
let body = crate::admin_client::into_result(body)?;
let AdminResponseBody::AgentList(entries) = body else {
anyhow::bail!("unexpected response while resolving principal UID: {body:?}");
};
entries
.into_iter()
.find(|entry| entry.principal == *principal)
.and_then(|entry| entry.owner_uid)
.ok_or_else(|| anyhow::anyhow!("principal '{principal}' has no admitted durable UID"))
}
async fn resolve_log_dir(principal: &PrincipalId, capsule: Option<&str>) -> Result<PathBuf> {
let home = AstridHome::resolve().context("Failed to resolve Astrid home directory")?;
Ok(match capsule {
Some(name) => home
.log_dir()
.join("principals")
.join(resolve_principal_uid(principal).await?.to_string())
.join(name),
None => home.log_dir(),
})
}
fn print_tail(path: &Path, n: usize) -> Result<()> {
let mut bytes = Vec::new();
let mut file =
fs::File::open(path).with_context(|| format!("Failed to open {}", path.display()))?;
file.read_to_end(&mut bytes)?;
let text = String::from_utf8_lossy(&bytes);
let lines: Vec<&str> = text.lines().collect();
let start = lines.len().saturating_sub(n);
for line in &lines[start..] {
println!("{line}");
}
Ok(())
}
fn follow_tail(path: &Path, n: usize) -> Result<()> {
use std::io::{Seek, SeekFrom};
use std::thread;
use std::time::Duration;
let mut file =
fs::File::open(path).with_context(|| format!("Failed to open {}", path.display()))?;
let len = file.metadata()?.len();
file.seek(SeekFrom::Start(0))?;
let mut text = String::new();
file.read_to_string(&mut text)?;
let lines: Vec<&str> = text.lines().collect();
let start = lines.len().saturating_sub(n);
for line in &lines[start..] {
println!("{line}");
}
let mut pos = len;
let mut reader = BufReader::new(file);
loop {
reader.seek(SeekFrom::Start(pos))?;
let mut line = String::new();
match reader.read_line(&mut line) {
Ok(0) => {
thread::sleep(Duration::from_millis(250));
},
Ok(read) => {
print!("{line}");
pos = pos.saturating_add(read as u64);
},
Err(e) => return Err(e.into()),
}
}
}
pub(crate) async fn run(args: &LogsArgs) -> Result<ExitCode> {
let principal = context::resolve_agent(args.agent.as_deref())?;
let dir = resolve_log_dir(&principal, args.capsule.as_deref()).await?;
let Some(path) = most_recent_log(&dir)? else {
eprintln!(
"{}",
Theme::info(&format!("(no logs in {})", dir.display()))
);
return Ok(ExitCode::SUCCESS);
};
if args.follow {
follow_tail(&path, args.lines)?;
Ok(ExitCode::SUCCESS)
} else {
print_tail(&path, args.lines)?;
Ok(ExitCode::SUCCESS)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn most_recent_log_returns_newest() {
let dir = tempfile::tempdir().unwrap();
let a = dir.path().join("a.log");
let b = dir.path().join("b.log");
fs::write(&a, "old").unwrap();
std::thread::sleep(std::time::Duration::from_millis(20));
fs::write(&b, "new").unwrap();
let recent = most_recent_log(dir.path()).unwrap().unwrap();
assert_eq!(recent, b);
}
#[test]
fn most_recent_log_handles_missing_dir() {
let dir = tempfile::tempdir().unwrap();
let missing = dir.path().join("nope");
assert!(most_recent_log(&missing).unwrap().is_none());
}
#[test]
fn print_tail_returns_last_n_lines() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("a.log");
let mut body = String::new();
for i in 0..20 {
use std::fmt::Write as _;
let _ = writeln!(body, "line {i}");
}
fs::write(&path, body).unwrap();
print_tail(&path, 5).unwrap();
}
}