Skip to main content

rush_sync_server/commands/stop/
command.rs

1use crate::commands::command::Command;
2use crate::commands::parsing::{parse_bulk_args, BulkMode};
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(get_translation(
33                "server.error.id_missing",
34                &[],
35            )));
36        }
37
38        let config = get_config()?;
39        let ctx = crate::server::shared::get_shared_context();
40
41        match parse_bulk_args(args) {
42            BulkMode::Single(identifier) => self.stop_single_server(&config, ctx, &identifier),
43            BulkMode::Range(start, end) => self.stop_range_servers(&config, ctx, start, end),
44            BulkMode::All => self.stop_all_servers(&config, ctx),
45            BulkMode::Invalid(error) => Err(AppError::Validation(error)),
46        }
47    }
48
49    fn priority(&self) -> u8 {
50        67
51    }
52}
53
54impl StopCommand {
55    // Stop single server
56    fn stop_single_server(
57        &self,
58        config: &Config,
59        ctx: &ServerContext,
60        identifier: &str,
61    ) -> Result<String> {
62        let (server_info, handle) = {
63            let servers_guard = ctx
64                .servers
65                .read()
66                .map_err(|_| AppError::Validation("Server-Context lock poisoned".to_string()))?;
67
68            let server_info = find_server(&servers_guard, identifier)?.clone();
69
70            if server_info.status != ServerStatus::Running {
71                return Ok(format!(
72                    "Server '{}' is not active (Status: {})",
73                    server_info.name, server_info.status
74                ));
75            }
76
77            // Atomically remove the handle
78            let handle = {
79                let mut handles_guard = ctx.handles.write().map_err(|_| {
80                    AppError::Validation("Handle-Context lock poisoned".to_string())
81                })?;
82                handles_guard.remove(&server_info.id)
83            };
84
85            (server_info, handle)
86        };
87
88        log::info!(
89            "Stopping server {} on port {}",
90            server_info.id,
91            server_info.port
92        );
93
94        // Set status to Stopped immediately
95        self.update_server_status(ctx, &server_info.id, ServerStatus::Stopped);
96
97        // Notify browser to close
98        self.notify_browser_shutdown(&server_info);
99
100        if let Some(handle) = handle {
101            // Graceful shutdown
102            self.shutdown_server_gracefully(handle, server_info.id.clone(), config);
103
104            // Persist status update (non-blocking)
105            let server_id = server_info.id.clone();
106            tokio::spawn(async move {
107                crate::server::shared::persist_server_update(&server_id, ServerStatus::Stopped)
108                    .await;
109            });
110
111            // Brief pause for consistent timing
112            std::thread::sleep(Duration::from_millis(
113                config.server.startup_delay_ms.min(500),
114            ));
115
116            let running_count = {
117                let servers = ctx.servers.read().unwrap_or_else(|e| {
118                    log::warn!("Server lock poisoned: {}", e);
119                    e.into_inner()
120                });
121                servers
122                    .values()
123                    .filter(|s| s.status == ServerStatus::Running)
124                    .count()
125            };
126
127            Ok(format!(
128                "Server '{}' stopped [PERSISTENT] ({}/{} running)",
129                server_info.name, running_count, config.server.max_concurrent
130            ))
131        } else {
132            // Handle was already removed - just update status
133            let server_id = server_info.id.clone();
134            tokio::spawn(async move {
135                crate::server::shared::persist_server_update(&server_id, ServerStatus::Stopped)
136                    .await;
137            });
138
139            Ok(format!(
140                "Server '{}' was already stopped [PERSISTENT]",
141                server_info.name
142            ))
143        }
144    }
145
146    // Stop servers by range (e.g., "stop 1-3")
147    fn stop_range_servers(
148        &self,
149        config: &Config,
150        ctx: &ServerContext,
151        start: u32,
152        end: u32,
153    ) -> Result<String> {
154        let mut results = Vec::new();
155        let mut stopped_count = 0;
156        let mut failed_count = 0;
157
158        for i in start..=end {
159            let identifier = format!("{}", i);
160
161            match self.stop_single_server(config, ctx, &identifier) {
162                Ok(message) => {
163                    if message.contains("stopped [PERSISTENT]") {
164                        stopped_count += 1;
165                        results.push(format!("Server {}: Stopped", i));
166                    } else {
167                        results.push(format!("Server {}: {}", i, message));
168                    }
169                }
170                Err(e) => {
171                    failed_count += 1;
172                    results.push(format!("Server {}: Failed - {}", i, e));
173                }
174            }
175        }
176
177        let summary = format!(
178            "Range stop completed: {} stopped, {} failed (Range: {}-{})",
179            stopped_count, failed_count, start, end
180        );
181
182        if results.is_empty() {
183            Ok(summary)
184        } else {
185            Ok(format!("{}\n\nResults:\n{}", summary, results.join("\n")))
186        }
187    }
188
189    // Stop all running servers
190    fn stop_all_servers(&self, config: &Config, ctx: &ServerContext) -> Result<String> {
191        let running_servers: Vec<_> = {
192            let servers = read_lock(&ctx.servers, "servers")?;
193            servers
194                .values()
195                .filter(|s| s.status == ServerStatus::Running)
196                .map(|s| (s.id.clone(), s.name.clone()))
197                .collect()
198        };
199
200        if running_servers.is_empty() {
201            return Ok("No running servers to stop".to_string());
202        }
203
204        if running_servers.len() > 20 {
205            return Err(AppError::Validation(
206                "Too many servers to stop at once (max 20). Use ranges instead.".to_string(),
207            ));
208        }
209
210        let mut results = Vec::new();
211        let mut stopped_count = 0;
212        let mut failed_count = 0;
213
214        // Stop servers in parallel for better performance
215        let server_stops: Vec<_> = running_servers
216            .into_iter()
217            .map(|(server_id, server_name)| {
218                match self.stop_single_server(config, ctx, &server_id) {
219                    Ok(message) => {
220                        if message.contains("stopped [PERSISTENT]") {
221                            stopped_count += 1;
222                            (server_name, "Stopped".to_string())
223                        } else {
224                            (server_name, message)
225                        }
226                    }
227                    Err(e) => {
228                        failed_count += 1;
229                        (server_name, format!("Failed - {}", e))
230                    }
231                }
232            })
233            .collect();
234
235        for (server_name, result) in server_stops {
236            results.push(format!("{}: {}", server_name, result));
237        }
238
239        let summary = format!(
240            "Stop all completed: {} stopped, {} failed",
241            stopped_count, failed_count
242        );
243
244        Ok(format!("{}\n\nResults:\n{}", summary, results.join("\n")))
245    }
246
247    // Browser notification
248    fn notify_browser_shutdown(&self, server_info: &crate::server::types::ServerInfo) {
249        let server_port = server_info.port;
250        let server_name = server_info.name.clone();
251
252        tokio::spawn(async move {
253            let server_url = format!("http://127.0.0.1:{}", server_port);
254            log::info!(
255                "Notifying browser to close for server {} (async)",
256                server_name
257            );
258
259            let client = reqwest::Client::new();
260            if let Err(e) = client
261                .get(format!("{}/api/close-browser", server_url))
262                .timeout(std::time::Duration::from_millis(300))
263                .send()
264                .await
265            {
266                log::warn!("Failed to notify browser: {}", e);
267            }
268
269            tokio::time::sleep(std::time::Duration::from_millis(500)).await;
270        });
271    }
272
273    // Graceful shutdown
274    fn shutdown_server_gracefully(
275        &self,
276        handle: actix_web::dev::ServerHandle,
277        server_id: String,
278        config: &Config,
279    ) {
280        let shutdown_timeout = config.server.shutdown_timeout;
281
282        tokio::spawn(async move {
283            use tokio::time::{timeout, Duration};
284
285            match timeout(Duration::from_secs(shutdown_timeout), handle.stop(true)).await {
286                Ok(_) => log::info!("Server {} stopped gracefully", server_id),
287                Err(_) => {
288                    log::warn!(
289                        "Server {} shutdown timeout ({}s), forcing stop",
290                        server_id,
291                        shutdown_timeout
292                    );
293                    handle.stop(false).await;
294                }
295            }
296        });
297    }
298
299    // Status update helper
300    fn update_server_status(&self, ctx: &ServerContext, server_id: &str, status: ServerStatus) {
301        if let Ok(mut servers) = ctx.servers.write() {
302            if let Some(server) = servers.get_mut(server_id) {
303                server.status = status;
304            }
305        }
306    }
307}