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}