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: "Monitor external processes or wait for state changes. Use to: (1) Wait for build/test completion; (2) Watch for file changes; (3) Monitor background services; (4) Track process status. Returns when monitored condition is met or timeout expires.".to_string(),
18            parameters: json!({
19                "type": "object",
20                "properties": {
21                    "mode": {
22                        "type": "string",
23                        "enum": ["process", "file", "port", "timer"],
24                        "description": "Monitor mode: 'process' watches a process, 'file' watches for file changes, 'port' waits for port availability, 'timer' is a simple countdown"
25                    },
26                    "target": {
27                        "type": "string",
28                        "description": "Target to monitor: PID or process name for 'process', file path for 'file', port number for 'port'"
29                    },
30                    "timeout": {
31                        "type": "integer",
32                        "default": 30000,
33                        "description": "Timeout in milliseconds (default 30s)"
34                    },
35                    "condition": {
36                        "type": "string",
37                        "enum": ["exit", "running", "exists", "changed", "available"],
38                        "default": "available",
39                        "description": "Condition to wait for: 'exit' waits for process to finish, 'running' waits for process to start, 'exists' waits for file to exist, 'changed' waits for file modification, 'available' waits for port to be available"
40                    }
41                },
42                "required": ["mode"]
43            }),
44        }
45    }
46
47    fn risk_level(&self) -> RiskLevel {
48        RiskLevel::Safe  // Read-only monitoring
49    }
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        // Use spawn_blocking for synchronous monitoring operations
61        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
76/// Monitor a process
77fn 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    // Parse target as PID or process name
81    let pid: Option<u32> = target_str.parse().ok();
82
83    match condition {
84        "exit" => {
85            // Wait for process to exit
86            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                // Check if process is still running
92                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            // Wait for process to start
107            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
129/// Monitor a file
130fn 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
175/// Monitor a port
176fn 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        // Try to connect to port
187        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
196/// Simple timer countdown
197fn 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
209/// Check if process is running by PID
210fn 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                // Look for PID in output
233                w.windows(pid_bytes.len()).any(|w2| w2 == pid_bytes)
234            }))
235            .unwrap_or(false)
236    }
237}
238
239/// Check if process is running by name
240fn 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}