Skip to main content

mc_minder/
log_monitor.rs

1use anyhow::{Result, Context};
2use log::{info, debug};
3use regex::Regex;
4use std::path::PathBuf;
5use std::sync::Arc;
6use tokio::sync::mpsc;
7use tokio::time::{interval, Duration};
8
9#[derive(Debug, Clone)]
10pub struct ChatMessage {
11    pub player: String,
12    pub content: String,
13    pub timestamp: chrono::DateTime<chrono::Local>,
14}
15
16pub struct LogMonitor {
17    log_path: PathBuf,
18    chat_pattern: Regex,
19    join_pattern: Regex,
20    leave_pattern: Regex,
21    death_pattern: Regex,
22}
23
24impl LogMonitor {
25    pub fn new(log_path: PathBuf) -> Result<Self> {
26        let chat_pattern = Regex::new(r"\[(\d{2}:\d{2}:\d{2})\] \[Server thread/INFO\]: <([^>]+)> (.+)")
27            .context("Failed to compile chat pattern")?;
28        
29        let join_pattern = Regex::new(r"\[(\d{2}:\d{2}:\d{2})\] \[Server thread/INFO\]: (\w+) joined the game")
30            .context("Failed to compile join pattern")?;
31        
32        let leave_pattern = Regex::new(r"\[(\d{2}:\d{2}:\d{2})\] \[Server thread/INFO\]: (\w+) left the game")
33            .context("Failed to compile leave pattern")?;
34        
35        let death_pattern = Regex::new(r"\[(\d{2}:\d{2}:\d{2})\] \[Server thread/INFO\]: (\w+) .*(died|was|fell|drowned|blew up|burned|froze|suffocated|starved)")
36            .context("Failed to compile death pattern")?;
37
38        Ok(Self {
39            log_path,
40            chat_pattern,
41            join_pattern,
42            leave_pattern,
43            death_pattern,
44        })
45    }
46
47    pub async fn start_monitoring(
48        self,
49        tx: mpsc::Sender<LogEvent>,
50        shutdown: Arc<tokio::sync::Notify>,
51    ) -> Result<()> {
52        info!("Started monitoring log file: {:?}", self.log_path);
53
54        let mut last_size = std::fs::metadata(&self.log_path)
55            .map(|m| m.len())
56            .unwrap_or(0);
57
58        let mut poll_interval = interval(Duration::from_millis(500));
59
60        loop {
61            tokio::select! {
62                _ = shutdown.notified() => {
63                    info!("Log monitor shutting down");
64                    break;
65                }
66                
67                _ = poll_interval.tick() => {
68                    match self.check_for_new_content(&mut last_size) {
69                        Ok(Some(events)) => {
70                            for log_event in events {
71                                if tx.send(log_event).await.is_err() {
72                                    debug!("Receiver dropped, stopping monitor");
73                                    return Ok(());
74                                }
75                            }
76                        }
77                        Ok(None) => {}
78                        Err(e) => {
79                            debug!("Error checking log content: {}", e);
80                        }
81                    }
82                }
83            }
84        }
85
86        Ok(())
87    }
88
89    fn check_for_new_content(&self, last_size: &mut u64) -> Result<Option<Vec<LogEvent>>> {
90        let metadata = std::fs::metadata(&self.log_path)?;
91        let current_size = metadata.len();
92
93        if current_size < *last_size {
94            *last_size = 0;
95            return Ok(None);
96        }
97
98        if current_size == *last_size {
99            return Ok(None);
100        }
101
102        let new_bytes = current_size - *last_size;
103        let file = std::fs::File::open(&self.log_path)?;
104        use std::io::{Read, Seek, SeekFrom};
105        let mut reader = std::io::BufReader::new(file);
106        reader.seek(SeekFrom::End(-(new_bytes as i64)))?;
107
108        let mut new_content = String::new();
109        reader.read_to_string(&mut new_content)?;
110        *last_size = current_size;
111
112        let events = self.parse_lines(&new_content);
113        if events.is_empty() {
114            Ok(None)
115        } else {
116            Ok(Some(events))
117        }
118    }
119
120    fn parse_lines(&self, content: &str) -> Vec<LogEvent> {
121        let mut events = Vec::new();
122        
123        for line in content.lines() {
124            if let Some(caps) = self.chat_pattern.captures(line) {
125                if let (Some(player), Some(content)) = (caps.get(2), caps.get(3)) {
126                    events.push(LogEvent::Chat(ChatMessage {
127                        player: player.as_str().to_string(),
128                        content: content.as_str().to_string(),
129                        timestamp: chrono::Local::now(),
130                    }));
131                }
132            } else if let Some(caps) = self.join_pattern.captures(line) {
133                if let Some(player) = caps.get(2) {
134                    events.push(LogEvent::PlayerJoin(player.as_str().to_string()));
135                }
136            } else if let Some(caps) = self.leave_pattern.captures(line) {
137                if let Some(player) = caps.get(2) {
138                    events.push(LogEvent::PlayerLeave(player.as_str().to_string()));
139                }
140            } else if let Some(caps) = self.death_pattern.captures(line) {
141                if let Some(player) = caps.get(2) {
142                    events.push(LogEvent::PlayerDeath(player.as_str().to_string()));
143                }
144            }
145        }
146        
147        events
148    }
149}
150
151#[derive(Debug, Clone)]
152pub enum LogEvent {
153    Chat(ChatMessage),
154    PlayerJoin(String),
155    PlayerLeave(String),
156    PlayerDeath(String),
157    ServerStart,
158    ServerStop,
159}