Skip to main content

matrixcode_core/tools/
monitor.rs

1use anyhow::Result;
2use async_trait::async_trait;
3use serde_json::{Value, json};
4use std::time::Duration;
5
6use super::{Tool, ToolDefinition};
7use crate::approval::RiskLevel;
8
9/// Monitor tool for watching processes and file changes
10pub struct MonitorTool;
11
12#[async_trait]
13impl Tool for MonitorTool {
14    fn definition(&self) -> ToolDefinition {
15        ToolDefinition {
16            name: "monitor".to_string(),
17            description: "监控外部进程或等待状态变化。用于:(1) 等待构建/测试完成;(2) 监视文件变化;(3) 监控后台服务;(4) 跟踪进程状态。当监控条件满足或超时到期时返回。".to_string(),
18            parameters: json!({
19                "type": "object",
20                "properties": {
21                    "mode": {
22                        "type": "string",
23                        "enum": ["process", "file", "port", "timer"],
24                        "description": "监控模式:'process' 监视进程,'file' 监视文件变化,'port' 等待端口可用,'timer' 简单计时"
25                    },
26                    "target": {
27                        "type": "string",
28                        "description": "监控目标:'process' 用 PID 或进程名,'file' 用文件路径,'port' 用端口号"
29                    },
30                    "timeout": {
31                        "type": "integer",
32                        "default": 30000,
33                        "description": "超时时间(毫秒,默认 30 秒)"
34                    },
35                    "condition": {
36                        "type": "string",
37                        "enum": ["exit", "running", "exists", "changed", "available"],
38                        "default": "available",
39                        "description": "等待条件:'exit' 等进程结束,'running' 等进程启动,'exists' 等文件存在,'changed' 等文件修改,'available' 等端口可用"
40                    }
41                },
42                "required": ["mode"]
43            }),
44        }
45    }
46
47    fn risk_level(&self) -> RiskLevel {
48        RiskLevel::Safe // Read-only monitoring
49    }
50
51    async fn execute(&self, params: Value) -> Result<String> {
52        let mode = params["mode"]
53            .as_str()
54            .ok_or_else(|| anyhow::anyhow!("missing 'mode'"))?;
55        let target = params["target"].as_str().map(|s| s.to_string());
56        let timeout_ms = params["timeout"].as_u64().unwrap_or(30000);
57        let condition = params["condition"].as_str().unwrap_or("available");
58
59        let mode = mode.to_string();
60        let condition = condition.to_string();
61
62        // Use spawn_blocking for synchronous monitoring operations
63        tokio::task::spawn_blocking(move || {
64            let timeout = Duration::from_millis(timeout_ms);
65            let start = std::time::Instant::now();
66
67            match mode.as_str() {
68                "process" => monitor_process(target.as_deref(), &condition, timeout, start),
69                "file" => monitor_file(target.as_deref(), &condition, timeout, start),
70                "port" => monitor_port(target.as_deref(), timeout, start),
71                "timer" => monitor_timer(timeout_ms, start),
72                _ => Ok(format!("Unknown monitor mode: {}", mode)),
73            }
74        })
75        .await?
76    }
77}
78
79/// Monitor a process
80fn monitor_process(
81    target: Option<&str>,
82    condition: &str,
83    timeout: Duration,
84    start: std::time::Instant,
85) -> Result<String> {
86    let target_str =
87        target.ok_or_else(|| anyhow::anyhow!("missing 'target' for process monitoring"))?;
88
89    // Parse target as PID or process name
90    let pid: Option<u32> = target_str.parse().ok();
91
92    match condition {
93        "exit" => {
94            // Wait for process to exit
95            loop {
96                if start.elapsed() > timeout {
97                    return Ok(format!(
98                        "Timeout: Process {} still running after {:.1}s",
99                        target_str,
100                        timeout.as_secs_f64()
101                    ));
102                }
103
104                // Check if process is still running
105                let running = if let Some(pid) = pid {
106                    is_process_running_by_pid(pid)
107                } else {
108                    is_process_running_by_name(target_str)
109                };
110
111                if !running {
112                    return Ok(format!("Process {} has exited", target_str));
113                }
114
115                std::thread::sleep(Duration::from_millis(500));
116            }
117        }
118        "running" => {
119            // Wait for process to start
120            loop {
121                if start.elapsed() > timeout {
122                    return Ok(format!(
123                        "Timeout: Process {} not found after {:.1}s",
124                        target_str,
125                        timeout.as_secs_f64()
126                    ));
127                }
128
129                let running = if let Some(pid) = pid {
130                    is_process_running_by_pid(pid)
131                } else {
132                    is_process_running_by_name(target_str)
133                };
134
135                if running {
136                    return Ok(format!("Process {} is now running", target_str));
137                }
138
139                std::thread::sleep(Duration::from_millis(500));
140            }
141        }
142        _ => Ok(format!("Unknown process condition: {}", condition)),
143    }
144}
145
146/// Monitor a file
147fn monitor_file(
148    target: Option<&str>,
149    condition: &str,
150    timeout: Duration,
151    start: std::time::Instant,
152) -> Result<String> {
153    let target_str =
154        target.ok_or_else(|| anyhow::anyhow!("missing 'target' for file monitoring"))?;
155    let path = std::path::Path::new(target_str);
156
157    let initial_mtime = path.metadata().and_then(|m| m.modified()).ok();
158
159    match condition {
160        "exists" => loop {
161            if start.elapsed() > timeout {
162                return Ok(format!(
163                    "Timeout: File {} does not exist after {:.1}s",
164                    target_str,
165                    timeout.as_secs_f64()
166                ));
167            }
168
169            if path.exists() {
170                return Ok(format!("File {} now exists", target_str));
171            }
172
173            std::thread::sleep(Duration::from_millis(500));
174        },
175        "changed" => loop {
176            if start.elapsed() > timeout {
177                return Ok(format!(
178                    "Timeout: File {} not changed after {:.1}s",
179                    target_str,
180                    timeout.as_secs_f64()
181                ));
182            }
183
184            let current_mtime = path.metadata().and_then(|m| m.modified()).ok();
185
186            if let (Some(initial), Some(current)) = (initial_mtime, current_mtime)
187                && current > initial
188            {
189                return Ok(format!("File {} has been modified", target_str));
190            }
191
192            std::thread::sleep(Duration::from_millis(500));
193        },
194        _ => Ok(format!("Unknown file condition: {}", condition)),
195    }
196}
197
198/// Monitor a port
199fn monitor_port(
200    target: Option<&str>,
201    timeout: Duration,
202    start: std::time::Instant,
203) -> Result<String> {
204    let target_str =
205        target.ok_or_else(|| anyhow::anyhow!("missing 'target' for port monitoring"))?;
206    let port: u16 = target_str
207        .parse()
208        .map_err(|_| anyhow::anyhow!("invalid port number: {}", target_str))?;
209
210    loop {
211        if start.elapsed() > timeout {
212            return Ok(format!(
213                "Timeout: Port {} not available after {:.1}s",
214                port,
215                timeout.as_secs_f64()
216            ));
217        }
218
219        // Try to connect to port
220        let addr = format!("127.0.0.1:{}", port);
221        if std::net::TcpStream::connect(&addr).is_ok() {
222            return Ok(format!("Port {} is now available", port));
223        }
224
225        std::thread::sleep(Duration::from_millis(500));
226    }
227}
228
229/// Simple timer countdown
230fn monitor_timer(timeout_ms: u64, start: std::time::Instant) -> Result<String> {
231    let duration = Duration::from_millis(timeout_ms);
232    loop {
233        let elapsed = start.elapsed();
234        if elapsed >= duration {
235            return Ok(format!(
236                "Timer completed after {:.1}s",
237                duration.as_secs_f64()
238            ));
239        }
240
241        std::thread::sleep(Duration::from_millis(100));
242    }
243}
244
245/// Check if process is running by PID
246fn is_process_running_by_pid(pid: u32) -> bool {
247    #[cfg(unix)]
248    {
249        use std::process::Command;
250        Command::new("ps")
251            .arg("-p")
252            .arg(pid.to_string())
253            .output()
254            .map(|o| o.status.success())
255            .unwrap_or(false)
256    }
257
258    #[cfg(windows)]
259    {
260        use std::process::Command;
261        let pid_str = pid.to_string();
262        let pid_bytes = pid_str.as_bytes();
263        Command::new("tasklist")
264            .arg("/FI")
265            .arg(format!("PID eq {}", pid))
266            .output()
267            .map(|o| {
268                o.stdout.windows(4).any(|w| {
269                    // Look for PID in output
270                    w.windows(pid_bytes.len()).any(|w2| w2 == pid_bytes)
271                })
272            })
273            .unwrap_or(false)
274    }
275}
276
277/// Check if process is running by name
278fn is_process_running_by_name(name: &str) -> bool {
279    #[cfg(unix)]
280    {
281        use std::process::Command;
282        Command::new("pgrep")
283            .arg("-x")
284            .arg(name)
285            .output()
286            .map(|o| !o.stdout.is_empty())
287            .unwrap_or(false)
288    }
289
290    #[cfg(windows)]
291    {
292        use std::process::Command;
293        Command::new("tasklist")
294            .arg("/FI")
295            .arg(format!("IMAGENAME eq {}", name))
296            .output()
297            .map(|o| o.stdout.windows(name.len()).any(|w| w == name.as_bytes()))
298            .unwrap_or(false)
299    }
300}