matrixcode_core/tools/
monitor.rs1use 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
9pub 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 }
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 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 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
89fn 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 let pid: Option<u32> = target_str.parse().ok();
101
102 match condition {
103 "exit" => {
104 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 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 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
156fn 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
208fn 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 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
239fn 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 w.windows(pid_bytes.len()).any(|w2| w2 == pid_bytes)
265 })
266 })
267 .unwrap_or(false)
268 }
269}
270
271fn 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}