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