rush_sync_server/commands/stop/
command.rs

1// Enhanced src/commands/stop/command.rs - RANGE & BULK SUPPORT
2use crate::commands::command::Command;
3use crate::core::prelude::*;
4use crate::server::types::{ServerContext, ServerStatus};
5use crate::server::utils::validation::find_server;
6use std::time::Duration;
7
8#[derive(Debug, Default)]
9pub struct StopCommand;
10
11impl StopCommand {
12    pub fn new() -> Self {
13        Self
14    }
15}
16
17impl Command for StopCommand {
18    fn name(&self) -> &'static str {
19        "stop"
20    }
21
22    fn description(&self) -> &'static str {
23        "Stop server(s) - supports ranges and bulk operations"
24    }
25
26    fn matches(&self, command: &str) -> bool {
27        command.trim().to_lowercase().starts_with("stop")
28    }
29
30    fn execute_sync(&self, args: &[&str]) -> Result<String> {
31        if args.is_empty() {
32            return Err(AppError::Validation(
33                "Server-ID/Name fehlt! Verwende 'stop <ID>', 'stop 1-3', 'stop all'".to_string(),
34            ));
35        }
36
37        let config = get_config()?;
38        let ctx = crate::server::shared::get_shared_context();
39
40        match self.parse_stop_args(args) {
41            StopMode::Single(identifier) => self.stop_single_server(&config, ctx, &identifier),
42            StopMode::Range(start, end) => self.stop_range_servers(&config, ctx, start, end),
43            StopMode::All => self.stop_all_servers(&config, ctx),
44            StopMode::Invalid(error) => Err(AppError::Validation(error)),
45        }
46    }
47
48    fn priority(&self) -> u8 {
49        67
50    }
51}
52
53#[derive(Debug)]
54enum StopMode {
55    Single(String),
56    Range(u32, u32),
57    All,
58    Invalid(String),
59}
60
61impl StopCommand {
62    // Parse different stop argument patterns
63    fn parse_stop_args(&self, args: &[&str]) -> StopMode {
64        if args.len() != 1 {
65            return StopMode::Invalid("Too many arguments".to_string());
66        }
67
68        let arg = args[0];
69
70        // "stop all"
71        if arg.eq_ignore_ascii_case("all") {
72            return StopMode::All;
73        }
74
75        // "stop 1-3" or "stop 001-005"
76        if let Some((start_str, end_str)) = arg.split_once('-') {
77            match (start_str.parse::<u32>(), end_str.parse::<u32>()) {
78                (Ok(start), Ok(end)) => {
79                    if start == 0 || end == 0 {
80                        return StopMode::Invalid("Range indices must be > 0".to_string());
81                    }
82                    if start > end {
83                        return StopMode::Invalid("Start must be <= end in range".to_string());
84                    }
85                    if end - start > 20 {
86                        return StopMode::Invalid(
87                            "Maximum 20 servers in range operation".to_string(),
88                        );
89                    }
90                    StopMode::Range(start, end)
91                }
92                _ => StopMode::Single(arg.to_string()),
93            }
94        } else {
95            // Single server by ID/name/number
96            StopMode::Single(arg.to_string())
97        }
98    }
99
100    // Stop single server (enhanced from original)
101    fn stop_single_server(
102        &self,
103        config: &Config,
104        ctx: &ServerContext,
105        identifier: &str,
106    ) -> Result<String> {
107        let (server_info, handle) = {
108            let servers_guard = ctx
109                .servers
110                .read()
111                .map_err(|_| AppError::Validation("Server-Context lock poisoned".to_string()))?;
112
113            let server_info = find_server(&servers_guard, identifier)?.clone();
114
115            if server_info.status != ServerStatus::Running {
116                return Ok(format!(
117                    "Server '{}' is not active (Status: {})",
118                    server_info.name, server_info.status
119                ));
120            }
121
122            // Handle atomisch entfernen
123            let handle = {
124                let mut handles_guard = ctx.handles.write().map_err(|_| {
125                    AppError::Validation("Handle-Context lock poisoned".to_string())
126                })?;
127                handles_guard.remove(&server_info.id)
128            };
129
130            (server_info, handle)
131        };
132
133        log::info!(
134            "Stopping server {} on port {}",
135            server_info.id,
136            server_info.port
137        );
138
139        // Status sofort auf "Stopped" setzen
140        self.update_server_status(ctx, &server_info.id, ServerStatus::Stopped);
141
142        // Browser-Benachrichtigung
143        self.notify_browser_shutdown(&server_info);
144
145        if let Some(handle) = handle {
146            // Graceful shutdown
147            self.shutdown_server_gracefully(handle, server_info.id.clone(), config);
148
149            // Persistence update (nicht blockierend)
150            let server_id = server_info.id.clone();
151            tokio::spawn(async move {
152                crate::server::shared::persist_server_update(&server_id, ServerStatus::Stopped)
153                    .await;
154            });
155
156            // Kurze Pause für konsistente Timing
157            std::thread::sleep(Duration::from_millis(
158                config.server.startup_delay_ms.min(500),
159            ));
160
161            let running_count = {
162                let servers = ctx.servers.read().unwrap_or_else(|e| {
163                    log::warn!("Server lock poisoned: {}", e);
164                    e.into_inner()
165                });
166                servers
167                    .values()
168                    .filter(|s| s.status == ServerStatus::Running)
169                    .count()
170            };
171
172            Ok(format!(
173                "Server '{}' stopped [PERSISTENT] ({}/{} running)",
174                server_info.name, running_count, config.server.max_concurrent
175            ))
176        } else {
177            // Handle war bereits weg - nur Status updaten
178            let server_id = server_info.id.clone();
179            tokio::spawn(async move {
180                crate::server::shared::persist_server_update(&server_id, ServerStatus::Stopped)
181                    .await;
182            });
183
184            Ok(format!(
185                "Server '{}' was already stopped [PERSISTENT]",
186                server_info.name
187            ))
188        }
189    }
190
191    // Stop servers by range (e.g., "stop 1-3")
192    fn stop_range_servers(
193        &self,
194        config: &Config,
195        ctx: &ServerContext,
196        start: u32,
197        end: u32,
198    ) -> Result<String> {
199        let mut results = Vec::new();
200        let mut stopped_count = 0;
201        let mut failed_count = 0;
202
203        for i in start..=end {
204            let identifier = format!("{}", i);
205
206            match self.stop_single_server(config, ctx, &identifier) {
207                Ok(message) => {
208                    if message.contains("stopped [PERSISTENT]") {
209                        stopped_count += 1;
210                        results.push(format!("Server {}: Stopped", i));
211                    } else {
212                        results.push(format!("Server {}: {}", i, message));
213                    }
214                }
215                Err(e) => {
216                    failed_count += 1;
217                    results.push(format!("Server {}: Failed - {}", i, e));
218                }
219            }
220        }
221
222        let summary = format!(
223            "Range stop completed: {} stopped, {} failed (Range: {}-{})",
224            stopped_count, failed_count, start, end
225        );
226
227        if results.is_empty() {
228            Ok(summary)
229        } else {
230            Ok(format!("{}\n\nResults:\n{}", summary, results.join("\n")))
231        }
232    }
233
234    // Stop all running servers
235    fn stop_all_servers(&self, config: &Config, ctx: &ServerContext) -> Result<String> {
236        let running_servers: Vec<_> = {
237            let servers = ctx.servers.read().unwrap();
238            servers
239                .values()
240                .filter(|s| s.status == ServerStatus::Running)
241                .map(|s| (s.id.clone(), s.name.clone()))
242                .collect()
243        };
244
245        if running_servers.is_empty() {
246            return Ok("No running servers to stop".to_string());
247        }
248
249        if running_servers.len() > 20 {
250            return Err(AppError::Validation(
251                "Too many servers to stop at once (max 20). Use ranges instead.".to_string(),
252            ));
253        }
254
255        let mut results = Vec::new();
256        let mut stopped_count = 0;
257        let mut failed_count = 0;
258
259        // Stop servers in parallel for better performance
260        let server_stops: Vec<_> = running_servers
261            .into_iter()
262            .map(|(server_id, server_name)| {
263                match self.stop_single_server(config, ctx, &server_id) {
264                    Ok(message) => {
265                        if message.contains("stopped [PERSISTENT]") {
266                            stopped_count += 1;
267                            (server_name, "Stopped".to_string())
268                        } else {
269                            (server_name, message)
270                        }
271                    }
272                    Err(e) => {
273                        failed_count += 1;
274                        (server_name, format!("Failed - {}", e))
275                    }
276                }
277            })
278            .collect();
279
280        for (server_name, result) in server_stops {
281            results.push(format!("{}: {}", server_name, result));
282        }
283
284        let summary = format!(
285            "Stop all completed: {} stopped, {} failed",
286            stopped_count, failed_count
287        );
288
289        Ok(format!("{}\n\nResults:\n{}", summary, results.join("\n")))
290    }
291
292    // Browser notification (from original)
293    fn notify_browser_shutdown(&self, server_info: &crate::server::types::ServerInfo) {
294        let server_port = server_info.port;
295        let server_name = server_info.name.clone();
296
297        tokio::spawn(async move {
298            let server_url = format!("http://127.0.0.1:{}", server_port);
299            log::info!(
300                "Notifying browser to close for server {} (async)",
301                server_name
302            );
303
304            let client = reqwest::Client::new();
305            if let Err(e) = client
306                .get(format!("{}/api/close-browser", server_url))
307                .timeout(std::time::Duration::from_millis(300))
308                .send()
309                .await
310            {
311                log::warn!("Failed to notify browser: {}", e);
312            }
313
314            tokio::time::sleep(std::time::Duration::from_millis(500)).await;
315        });
316    }
317
318    // Graceful shutdown (from original)
319    fn shutdown_server_gracefully(
320        &self,
321        handle: actix_web::dev::ServerHandle,
322        server_id: String,
323        config: &Config,
324    ) {
325        let shutdown_timeout = config.server.shutdown_timeout;
326
327        tokio::spawn(async move {
328            use tokio::time::{timeout, Duration};
329
330            match timeout(Duration::from_secs(shutdown_timeout), handle.stop(true)).await {
331                Ok(_) => log::info!("Server {} stopped gracefully", server_id),
332                Err(_) => {
333                    log::warn!(
334                        "Server {} shutdown timeout ({}s), forcing stop",
335                        server_id,
336                        shutdown_timeout
337                    );
338                    handle.stop(false).await;
339                }
340            }
341        });
342    }
343
344    // Status update helper
345    fn update_server_status(&self, ctx: &ServerContext, server_id: &str, status: ServerStatus) {
346        if let Ok(mut servers) = ctx.servers.write() {
347            if let Some(server) = servers.get_mut(server_id) {
348                server.status = status;
349            }
350        }
351    }
352}