use crate::features::config::{device_store::DeviceStore, state::DeviceState};
use crate::tools::{
macros::{print_error, print_info},
session::DeviceSession,
shell_escape::escape_single_quote,
types::DeviceIdentifier,
};
use anyhow::{anyhow, Context, Result};
pub struct LogsArgs {
pub lines: usize,
pub priority: Option<crate::LogLevel>,
pub unit: Option<String>,
pub grep: Option<String>,
pub since: Option<String>,
pub clear: bool,
pub force: bool,
pub kernel: bool,
}
pub async fn execute(args: LogsArgs) -> Result<()> {
if args.clear {
return execute_clear_logs(args.force).await;
}
validate_args(&args)?;
let current_host = DeviceState::get_current()?;
let device_id = DeviceIdentifier::Host(current_host);
let device = DeviceStore::find(&device_id)?;
let mut session = DeviceSession::connect(&device)
.context("Failed to connect to device")?;
let command = build_journalctl_command(&args)?;
print_info(format!("Retrieving logs from {}...", device.display_name()));
let output = session.exec_as_root(&command)
.context("Failed to retrieve logs. Root access required.")?;
for line in &output {
println!("{}", line);
}
if output.is_empty() {
print_info("No logs found matching the criteria");
}
Ok(())
}
async fn execute_clear_logs(force: bool) -> Result<()> {
if !force {
return Err(anyhow!(
"Clearing logs requires --force flag. Use: audb logs --clear --force"
));
}
let current_host = DeviceState::get_current()?;
let device_id = DeviceIdentifier::Host(current_host);
let device = DeviceStore::find(&device_id)?;
let mut session = DeviceSession::connect(&device)
.context("Failed to connect to device")?;
print_info(format!(
"Clearing logs on {}...",
device.display_name()
));
let command = "journalctl --rotate && journalctl --vacuum-time=1s";
session
.exec_as_root(command)
.context("Failed to clear logs. Root access required.")?;
print_info("Logs cleared successfully");
Ok(())
}
fn validate_args(args: &LogsArgs) -> Result<()> {
if args.lines == 0 {
return Err(anyhow!("Lines count must be greater than 0"));
}
if args.lines > 10000 {
print_error("Large line count may take time to retrieve");
}
if args.kernel && args.unit.is_some() {
return Err(anyhow!(
"Cannot specify both --kernel and --unit"
));
}
Ok(())
}
fn build_journalctl_command(args: &LogsArgs) -> Result<String> {
let mut cmd = String::from("journalctl");
if args.kernel {
cmd.push_str(" -k");
}
cmd.push_str(&format!(" -n {}", args.lines));
if let Some(ref priority) = args.priority {
cmd.push_str(&format!(" -p {}", priority.to_journalctl_priority()));
}
if let Some(ref unit) = args.unit {
let escaped = escape_single_quote(unit);
cmd.push_str(&format!(" -u '{}'", escaped));
}
if let Some(ref since) = args.since {
let escaped = escape_single_quote(since);
cmd.push_str(&format!(" --since '{}'", escaped));
}
cmd.push_str(" --no-pager --no-hostname");
if let Some(ref grep_pattern) = args.grep {
let escaped = escape_single_quote(grep_pattern);
cmd.push_str(&format!(" | grep '{}'", escaped));
}
Ok(cmd)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_basic_command() {
let args = LogsArgs {
lines: 100,
priority: None,
unit: None,
since: None,
grep: None,
clear: false,
force: false,
kernel: false,
};
let cmd = build_journalctl_command(&args).unwrap();
assert!(cmd.contains("journalctl"));
assert!(cmd.contains("-n 100"));
assert!(cmd.contains("--no-pager"));
assert!(cmd.contains("--no-hostname"));
}
#[test]
fn test_build_command_with_priority() {
let args = LogsArgs {
lines: 50,
priority: Some(crate::LogLevel::Err),
unit: None,
since: None,
grep: None,
clear: false,
force: false,
kernel: false,
};
let cmd = build_journalctl_command(&args).unwrap();
assert!(cmd.contains("-p err"));
}
#[test]
fn test_build_command_with_unit() {
let args = LogsArgs {
lines: 100,
priority: None,
unit: Some("test.service".to_string()),
since: None,
grep: None,
clear: false,
force: false,
kernel: false,
};
let cmd = build_journalctl_command(&args).unwrap();
assert!(cmd.contains("-u 'test.service'"));
}
#[test]
fn test_build_command_with_kernel() {
let args = LogsArgs {
lines: 100,
priority: None,
unit: None,
since: None,
grep: None,
clear: false,
force: false,
kernel: true,
};
let cmd = build_journalctl_command(&args).unwrap();
assert!(cmd.contains(" -k"));
}
#[test]
fn test_build_command_with_grep() {
let args = LogsArgs {
lines: 100,
priority: None,
unit: None,
since: None,
grep: Some("ERROR".to_string()),
clear: false,
force: false,
kernel: false,
};
let cmd = build_journalctl_command(&args).unwrap();
assert!(cmd.contains("| grep 'ERROR'"));
}
#[test]
fn test_shell_injection_protection() {
let args = LogsArgs {
lines: 100,
priority: None,
unit: Some("evil'; rm -rf /; echo '".to_string()),
since: None,
grep: None,
clear: false,
force: false,
kernel: false,
};
let cmd = build_journalctl_command(&args).unwrap();
assert!(cmd.contains("'\\''"));
}
#[test]
fn test_validate_kernel_unit_conflict() {
let args = LogsArgs {
lines: 100,
priority: None,
unit: Some("test.service".to_string()),
since: None,
grep: None,
clear: false,
force: false,
kernel: true,
};
assert!(validate_args(&args).is_err());
}
#[test]
fn test_validate_zero_lines() {
let args = LogsArgs {
lines: 0,
priority: None,
unit: None,
since: None,
grep: None,
clear: false,
force: false,
kernel: false,
};
assert!(validate_args(&args).is_err());
}
#[test]
fn test_validate_valid_args() {
let args = LogsArgs {
lines: 100,
priority: None,
unit: None,
since: None,
grep: None,
clear: false,
force: false,
kernel: false,
};
assert!(validate_args(&args).is_ok());
}
}