use std::io::{BufRead, BufReader, Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use std::time::Duration;
use anyhow::{Context, Result, bail};
use clap::{Args, ValueEnum};
#[derive(Debug, Args)]
pub struct LogsArgs {
#[arg(long, short = 'c', default_value = "daemon")]
pub component: LogComponent,
#[arg(long, short = 'f')]
pub follow: bool,
#[arg(long, short = 'n', default_value = "100")]
pub lines: usize,
#[arg(long)]
pub data_dir: Option<PathBuf>,
}
#[derive(Debug, Clone, ValueEnum)]
pub enum LogComponent {
Daemon,
Helper,
Agent,
Dockerd,
Containerd,
}
pub async fn execute(args: LogsArgs) -> Result<()> {
let log_path = resolve_log_path(&args);
if !log_path.exists() {
bail!(
"Log file not found: {}\nIs the {} running?",
log_path.display(),
args.component.label()
);
}
if args.follow {
tail_follow(&log_path, args.lines).await
} else {
tail_lines(&log_path, args.lines)
}
}
impl LogComponent {
fn label(&self) -> &'static str {
match self {
Self::Daemon => "daemon",
Self::Helper => "helper",
Self::Agent => "agent",
Self::Dockerd => "dockerd",
Self::Containerd => "containerd",
}
}
}
fn resolve_log_path(args: &LogsArgs) -> PathBuf {
match args.component {
LogComponent::Helper => {
PathBuf::from(arcbox_constants::paths::privileged_log::HELPER_LOG_DIR)
.join(arcbox_constants::paths::privileged_log::HELPER_LOG)
}
_ => {
let data_dir = resolve_data_dir(args.data_dir.as_ref());
let log_dir = data_dir.join(arcbox_constants::paths::host::LOG);
let file_name = match args.component {
LogComponent::Daemon => arcbox_constants::paths::host::DAEMON_LOG,
LogComponent::Agent => arcbox_constants::paths::host::AGENT_LOG,
LogComponent::Dockerd => "dockerd.log",
LogComponent::Containerd => "containerd.log",
LogComponent::Helper => unreachable!(),
};
log_dir.join(file_name)
}
}
}
fn tail_lines(path: &Path, n: usize) -> Result<()> {
let mut file =
std::fs::File::open(path).with_context(|| format!("Failed to open {}", path.display()))?;
let file_len = file.metadata()?.len();
if file_len == 0 || n == 0 {
return Ok(());
}
const CHUNK: u64 = 64 * 1024;
let mut newlines_found = 0usize;
let mut scan_pos = file_len;
let mut start_offset = 0u64;
'outer: while scan_pos > 0 {
let read_size = scan_pos.min(CHUNK);
scan_pos -= read_size;
file.seek(SeekFrom::Start(scan_pos))?;
let mut buf = vec![0u8; read_size as usize];
file.read_exact(&mut buf)?;
for (idx, &b) in buf.iter().enumerate().rev() {
if b == b'\n' {
newlines_found += 1;
if newlines_found > n {
start_offset = scan_pos + idx as u64 + 1;
break 'outer;
}
}
}
}
file.seek(SeekFrom::Start(start_offset))?;
let reader = BufReader::new(file);
for line in reader.lines() {
println!("{}", line?);
}
Ok(())
}
async fn tail_follow(path: &Path, n: usize) -> Result<()> {
tail_lines(path, n)?;
let mut file =
std::fs::File::open(path).with_context(|| format!("Failed to open {}", path.display()))?;
let mut last_inode = file_inode(&file);
file.seek(SeekFrom::End(0))?;
let mut reader = BufReader::new(file);
let mut line = String::new();
loop {
line.clear();
match reader.read_line(&mut line) {
Ok(0) => {
if let Ok(new_file) = std::fs::File::open(path) {
let new_inode = file_inode(&new_file);
if new_inode != last_inode {
last_inode = new_inode;
reader = BufReader::new(new_file);
continue;
}
}
tokio::time::sleep(Duration::from_millis(200)).await;
}
Ok(_) => {
print!("{line}");
}
Err(e) => {
bail!("Error reading log file: {e}");
}
}
}
}
#[cfg(unix)]
fn file_inode(file: &std::fs::File) -> u64 {
use std::os::unix::fs::MetadataExt;
file.metadata().map_or(0, |m| m.ino())
}
#[cfg(not(unix))]
fn file_inode(_file: &std::fs::File) -> u64 {
0
}
fn resolve_data_dir(data_dir: Option<&PathBuf>) -> PathBuf {
arcbox_constants::paths::HostLayout::resolve(data_dir.map(PathBuf::as_path)).data_dir
}