diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index da51793..0aeb6d2 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -31,3 +31,6 @@ tauri-plugin-log = "2" tauri-plugin-notification = "2" tauri-plugin-shell = "2" tauri-plugin-fs = "2" + +[dev-dependencies] +tempfile = "3" diff --git a/src-tauri/src/watcher/inotify.rs b/src-tauri/src/watcher/inotify.rs index ecf30e2..3013ad2 100644 --- a/src-tauri/src/watcher/inotify.rs +++ b/src-tauri/src/watcher/inotify.rs @@ -1,7 +1,121 @@ -use super::SystemEvent; +use super::{now_unix, EventSource, SystemEvent}; +use notify::{recommended_watcher, RecursiveMode, Watcher}; use std::collections::HashMap; +use std::io::{Read, Seek}; use tokio::sync::mpsc; -pub async fn watch(_log_paths: HashMap, _tx: mpsc::Sender) { - // implemented in Task 8 +pub async fn watch(log_paths: HashMap, tx: mpsc::Sender) { + if log_paths.is_empty() { + return; + } + + let expanded: HashMap = log_paths + .iter() + .map(|(k, v)| (k.clone(), expand_tilde(v))) + .collect(); + + tokio::task::spawn_blocking(move || { + let (notify_tx, notify_rx) = std::sync::mpsc::channel(); + let mut watcher = match recommended_watcher(notify_tx) { + Ok(w) => w, + Err(e) => { + log::error!("inotify watcher init failed: {e}"); + return; + } + }; + + // positions: path -> (app_name, byte_offset) + let mut positions: HashMap = HashMap::new(); + + for (app_name, path) in &expanded { + let pb = std::path::Path::new(path); + if pb.exists() { + let len = std::fs::metadata(pb).map(|m| m.len()).unwrap_or(0); + positions.insert(path.clone(), (app_name.clone(), len)); + watcher.watch(pb, RecursiveMode::NonRecursive).ok(); + } + } + + for result in notify_rx { + if let Ok(event) = result { + for path in event.paths { + let path_str = path.to_string_lossy().to_string(); + if let Some((app_name, pos)) = positions.get_mut(&path_str) { + let (lines, new_pos) = read_new_lines(&path_str, *pos); + *pos = new_pos; + for line in lines { + tx.blocking_send(SystemEvent { + source: EventSource::AppLog { + app: app_name.clone(), + }, + raw_line: line, + timestamp: now_unix(), + }) + .ok(); + } + } + } + } + } + }); +} + +pub fn expand_tilde(path: &str) -> String { + if let Some(rest) = path.strip_prefix("~/") { + let home = std::env::var("HOME").unwrap_or_else(|_| "/home/user".into()); + format!("{home}/{rest}") + } else { + path.to_string() + } +} + +pub fn read_new_lines(path: &str, from_byte: u64) -> (Vec, u64) { + let mut file = match std::fs::File::open(path) { + Ok(f) => f, + Err(_) => return (vec![], from_byte), + }; + if file.seek(std::io::SeekFrom::Start(from_byte)).is_err() { + return (vec![], from_byte); + } + let mut content = String::new(); + let _ = file.read_to_string(&mut content); + let new_pos = from_byte + content.len() as u64; + let lines: Vec = content + .lines() + .filter(|l| !l.is_empty()) + .map(|l| l.to_string()) + .collect(); + (lines, new_pos) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn expand_tilde_home() { + let home = std::env::var("HOME").unwrap_or("/home/user".into()); + let expanded = expand_tilde("~/.config/retroarch/retroarch.log"); + assert!(expanded.starts_with(&home)); + assert!(expanded.ends_with(".config/retroarch/retroarch.log")); + } + + #[test] + fn expand_tilde_no_tilde() { + let path = "/absolute/path/to/file.log"; + assert_eq!(expand_tilde(path), path); + } + + #[tokio::test] + async fn reads_new_lines_from_file() { + use std::io::Write; + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("test.log"); + let mut f = std::fs::File::create(&path).unwrap(); + write!(f, "line one\n").unwrap(); + + let (lines, pos) = read_new_lines(path.to_str().unwrap(), 0); + assert_eq!(lines, vec!["line one".to_string()]); + assert!(pos > 0); + } }