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": 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 }
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 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
79fn 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 let pid: Option<u32> = target_str.parse().ok();
91
92 match condition {
93 "exit" => {
94 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 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 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
146fn 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
198fn 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 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
229fn 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
245fn 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 w.windows(pid_bytes.len()).any(|w2| w2 == pid_bytes)
271 })
272 })
273 .unwrap_or(false)
274 }
275}
276
277fn 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}