Skip to main content

rusmes_cli/commands/
status.rs

1//! Check server status
2
3use anyhow::{Context, Result};
4use std::fs;
5use std::net::TcpStream;
6use std::path::Path;
7use std::time::Duration;
8
9#[cfg(target_os = "linux")]
10use std::io::Read;
11
12const PID_FILE: &str = "./data/rusmes.pid";
13const SMTP_DEFAULT_PORT: u16 = 25;
14const IMAP_DEFAULT_PORT: u16 = 143;
15
16/// Check server status
17pub fn run() -> Result<()> {
18    println!("Checking RusMES server status...");
19    println!();
20
21    // Check PID file
22    let pid = check_pid_file()?;
23
24    // Check if process is running
25    let process_running = if let Some(pid) = pid {
26        check_process_running(pid)?
27    } else {
28        false
29    };
30
31    // Check if ports are listening
32    let smtp_listening = check_port_listening("127.0.0.1", SMTP_DEFAULT_PORT);
33    let imap_listening = check_port_listening("127.0.0.1", IMAP_DEFAULT_PORT);
34
35    // Display status
36    println!("Server status:");
37    if process_running {
38        println!("  Status: RUNNING");
39        if let Some(pid) = pid {
40            println!("  PID: {}", pid);
41        }
42    } else if pid.is_some() {
43        println!("  Status: STOPPED (stale PID file)");
44    } else {
45        println!("  Status: STOPPED");
46    }
47
48    println!();
49    println!("Service status:");
50    println!(
51        "  SMTP (port {}): {}",
52        SMTP_DEFAULT_PORT,
53        if smtp_listening {
54            "listening"
55        } else {
56            "not listening"
57        }
58    );
59    println!(
60        "  IMAP (port {}): {}",
61        IMAP_DEFAULT_PORT,
62        if imap_listening {
63            "listening"
64        } else {
65            "not listening"
66        }
67    );
68
69    // Get uptime if running
70    if process_running {
71        if let Some(pid_val) = pid {
72            if let Ok(uptime) = get_process_uptime(pid_val) {
73                println!();
74                println!("Uptime: {}", format_uptime(uptime));
75            }
76        }
77    }
78
79    // Connection count (placeholder - would need actual implementation)
80    if process_running {
81        println!();
82        println!("Active connections: N/A (not implemented)");
83    }
84
85    Ok(())
86}
87
88/// Check PID file and return PID if exists
89fn check_pid_file() -> Result<Option<u32>> {
90    let path = Path::new(PID_FILE);
91    if !path.exists() {
92        return Ok(None);
93    }
94
95    let content = fs::read_to_string(path).context("Failed to read PID file")?;
96
97    let pid: u32 = content.trim().parse().context("Invalid PID in PID file")?;
98
99    Ok(Some(pid))
100}
101
102/// Check if process with given PID is running
103fn check_process_running(_pid: u32) -> Result<bool> {
104    // On Linux, check if /proc/PID exists
105    #[cfg(target_os = "linux")]
106    {
107        let proc_path = format!("/proc/{}", _pid);
108        Ok(Path::new(&proc_path).exists())
109    }
110
111    // On other platforms, use a different method
112    #[cfg(not(target_os = "linux"))]
113    {
114        // Try to send signal 0 (null signal) to check if process exists
115        // This is a placeholder - would need platform-specific implementation
116        Ok(false)
117    }
118}
119
120/// Check if a port is listening
121fn check_port_listening(host: &str, port: u16) -> bool {
122    let address = format!("{}:{}", host, port);
123    address
124        .parse()
125        .ok()
126        .map(|addr| TcpStream::connect_timeout(&addr, Duration::from_millis(100)).is_ok())
127        .unwrap_or(false)
128}
129
130/// Get process uptime in seconds
131fn get_process_uptime(_pid: u32) -> Result<u64> {
132    #[cfg(target_os = "linux")]
133    {
134        let stat_path = format!("/proc/{}/stat", _pid);
135        let mut file = fs::File::open(&stat_path).context("Failed to open process stat file")?;
136
137        let mut content = String::new();
138        file.read_to_string(&mut content)
139            .context("Failed to read process stat file")?;
140
141        // Parse stat file - start time is the 22nd field
142        let fields: Vec<&str> = content.split_whitespace().collect();
143        if fields.len() < 22 {
144            anyhow::bail!("Invalid stat file format");
145        }
146
147        let start_time: u64 = fields[21]
148            .parse()
149            .context("Failed to parse process start time")?;
150
151        // Get system uptime
152        let uptime_content =
153            fs::read_to_string("/proc/uptime").context("Failed to read system uptime")?;
154        let uptime_fields: Vec<&str> = uptime_content.split_whitespace().collect();
155        let system_uptime: f64 = uptime_fields[0]
156            .parse()
157            .context("Failed to parse system uptime")?;
158
159        // Get clock ticks per second
160        let clock_ticks = 100; // Usually 100 on Linux
161
162        // Calculate process uptime
163        let start_time_seconds = start_time / clock_ticks;
164        let current_time = system_uptime as u64;
165        let process_uptime = current_time.saturating_sub(start_time_seconds);
166
167        Ok(process_uptime)
168    }
169
170    #[cfg(not(target_os = "linux"))]
171    {
172        // Placeholder for other platforms
173        Ok(0)
174    }
175}
176
177/// Format uptime in human-readable format
178fn format_uptime(seconds: u64) -> String {
179    let days = seconds / 86400;
180    let hours = (seconds % 86400) / 3600;
181    let minutes = (seconds % 3600) / 60;
182    let secs = seconds % 60;
183
184    if days > 0 {
185        format!("{}d {}h {}m {}s", days, hours, minutes, secs)
186    } else if hours > 0 {
187        format!("{}h {}m {}s", hours, minutes, secs)
188    } else if minutes > 0 {
189        format!("{}m {}s", minutes, secs)
190    } else {
191        format!("{}s", secs)
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn test_format_uptime_seconds() {
201        let uptime = format_uptime(45);
202        assert_eq!(uptime, "45s");
203    }
204
205    #[test]
206    fn test_format_uptime_minutes() {
207        let uptime = format_uptime(150);
208        assert_eq!(uptime, "2m 30s");
209    }
210
211    #[test]
212    fn test_format_uptime_hours() {
213        let uptime = format_uptime(3665);
214        assert_eq!(uptime, "1h 1m 5s");
215    }
216
217    #[test]
218    fn test_format_uptime_days() {
219        let uptime = format_uptime(90061);
220        assert_eq!(uptime, "1d 1h 1m 1s");
221    }
222
223    #[test]
224    fn test_format_uptime_zero() {
225        let uptime = format_uptime(0);
226        assert_eq!(uptime, "0s");
227    }
228
229    #[test]
230    fn test_format_uptime_exact_minute() {
231        let uptime = format_uptime(60);
232        assert_eq!(uptime, "1m 0s");
233    }
234
235    #[test]
236    fn test_format_uptime_exact_hour() {
237        let uptime = format_uptime(3600);
238        assert_eq!(uptime, "1h 0m 0s");
239    }
240
241    #[test]
242    fn test_format_uptime_exact_day() {
243        let uptime = format_uptime(86400);
244        assert_eq!(uptime, "1d 0h 0m 0s");
245    }
246
247    #[test]
248    fn test_format_uptime_multiple_days() {
249        let uptime = format_uptime(259200); // 3 days
250        assert_eq!(uptime, "3d 0h 0m 0s");
251    }
252}