use std::{ fs::{File, OpenOptions}, io::{Read, Seek, SeekFrom}, path::PathBuf, sync::mpsc::{channel, Receiver, Sender}, time::Duration, }; use notify::{event, Config, PollWatcher, RecursiveMode, Watcher, Event}; use regex::Regex; #[cfg(target_os = "windows")] use std::os::windows::fs::OpenOptionsExt; struct LogFileReader { tx: Sender, file_path: PathBuf, contents: Vec, position: u64, } impl Drop for LogFileReader { fn drop(&mut self) { log::debug!("LogFileReader dropped"); } } #[cfg(target_os = "windows")] fn share_mode(options: &mut OpenOptions) { options.share_mode(0x00000004 | 0x00000001 | 0x00000002); } #[cfg(not(target_os = "windows"))] fn share_mode(options: &mut OpenOptions) {} impl LogFileReader { fn new(file: &PathBuf, tx: Sender) -> Result { let mut lfr = LogFileReader { tx, file_path: file.to_path_buf(), contents: Vec::new(), position: 0, }; let mut options = OpenOptions::new(); options.read(true).write(false); share_mode(&mut options); let mut file = options.open(file)?; lfr.position = file .seek(SeekFrom::End(0)) .expect("Could not start at end of file"); Ok(lfr) } fn on_write_handler(&mut self) { self.contents.truncate(0); let mut options = OpenOptions::new(); options.read(true).write(false); share_mode(&mut options); let mut file = options .open(&self.file_path) .expect("Could not re-open file"); let _ = file.seek(SeekFrom::Start(self.position)); self.position += file.read_to_end(&mut self.contents).unwrap() as u64; for line in String::from_utf8_lossy(&self.contents).lines() { let line = line.to_string(); if !line.is_empty() { self.tx.send(line).expect("Could not send line to receiver"); } } // do_process(contents) } } pub fn poe_client_log_receiver(path: PathBuf) -> (Receiver, PollWatcher) { let (tx, rx) = channel(); let mut lfr = LogFileReader::new(&path, tx).unwrap(); (rx, watch_file_for_writes(path, Box::new(move || lfr.on_write_handler()))) } const POE_STEAM_APP_ID: u32 = 238960; pub fn get_poe_path() -> Option { if let Some(mut steam_dir) = steamlocate::SteamDir::locate() { if let Some(app) = steam_dir.app(&POE_STEAM_APP_ID) { let mut path = app.path.clone(); path.push("logs"); path.push("Client.txt"); return Some(path); } } None } #[allow(dead_code)] fn entered_filtered_rx<'a>(rx: &'a Receiver) -> impl Iterator + 'a { rx.iter().filter_map(|str| { if str.contains("You have entered ") { Some(str) } else { None } }) } /// Returns an iterator of strings with the id pub fn area_filtered_rx<'a>(rx: &'a Receiver) -> impl Iterator + 'a { // Generating level 68 area "1_SideArea5_6" with seed 4103532853 let reg = Regex::new("Generating level ([0-9]+) area \"(.+)\"").unwrap(); let area_id_location = 2; rx.try_iter().filter_map(move |str| { let caps = reg.captures(&str); if let Some(caps) = caps { Some(caps[area_id_location].to_string()) } else { None } }) } pub fn filter_func(str : String) -> Option { let reg = Regex::new("Generating level ([0-9]+) area \"(.+)\"").unwrap(); let area_id_location = 2; let caps = reg.captures(&str); if let Some(caps) = caps { Some(caps[area_id_location].to_string()) } else { None } } pub fn blocking_area_filtered_rx<'a>(rx: &'a Receiver) -> impl Iterator + 'a { // Generating level 68 area "1_SideArea5_6" with seed 4103532853 rx.iter() } fn watch_file_for_writes(path: PathBuf, mut on_write_handler: Box) -> PollWatcher { // Create a watcher object, delivering debounced events. // The notification back-end is selected based on the platform. let config = Config::default().with_poll_interval(Duration::from_millis(150)); let mut watcher = PollWatcher::new( move |e: Result| match e { Ok(e2) => { match e2.kind { notify::EventKind::Modify(_) => { on_write_handler() }, _ => (), } }, e => log::error!("Error watching for file something something: {e:?}"), }, config, ) .unwrap(); //let mut watcher = watcher(tx, Duration::from_millis(100)).unwrap(); // Add a path to be watched. All files and directories at that path and // below will be monitored for changes. watcher .watch(path.as_path(), RecursiveMode::NonRecursive) .unwrap(); watcher }