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