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            ..Default::default()
45        }
46    }
47
48    fn risk_level(&self) -> RiskLevel {
49        RiskLevel::Safe // Read-only monitoring
50    }
51
52    async fn execute(&self, params: Value) -> Result<String> {
53        let mode = params["mode"]
54            .as_str()
55            .ok_or_else(|| anyhow::anyhow!("missing 'mode'"))?;
56        let target = params["target"].as_str().map(|s| s.to_string());
57        let timeout_ms = params["timeout"].as_u64().unwrap_or(30000);
58        let condition = params["condition"].as_str().unwrap_or("available");
59
60        let mode = mode.to_string();
61        let condition = condition.to_string();
62
63        // Use spawn_blocking for synchronous monitoring operations
64        tokio::task::spawn_blocking(move || {
65            let timeout = Duration::from_millis(timeout_ms);
66            let start = std::time::Instant::now();
67
68            match mode.as_str() {
69                "process" => monitor_process(target.as_deref(), &condition, timeout, start),
70                "file" => monitor_file(target.as_deref(), &condition, timeout, start),
71                "port" => monitor_port(target.as_deref(), timeout, start),
72                "timer" => monitor_timer(timeout_ms, start),
73                _ => Ok(format!("Unknown monitor mode: {}", mode)),
74            }
75        })
76        .await?
77    }
78}
79
80/// Monitor a process
81fn monitor_process(
82    target: Option<&str>,
83    condition: &str,
84    timeout: Duration,
85    start: std::time::Instant,
86) -> Result<String> {
87    let target_str =
88        target.ok_or_else(|| anyhow::anyhow!("missing 'target' for process monitoring"))?;
89
90    // Parse target as PID or process name
91    let pid: Option<u32> = target_str.parse().ok();
92
93    match condition {
94        "exit" => {
95            // Wait for process to exit
96            loop {
97                if start.elapsed() > timeout {
98                    return Ok(format!(
99                        "Timeout: Process {} still running after {:.1}s",
100                        target_str,
101                        timeout.as_secs_f64()
102                    ));
103                }
104
105                // Check if process is still running
106                let running = if let Some(pid) = pid {
107                    is_process_running_by_pid(pid)
108                } else {
109                    is_process_running_by_name(target_str)
110                };
111
112                if !running {
113                    return Ok(format!("Process {} has exited", target_str));
114                }
115
116                std::thread::sleep(Duration::from_millis(500));
117            }
118        }
119        "running" => {
120            // Wait for process to start
121            loop {
122                if start.elapsed() > timeout {
123                    return Ok(format!(
124                        "Timeout: Process {} not found after {:.1}s",
125                        target_str,
126                        timeout.as_secs_f64()
127                    ));
128                }
129
130                let running = if let Some(pid) = pid {
131                    is_process_running_by_pid(pid)
132                } else {
133                    is_process_running_by_name(target_str)
134                };
135
136                if running {
137                    return Ok(format!("Process {} is now running", target_str));
138                }
139
140                std::thread::sleep(Duration::from_millis(500));
141            }
142        }
143        _ => Ok(format!("Unknown process condition: {}", condition)),
144    }
145}
146
147/// Monitor a file
148fn monitor_file(
149    target: Option<&str>,
150    condition: &str,
151    timeout: Duration,
152    start: std::time::Instant,
153) -> Result<String> {
154    let target_str =
155        target.ok_or_else(|| anyhow::anyhow!("missing 'target' for file monitoring"))?;
156    let path = std::path::Path::new(target_str);
157
158    let initial_mtime = path.metadata().and_then(|m| m.modified()).ok();
159
160    match condition {
161        "exists" => loop {
162            if start.elapsed() > timeout {
163                return Ok(format!(
164                    "Timeout: File {} does not exist after {:.1}s",
165                    target_str,
166                    timeout.as_secs_f64()
167                ));
168            }
169
170            if path.exists() {
171                return Ok(format!("File {} now exists", target_str));
172            }
173
174            std::thread::sleep(Duration::from_millis(500));
175        },
176        "changed" => loop {
177            if start.elapsed() > timeout {
178                return Ok(format!(
179                    "Timeout: File {} not changed after {:.1}s",
180                    target_str,
181                    timeout.as_secs_f64()
182                ));
183            }
184
185            let current_mtime = path.metadata().and_then(|m| m.modified()).ok();
186
187            if let (Some(initial), Some(current)) = (initial_mtime, current_mtime)
188                && current > initial
189            {
190                return Ok(format!("File {} has been modified", target_str));
191            }
192
193            std::thread::sleep(Duration::from_millis(500));
194        },
195        _ => Ok(format!("Unknown file condition: {}", condition)),
196    }
197}
198
199/// Monitor a port
200fn monitor_port(
201    target: Option<&str>,
202    timeout: Duration,
203    start: std::time::Instant,
204) -> Result<String> {
205    let target_str =
206        target.ok_or_else(|| anyhow::anyhow!("missing 'target' for port monitoring"))?;
207    let port: u16 = target_str
208        .parse()
209        .map_err(|_| anyhow::anyhow!("invalid port number: {}", target_str))?;
210
211    loop {
212        if start.elapsed() > timeout {
213            return Ok(format!(
214                "Timeout: Port {} not available after {:.1}s",
215                port,
216                timeout.as_secs_f64()
217            ));
218        }
219
220        // Try to connect to port
221        let addr = format!("127.0.0.1:{}", port);
222        if std::net::TcpStream::connect(&addr).is_ok() {
223            return Ok(format!("Port {} is now available", port));
224        }
225
226        std::thread::sleep(Duration::from_millis(500));
227    }
228}
229
230/// Simple timer countdown
231fn monitor_timer(timeout_ms: u64, start: std::time::Instant) -> Result<String> {
232    let duration = Duration::from_millis(timeout_ms);
233    loop {
234        let elapsed = start.elapsed();
235        if elapsed >= duration {
236            return Ok(format!(
237                "Timer completed after {:.1}s",
238                duration.as_secs_f64()
239            ));
240        }
241
242        std::thread::sleep(Duration::from_millis(100));
243    }
244}
245
246/// Check if process is running by PID
247fn is_process_running_by_pid(pid: u32) -> bool {
248    #[cfg(unix)]
249    {
250        use std::process::Command;
251        Command::new("ps")
252            .arg("-p")
253            .arg(pid.to_string())
254            .output()
255            .map(|o| o.status.success())
256            .unwrap_or(false)
257    }
258
259    #[cfg(windows)]
260    {
261        use std::process::Command;
262        let pid_str = pid.to_string();
263        let pid_bytes = pid_str.as_bytes();
264        Command::new("tasklist")
265            .arg("/FI")
266            .arg(format!("PID eq {}", pid))
267            .output()
268            .map(|o| {
269                o.stdout.windows(4).any(|w| {
270                    // Look for PID in output
271                    w.windows(pid_bytes.len()).any(|w2| w2 == pid_bytes)
272                })
273            })
274            .unwrap_or(false)
275    }
276}
277
278/// Check if process is running by name
279fn is_process_running_by_name(name: &str) -> bool {
280    #[cfg(unix)]
281    {
282        use std::process::Command;
283        Command::new("pgrep")
284            .arg("-x")
285            .arg(name)
286            .output()
287            .map(|o| !o.stdout.is_empty())
288            .unwrap_or(false)
289    }
290
291    #[cfg(windows)]
292    {
293        use std::process::Command;
294        Command::new("tasklist")
295            .arg("/FI")
296            .arg(format!("IMAGENAME eq {}", name))
297            .output()
298            .map(|o| o.stdout.windows(name.len()).any(|w| w == name.as_bytes()))
299            .unwrap_or(false)
300    }
301}