1use std::io::{Read, Seek, SeekFrom, Write};
10use std::time::Duration;
11
12use crate::app::{EXIT_RUNTIME, EXIT_USAGE};
13
14pub async fn run(args: &[String]) -> i32 {
16 match args.first().map(String::as_str) {
17 Some("tail") => tail().await,
18 Some("path") => match rpi_extensions::event_log_path() {
19 Some(path) => {
20 println!("{}", path.display());
21 0
22 }
23 None => {
24 eprintln!("error: could not resolve the event log path (set RPI_EVENT_LOG_PATH)");
25 EXIT_RUNTIME
26 }
27 },
28 Some("--help" | "-h") | None => {
29 print_help();
30 0
31 }
32 Some(other) => {
33 eprintln!("error: unknown `events` subcommand `{other}`");
34 print_help();
35 EXIT_USAGE
36 }
37 }
38}
39
40fn print_help() {
41 println!("Usage: rpi events <command>");
42 println!();
43 println!("Commands:");
44 println!(" path Print the event log path (JSONL)");
45 println!(" tail Print the log and follow new entries (Ctrl+C to stop)");
46 println!();
47 println!("The journal is written only when RPI_EVENT_LOG=1 is set at run time.");
48}
49
50async fn tail() -> i32 {
54 let Some(path) = rpi_extensions::event_log_path() else {
55 eprintln!("error: could not resolve the event log path (set RPI_EVENT_LOG_PATH)");
56 return EXIT_RUNTIME;
57 };
58 eprintln!("tailing {} — Ctrl+C to stop", path.display());
59 let mut offset = 0u64;
60 loop {
61 if let Ok(mut file) = std::fs::File::open(&path) {
62 let len = file.metadata().map(|m| m.len()).unwrap_or(0);
63 if len < offset {
65 offset = 0;
66 }
67 if len > offset && file.seek(SeekFrom::Start(offset)).is_ok() {
68 let mut chunk = String::new();
69 if file.read_to_string(&mut chunk).is_ok() {
70 if let Some(newline) = chunk.rfind('\n') {
71 print!("{}", &chunk[..=newline]);
72 let _ = std::io::stdout().flush();
73 offset += (newline + 1) as u64;
74 }
75 }
76 }
77 }
78 tokio::time::sleep(Duration::from_millis(400)).await;
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[tokio::test]
87 async fn help_and_unknown_are_handled() {
88 assert_eq!(run(&[]).await, 0);
89 assert_eq!(run(&["--help".to_string()]).await, 0);
90 assert_eq!(run(&["bogus".to_string()]).await, EXIT_USAGE);
91 }
92
93 #[tokio::test]
94 async fn path_command_returns_a_valid_code() {
95 let code = run(&["path".to_string()]).await;
98 assert!(code == 0 || code == EXIT_RUNTIME);
99 }
100}