Skip to main content

rush_sync_server/commands/list/
command.rs

1use crate::commands::command::Command;
2use crate::core::prelude::*;
3use crate::server::types::{ServerContext, ServerStatus};
4
5#[derive(Debug, Default)]
6pub struct ListCommand;
7
8impl ListCommand {
9    pub fn new() -> Self {
10        Self
11    }
12}
13
14impl Command for ListCommand {
15    fn name(&self) -> &'static str {
16        "list"
17    }
18    fn description(&self) -> &'static str {
19        "List all web servers (persistent)"
20    }
21    fn matches(&self, command: &str) -> bool {
22        let cmd = command.trim().to_lowercase();
23        cmd == "list" || cmd == "list servers" || cmd == "list server"
24    }
25
26    fn execute_sync(&self, args: &[&str]) -> Result<String> {
27        let config = get_config()?;
28        let ctx = crate::server::shared::get_shared_context();
29
30        let opts = Self::parse_args(args);
31
32        // Special mode: list memory
33        if opts.show_memory {
34            return Ok(self.list_memory(ctx, &config));
35        }
36
37        Ok(self.list_servers(ctx, &config, opts.status_filter, opts.sort_mode))
38    }
39
40    fn priority(&self) -> u8 {
41        60
42    }
43}
44
45#[derive(Debug, Clone, Copy)]
46enum SortMode {
47    PortAsc,
48    PortDesc,
49    NameAsc,
50    NameDesc,
51}
52
53struct ListOpts {
54    status_filter: Option<ServerStatus>,
55    sort_mode: SortMode,
56    show_memory: bool,
57}
58
59impl ListCommand {
60    /// Parse args: status filter + sort flags + special modes
61    fn parse_args(args: &[&str]) -> ListOpts {
62        let mut status_filter = None;
63        let mut sort_mode = SortMode::PortAsc;
64        let mut show_memory = false;
65
66        let mut i = 0;
67        while i < args.len() {
68            let arg = args[i].to_lowercase();
69            match arg.as_str() {
70                "running" => status_filter = Some(ServerStatus::Running),
71                "stopped" => status_filter = Some(ServerStatus::Stopped),
72                "failed" => status_filter = Some(ServerStatus::Failed),
73                "memory" | "mem" => show_memory = true,
74                "-port" | "--port" => {
75                    let dir = args.get(i + 1).map(|s| s.to_lowercase());
76                    sort_mode = if dir.as_deref() == Some("desc") {
77                        i += 1;
78                        SortMode::PortDesc
79                    } else {
80                        if dir.as_deref() == Some("asc") { i += 1; }
81                        SortMode::PortAsc
82                    };
83                }
84                "-name" | "--name" => {
85                    let dir = args.get(i + 1).map(|s| s.to_lowercase());
86                    sort_mode = if dir.as_deref() == Some("desc") {
87                        i += 1;
88                        SortMode::NameDesc
89                    } else {
90                        if dir.as_deref() == Some("asc") { i += 1; }
91                        SortMode::NameAsc
92                    };
93                }
94                _ => {}
95            }
96            i += 1;
97        }
98
99        ListOpts { status_filter, sort_mode, show_memory }
100    }
101
102    fn list_servers(
103        &self,
104        ctx: &ServerContext,
105        config: &Config,
106        status_filter: Option<ServerStatus>,
107        sort_mode: SortMode,
108    ) -> String {
109        let servers = match ctx.servers.read() {
110            Ok(s) => s,
111            Err(e) => {
112                log::error!("servers lock poisoned: {}", e);
113                return "Error: server lock poisoned".to_string();
114            }
115        };
116
117        if servers.is_empty() {
118            return "No servers created. Use 'create' to add one.".to_string();
119        }
120
121        let mut server_list: Vec<_> = servers.values().collect();
122
123        // Filter
124        if let Some(filter) = status_filter {
125            server_list.retain(|s| s.status == filter);
126        }
127
128        if server_list.is_empty() {
129            let filter_name = match status_filter {
130                Some(ServerStatus::Running) => "running",
131                Some(ServerStatus::Stopped) => "stopped",
132                Some(ServerStatus::Failed) => "failed",
133                None => "matching",
134            };
135            return format!("No {} servers found.", filter_name);
136        }
137
138        // Sort
139        match sort_mode {
140            SortMode::PortAsc => server_list.sort_by_key(|s| s.port),
141            SortMode::PortDesc => server_list.sort_by(|a, b| b.port.cmp(&a.port)),
142            SortMode::NameAsc => server_list.sort_by(|a, b| a.name.cmp(&b.name)),
143            SortMode::NameDesc => server_list.sort_by(|a, b| b.name.cmp(&a.name)),
144        }
145
146        let running = servers
147            .values()
148            .filter(|s| s.status == ServerStatus::Running)
149            .count();
150        let total = servers.len();
151
152        let filter_label = match status_filter {
153            Some(ServerStatus::Running) => " [Running]",
154            Some(ServerStatus::Stopped) => " [Stopped]",
155            Some(ServerStatus::Failed) => " [Failed]",
156            None => "",
157        };
158
159        let mut result = format!(
160            "\n  Servers ({}/{} running, max {}){}\n\n",
161            running, total, config.server.max_concurrent, filter_label
162        );
163
164        for (i, server) in server_list.iter().enumerate() {
165            let status = match server.status {
166                ServerStatus::Running => "[Running]",
167                ServerStatus::Stopped => "[Stopped]",
168                ServerStatus::Failed => "[Failed]",
169            };
170
171            let url = format!(
172                "http://{}:{}",
173                config.server.bind_address, server.port
174            );
175
176            result.push_str(&format!(
177                "  {:>3}. {:<12} {}  {}\n",
178                i + 1,
179                server.name,
180                url,
181                status,
182            ));
183        }
184
185        result
186    }
187
188    /// Show memory/disk usage per server directory + process total
189    fn list_memory(&self, ctx: &ServerContext, _config: &Config) -> String {
190        let servers = match ctx.servers.read() {
191            Ok(s) => s,
192            Err(e) => {
193                log::error!("servers lock poisoned: {}", e);
194                return "Error: server lock poisoned".to_string();
195            }
196        };
197
198        if servers.is_empty() {
199            return "No servers created.".to_string();
200        }
201
202        let mut server_list: Vec<_> = servers.values().collect();
203        server_list.sort_by_key(|s| s.port);
204
205        let base_dir = crate::core::helpers::get_base_dir().ok();
206
207        // Collect sizes
208        let mut entries: Vec<(String, u16, String, u64)> = Vec::new();
209        let mut total_disk: u64 = 0;
210
211        for server in &server_list {
212            let dir_size = base_dir.as_ref().map_or(0, |base| {
213                let dir = base.join("www").join(format!("{}-[{}]", server.name, server.port));
214                Self::dir_size(&dir)
215            });
216            total_disk += dir_size;
217
218            let status = match server.status {
219                ServerStatus::Running => "[Running]",
220                ServerStatus::Stopped => "[Stopped]",
221                ServerStatus::Failed => "[Failed]",
222            };
223
224            entries.push((server.name.clone(), server.port, status.to_string(), dir_size));
225        }
226
227        // Sort by size descending
228        entries.sort_by(|a, b| b.3.cmp(&a.3));
229
230        let process_mem = Self::get_process_memory();
231
232        let mut result = format!(
233            "\n  Memory & Disk Usage ({} servers)\n\n",
234            server_list.len()
235        );
236
237        for (i, (name, port, status, size)) in entries.iter().enumerate() {
238            result.push_str(&format!(
239                "  {:>3}. {:<12} :{:<5} {:>8}  {}\n",
240                i + 1,
241                name,
242                port,
243                Self::format_bytes(*size),
244                status,
245            ));
246        }
247
248        result.push_str(&format!(
249            "\n  Total disk: {}",
250            Self::format_bytes(total_disk)
251        ));
252
253        if !process_mem.is_empty() {
254            result.push_str(&format!("  |  Process RAM: {}", process_mem));
255        }
256
257        result.push('\n');
258        result
259    }
260
261    /// Calculate directory size recursively
262    fn dir_size(path: &std::path::Path) -> u64 {
263        if !path.exists() {
264            return 0;
265        }
266        let mut total = 0u64;
267        if let Ok(entries) = std::fs::read_dir(path) {
268            for entry in entries.flatten() {
269                let meta = match entry.metadata() {
270                    Ok(m) => m,
271                    Err(_) => continue,
272                };
273                if meta.is_dir() {
274                    total += Self::dir_size(&entry.path());
275                } else {
276                    total += meta.len();
277                }
278            }
279        }
280        total
281    }
282
283    fn format_bytes(bytes: u64) -> String {
284        if bytes == 0 {
285            return "0 B".to_string();
286        }
287        let units = ["B", "KB", "MB", "GB"];
288        let mut size = bytes as f64;
289        let mut unit_idx = 0;
290        while size >= 1024.0 && unit_idx < units.len() - 1 {
291            size /= 1024.0;
292            unit_idx += 1;
293        }
294        if unit_idx == 0 {
295            format!("{} B", bytes)
296        } else {
297            format!("{:.1} {}", size, units[unit_idx])
298        }
299    }
300
301    /// Get process RSS memory
302    fn get_process_memory() -> String {
303        #[cfg(target_os = "macos")]
304        {
305            use std::mem;
306            extern "C" {
307                fn mach_task_self() -> u32;
308                fn task_info(
309                    task: u32,
310                    flavor: u32,
311                    info: *mut libc::c_void,
312                    count: *mut u32,
313                ) -> i32;
314            }
315
316            #[repr(C)]
317            struct MachTaskBasicInfo {
318                virtual_size: u64,
319                resident_size: u64,
320                resident_size_max: u64,
321                user_time: [u32; 2],
322                system_time: [u32; 2],
323                policy: i32,
324                suspend_count: i32,
325            }
326
327            let mut info: MachTaskBasicInfo = unsafe { mem::zeroed() };
328            let mut count = (mem::size_of::<MachTaskBasicInfo>() / mem::size_of::<u32>()) as u32;
329
330            let result = unsafe {
331                task_info(
332                    mach_task_self(),
333                    20, // MACH_TASK_BASIC_INFO
334                    &mut info as *mut _ as *mut libc::c_void,
335                    &mut count,
336                )
337            };
338
339            if result == 0 {
340                let rss_mb = info.resident_size as f64 / (1024.0 * 1024.0);
341                format!("{:.1} MB", rss_mb)
342            } else {
343                String::new()
344            }
345        }
346        #[cfg(target_os = "linux")]
347        {
348            if let Ok(status) = std::fs::read_to_string("/proc/self/status") {
349                for line in status.lines() {
350                    if line.starts_with("VmRSS:") {
351                        let kb: f64 = line
352                            .split_whitespace()
353                            .nth(1)
354                            .and_then(|s| s.parse().ok())
355                            .unwrap_or(0.0);
356                        return format!("{:.1} MB", kb / 1024.0);
357                    }
358                }
359            }
360            String::new()
361        }
362        #[cfg(not(any(target_os = "macos", target_os = "linux")))]
363        {
364            String::new()
365        }
366    }
367}