rusmes_cli/commands/
status.rs1use 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
16pub fn run() -> Result<()> {
18 println!("Checking RusMES server status...");
19 println!();
20
21 let pid = check_pid_file()?;
23
24 let process_running = if let Some(pid) = pid {
26 check_process_running(pid)?
27 } else {
28 false
29 };
30
31 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 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 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 if process_running {
81 println!();
82 println!("Active connections: N/A (not implemented)");
83 }
84
85 Ok(())
86}
87
88fn 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
102fn check_process_running(_pid: u32) -> Result<bool> {
104 #[cfg(target_os = "linux")]
106 {
107 let proc_path = format!("/proc/{}", _pid);
108 Ok(Path::new(&proc_path).exists())
109 }
110
111 #[cfg(not(target_os = "linux"))]
113 {
114 Ok(false)
117 }
118}
119
120fn 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
130fn 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 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 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 let clock_ticks = 100; 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 Ok(0)
174 }
175}
176
177fn 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); assert_eq!(uptime, "3d 0h 0m 0s");
251 }
252}