rush_sync_server/commands/stop/
command.rs

1// Fixed src/commands/stop/command.rs - MIT BROWSER CLOSE
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 a web server (persistent)"
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>'".to_string(),
34            ));
35        }
36
37        let config = get_config()?;
38
39        let ctx = crate::server::shared::get_shared_context();
40        self.stop_server(&config, ctx, args[0])
41    }
42
43    fn priority(&self) -> u8 {
44        67
45    }
46}
47
48impl StopCommand {
49    fn stop_server(
50        &self,
51        config: &Config,
52        ctx: &ServerContext,
53        identifier: &str,
54    ) -> Result<String> {
55        // Atomare Operationen für Race-Condition-Schutz
56        let (server_info, handle) = {
57            let servers_guard = ctx
58                .servers
59                .read()
60                .map_err(|_| AppError::Validation("Server-Context lock poisoned".to_string()))?;
61
62            let server_info = find_server(&servers_guard, identifier)?.clone();
63
64            if server_info.status != ServerStatus::Running {
65                return Ok(format!(
66                    "Server '{}' is not active (Status: {})",
67                    server_info.name, server_info.status
68                ));
69            }
70
71            // Handle atomisch entfernen
72            let handle = {
73                let mut handles_guard = ctx.handles.write().map_err(|_| {
74                    AppError::Validation("Handle-Context lock poisoned".to_string())
75                })?;
76                handles_guard.remove(&server_info.id)
77            };
78
79            (server_info, handle)
80        };
81
82        log::info!(
83            "Stopping server {} on port {}",
84            server_info.id,
85            server_info.port
86        );
87
88        // Status sofort auf "Stopping" setzen um Race Conditions zu vermeiden
89        self.update_server_status(ctx, &server_info.id, ServerStatus::Stopped);
90
91        // Browser-Benachrichtigung
92        self.notify_browser_shutdown(&server_info);
93
94        if let Some(handle) = handle {
95            // Graceful shutdown
96            self.shutdown_server_gracefully(handle, server_info.id.clone(), config);
97
98            // Persistence update (nicht blockierend)
99            let server_id = server_info.id.clone();
100            tokio::spawn(async move {
101                crate::server::shared::persist_server_update(&server_id, ServerStatus::Stopped)
102                    .await;
103            });
104
105            // Kurze Pause für konsistente Timing
106            std::thread::sleep(Duration::from_millis(
107                config.server.startup_delay_ms.min(500),
108            ));
109
110            let running_count = {
111                let servers = ctx.servers.read().unwrap_or_else(|e| {
112                    log::warn!("Server lock poisoned: {}", e);
113                    e.into_inner()
114                });
115                servers
116                    .values()
117                    .filter(|s| s.status == ServerStatus::Running)
118                    .count()
119            };
120
121            Ok(format!(
122                "Server '{}' stopped [PERSISTENT] ({}/{} running)",
123                server_info.name, running_count, config.server.max_concurrent
124            ))
125        } else {
126            // Handle war bereits weg - nur Status updaten
127            let server_id = server_info.id.clone();
128            tokio::spawn(async move {
129                crate::server::shared::persist_server_update(&server_id, ServerStatus::Stopped)
130                    .await;
131            });
132
133            Ok(format!(
134                "Server '{}' was already stopped [PERSISTENT]",
135                server_info.name
136            ))
137        }
138    }
139
140    fn notify_browser_shutdown(&self, server_info: &crate::server::types::ServerInfo) {
141        let server_port = server_info.port;
142        let server_name = server_info.name.clone();
143
144        // Keine eigene Runtime: einfach task spawnen
145        tokio::spawn(async move {
146            let server_url = format!("http://127.0.0.1:{}", server_port);
147            log::info!(
148                "Notifying browser to close for server {} (async)",
149                server_name
150            );
151
152            let client = reqwest::Client::new();
153            if let Err(e) = client
154                .get(format!("{}/api/close-browser", server_url)) // <- ohne &
155                .timeout(std::time::Duration::from_millis(300))
156                .send()
157                .await
158            {
159                log::warn!("Failed to notify browser: {}", e);
160            }
161
162            // Mini-Pause, damit Browser reagieren kann
163            tokio::time::sleep(std::time::Duration::from_millis(500)).await;
164        });
165    }
166
167    fn shutdown_server_gracefully(
168        &self,
169        handle: actix_web::dev::ServerHandle,
170        server_id: String,
171        config: &Config,
172    ) {
173        let shutdown_timeout = config.server.shutdown_timeout;
174
175        tokio::spawn(async move {
176            use tokio::time::{timeout, Duration};
177
178            match timeout(Duration::from_secs(shutdown_timeout), handle.stop(true)).await {
179                Ok(_) => log::info!("Server {} stopped gracefully", server_id),
180                Err(_) => {
181                    log::warn!(
182                        "Server {} shutdown timeout ({}s), forcing stop",
183                        server_id,
184                        shutdown_timeout
185                    );
186                    handle.stop(false).await;
187                }
188            }
189        });
190    }
191
192    fn update_server_status(&self, ctx: &ServerContext, server_id: &str, status: ServerStatus) {
193        if let Ok(mut servers) = ctx.servers.write() {
194            if let Some(server) = servers.get_mut(server_id) {
195                server.status = status;
196            }
197        }
198    }
199}