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 ..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(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 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
80fn 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 let pid: Option<u32> = target_str.parse().ok();
92
93 match condition {
94 "exit" => {
95 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 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 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
147fn 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
199fn 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 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
230fn 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
246fn 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 w.windows(pid_bytes.len()).any(|w2| w2 == pid_bytes)
272 })
273 })
274 .unwrap_or(false)
275 }
276}
277
278fn 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}