rush_sync_server/commands/list/
command.rs1use 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 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 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 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 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 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 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 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 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 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, &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}