rush_sync_server/commands/stop/
command.rs1use 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 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 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 self.update_server_status(ctx, &server_info.id, ServerStatus::Stopped);
96
97 self.notify_browser_shutdown(&server_info);
99
100 if let Some(handle) = handle {
101 self.shutdown_server_gracefully(handle, server_info.id.clone(), config);
103
104 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 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 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 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 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 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 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 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 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}