feat(robin): M1 System Presence — journald/kmsg/inotify watcher, pattern classifier, tray badge, chat panel #2
2 changed files with 120 additions and 3 deletions
|
|
@ -31,3 +31,6 @@ tauri-plugin-log = "2"
|
||||||
tauri-plugin-notification = "2"
|
tauri-plugin-notification = "2"
|
||||||
tauri-plugin-shell = "2"
|
tauri-plugin-shell = "2"
|
||||||
tauri-plugin-fs = "2"
|
tauri-plugin-fs = "2"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3"
|
||||||
|
|
|
||||||
|
|
@ -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::collections::HashMap;
|
||||||
|
use std::io::{Read, Seek};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
pub async fn watch(_log_paths: HashMap<String, String>, _tx: mpsc::Sender<SystemEvent>) {
|
pub async fn watch(log_paths: HashMap<String, String>, tx: mpsc::Sender<SystemEvent>) {
|
||||||
// implemented in Task 8
|
if log_paths.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let expanded: HashMap<String, String> = 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<String, (String, u64)> = 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<String>, 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<String> = 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue