Skip to main content

mc_minder/monitor/
mod.rs

1use anyhow::{Context, Result};
2use log::{info, debug, warn};
3use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
4use regex::Regex;
5use std::path::PathBuf;
6use tokio::sync::mpsc;
7use tokio::sync::mpsc::Receiver;
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
24#[derive(Debug, Clone)]
25pub enum LogEvent {
26    Chat(ChatMessage),
27    PlayerJoin(String),
28    PlayerLeave(String),
29    PlayerDeath(String),
30    ServerStart,
31    ServerStop,
32}
33
34/// 文件标识,用于检测文件轮转
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36struct FileId {
37    size: u64,
38    modified_secs: i64,
39}
40
41impl FileId {
42    /// 从文件元数据创建文件标识
43    fn from_metadata(metadata: &std::fs::Metadata) -> Option<Self> {
44        let size = metadata.len();
45        let modified = metadata.modified().ok()?;
46        let modified_secs = modified.duration_since(std::time::UNIX_EPOCH).ok()?.as_secs() as i64;
47        Some(Self { size, modified_secs })
48    }
49}
50
51impl LogMonitor {
52    pub fn new(log_path: PathBuf) -> Result<Self> {
53        let chat_pattern = Regex::new(r"\[(\d{2}:\d{2}:\d{2})\] \[Server thread/INFO\]: <([^>]+)> (.+)")
54            .context("Failed to compile chat pattern")?;
55        
56        let join_pattern = Regex::new(r"\[(\d{2}:\d{2}:\d{2})\] \[Server thread/INFO\]: (\w+) joined the game")
57            .context("Failed to compile join pattern")?;
58        
59        let leave_pattern = Regex::new(r"\[(\d{2}:\d{2}:\d{2})\] \[Server thread/INFO\]: (\w+) left the game")
60            .context("Failed to compile leave pattern")?;
61        
62        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)")
63            .context("Failed to compile death pattern")?;
64
65        Ok(Self {
66            log_path,
67            chat_pattern,
68            join_pattern,
69            leave_pattern,
70            death_pattern,
71        })
72    }
73
74    pub fn start_monitoring(self) -> Result<Receiver<LogEvent>> {
75        let (tx, rx) = mpsc::channel(100);
76        
77        let log_path = self.log_path.clone();
78        
79        info!("Started monitoring log file: {:?}", log_path);
80
81        let mut last_offset: u64 = 0;
82        let mut last_file_id: Option<FileId> = None;
83
84        if log_path.exists() {
85            if let Ok(metadata) = std::fs::metadata(&log_path) {
86                last_offset = metadata.len();
87                last_file_id = FileId::from_metadata(&metadata);
88            }
89        } else {
90            warn!(
91                "Log file not found: {:?}. \n\
92                 Please start the Minecraft server first to generate the log file.",
93                log_path
94            );
95        }
96
97        let (notify_tx, notify_rx) = std::sync::mpsc::channel();
98        
99        let mut watcher = RecommendedWatcher::new(
100            move |res: Result<Event, notify::Error>| {
101                if let Ok(event) = res {
102                    let _ = notify_tx.send(event);
103                }
104            },
105            Config::default(),
106        ).context("Failed to create file watcher")?;
107
108        let parent_dir = log_path
109            .parent()
110            .context("Log file has no parent directory")?
111            .to_path_buf();
112        
113        let parent_dir_for_unwatch = parent_dir.clone();
114        
115        watcher
116            .watch(&parent_dir, RecursiveMode::NonRecursive)
117            .context("Failed to watch log directory")?;
118
119        let patterns = (
120            self.chat_pattern,
121            self.join_pattern,
122            self.leave_pattern,
123            self.death_pattern,
124        );
125
126        std::thread::spawn(move || {
127            loop {
128                match notify_rx.recv() {
129                    Ok(event) => {
130                        if !event.paths.iter().any(|p| p.file_name().map(|n| n == "latest.log").unwrap_or(false)) {
131                            continue;
132                        }
133
134                        match event.kind {
135                            EventKind::Modify(_) | EventKind::Create(_) => {
136                                if let Ok(events) = Self::check_file_changes(
137                                    &log_path,
138                                    &mut last_offset,
139                                    &mut last_file_id,
140                                    &patterns,
141                                ) {
142                                    for log_event in events {
143                                        if tx.blocking_send(log_event).is_err() {
144                                            debug!("Receiver dropped, stopping monitor");
145                                            return;
146                                        }
147                                    }
148                                }
149                            }
150                            EventKind::Remove(_) => {
151                                debug!("Log file removed/rotated, resetting state");
152                                last_offset = 0;
153                                last_file_id = None;
154                            }
155                            _ => {}
156                        }
157                    }
158                    Err(_) => {
159                        debug!("Notify channel closed, stopping monitor");
160                        break;
161                    }
162                }
163            }
164            let _ = watcher.unwatch(&parent_dir_for_unwatch);
165        });
166
167        Ok(rx)
168    }
169
170    fn check_file_changes(
171        log_path: &PathBuf,
172        last_offset: &mut u64,
173        last_file_id: &mut Option<FileId>,
174        patterns: &(Regex, Regex, Regex, Regex),
175    ) -> Result<Vec<LogEvent>> {
176        // 检查文件是否存在
177        if !log_path.exists() {
178            return Ok(Vec::new());
179        }
180
181        let metadata = std::fs::metadata(log_path)?;
182        let current_file_id = FileId::from_metadata(&metadata);
183
184        // 检测文件轮转:如果文件标识变化,重置偏移量
185        if let (Some(current), Some(last)) = (current_file_id, *last_file_id) {
186            if current != last {
187                debug!("File rotation detected, resetting offset");
188                *last_offset = 0;
189            }
190        }
191
192        let current_size = metadata.len();
193
194        // 如果文件变小了,说明可能被截断或轮转,重置偏移量
195        if current_size < *last_offset {
196            debug!("File size decreased, resetting offset");
197            *last_offset = 0;
198        }
199
200        // 没有新内容
201        if current_size == *last_offset {
202            return Ok(Vec::new());
203        }
204
205        // 从 last_offset 位置读取到文件末尾
206        let new_content = Self::read_from_offset(log_path, *last_offset, current_size)?;
207
208        // 更新状态
209        *last_offset = current_size;
210        *last_file_id = current_file_id;
211
212        let events = Self::parse_lines(&new_content, patterns);
213        Ok(events)
214    }
215
216    /// 从指定偏移量读取文件内容
217    fn read_from_offset(log_path: &PathBuf, offset: u64, end: u64) -> Result<String> {
218        use std::fs::File;
219        use std::io::{Read, Seek, SeekFrom};
220
221        let mut file = File::open(log_path)?;
222        file.seek(SeekFrom::Start(offset))?;
223
224        let bytes_to_read = (end - offset) as usize;
225        let mut buffer = Vec::with_capacity(bytes_to_read);
226        file.take(bytes_to_read as u64).read_to_end(&mut buffer)?;
227
228        String::from_utf8(buffer)
229            .context("Failed to convert file content to UTF-8 string")
230    }
231
232    fn parse_lines(content: &str, patterns: &(Regex, Regex, Regex, Regex)) -> Vec<LogEvent> {
233        let (chat_pattern, join_pattern, leave_pattern, death_pattern) = patterns;
234        let mut events = Vec::new();
235        
236        for line in content.lines() {
237            if let Some(caps) = chat_pattern.captures(line) {
238                if let (Some(player), Some(content)) = (caps.get(2), caps.get(3)) {
239                    events.push(LogEvent::Chat(ChatMessage {
240                        player: player.as_str().to_string(),
241                        content: content.as_str().to_string(),
242                        timestamp: chrono::Local::now(),
243                    }));
244                }
245            } else if let Some(caps) = join_pattern.captures(line) {
246                if let Some(player) = caps.get(2) {
247                    events.push(LogEvent::PlayerJoin(player.as_str().to_string()));
248                }
249            } else if let Some(caps) = leave_pattern.captures(line) {
250                if let Some(player) = caps.get(2) {
251                    events.push(LogEvent::PlayerLeave(player.as_str().to_string()));
252                }
253            } else if let Some(caps) = death_pattern.captures(line) {
254                if let Some(player) = caps.get(2) {
255                    events.push(LogEvent::PlayerDeath(player.as_str().to_string()));
256                }
257            }
258        }
259        
260        events
261    }
262}