rush_sync_server/server/
instance.rs

1// =====================================================
2// FILE: src/server/instance.rs - ACTIX-WEB SERVER INSTANCE
3// =====================================================
4
5use crate::core::prelude::*;
6use crate::server::middleware;
7use crate::server::routes;
8use crate::server::{ServerConfig, ServerInfo, ServerMode, ServerStatus};
9
10use actix_cors::Cors;
11use actix_files::Files;
12use actix_web::middleware::{Condition, Logger};
13use actix_web::{web, App, HttpServer};
14use notify::RecommendedWatcher;
15use std::path::Path;
16use std::sync::{Arc, Mutex};
17use tokio::sync::oneshot;
18
19/// Einzelne Server-Instanz mit Actix-Web
20pub struct ServerInstance {
21    pub info: Arc<Mutex<ServerInfo>>,
22    pub config: ServerConfig,
23    pub(crate) shutdown_tx: Option<oneshot::Sender<()>>,
24    pub(crate) server_handle: Option<actix_web::dev::ServerHandle>, // NEU!
25    pub(crate) file_watcher: Option<RecommendedWatcher>,
26}
27
28impl ServerInstance {
29    /// Erstellt neue Server-Instanz
30    pub fn new(port: u16, mode: ServerMode) -> Self {
31        let info = ServerInfo::new(port, mode);
32        let config = ServerConfig::for_mode(mode, port);
33
34        log::info!(
35            "🚀 Creating server instance: {} on port {}",
36            info.id[..8].to_uppercase(),
37            port
38        );
39
40        Self {
41            info: Arc::new(Mutex::new(info)),
42            config,
43            shutdown_tx: None,
44            server_handle: None,
45            file_watcher: None,
46        }
47    }
48
49    /// Startet den Actix-Web Server
50    pub async fn start(&mut self) -> Result<()> {
51        // Status auf Starting setzen
52        {
53            let mut info = self.info.lock().unwrap_or_else(|poisoned| {
54                log::warn!("Recovered from poisoned mutex");
55                poisoned.into_inner()
56            });
57            info.status = ServerStatus::Starting;
58            info.last_modified = Some(chrono::Utc::now());
59        }
60
61        // Konfiguration validieren
62        self.config.validate()?;
63
64        // Working Directory erstellen
65        self.setup_working_directory().await?;
66
67        // File Watcher für Hot-Reloading (nur Dev-Modus)
68        if self.config.mode == ServerMode::Dev && self.config.hot_reload {
69            self.setup_file_watcher().await?;
70        }
71
72        let bind_addr = self.config.bind_address();
73        let static_dir = self.config.static_dir.clone();
74        let server_info = Arc::clone(&self.info);
75        let config = self.config.clone();
76
77        log::info!("🌐 Starting Actix-Web server on {}", bind_addr);
78
79        // Shutdown-Channel erstellen
80        let (shutdown_tx, shutdown_rx) = oneshot::channel();
81        self.shutdown_tx = Some(shutdown_tx);
82
83        // Actix-Web Server konfigurieren und starten
84        let server = HttpServer::new(move || {
85            // CORS-Objekt vorab bauen (gleicher Typ in beiden Zweigen)
86            let cors = if config.cors_enabled {
87                Cors::permissive()
88                    .allow_any_origin()
89                    .allow_any_method()
90                    .allow_any_header()
91            } else {
92                Cors::default()
93            };
94
95            App::new()
96                // Logger nur wenn aktiviert (kein Typwechsel dank Condition)
97                .wrap(Condition::new(config.debug_logs, Logger::default()))
98                // CORS immer wrappen (cors hat in beiden Fällen den gleichen Typ)
99                .wrap(cors)
100                // Eigene Middleware
101                .wrap(middleware::ServerInfoMiddleware::new(Arc::clone(
102                    &server_info,
103                )))
104                // Basis-Routen
105                .route("/", web::get().to(routes::index))
106                .route("/health", web::get().to(routes::health_check))
107                .route("/api/info", web::get().to(routes::server_info))
108                .route("/api/status", web::get().to(routes::server_status))
109                // Dev-Routen bedingt per configure (verändert App-Typ nicht)
110                .configure(|app_cfg| {
111                    if config.mode == ServerMode::Dev {
112                        app_cfg
113                            .route("/dev/reload", web::post().to(routes::dev_reload))
114                            .route("/dev/logs", web::get().to(routes::dev_logs));
115                    }
116                })
117                // Statische Dateien zuletzt registrieren
118                .service(
119                    Files::new("/", static_dir.clone())
120                        .index_file("index.html")
121                        .use_last_modified(true),
122                )
123        });
124
125        // Server starten und Handle speichern
126        let server = server.run();
127        let server_handle = server.handle();
128        self.server_handle = Some(server_handle.clone());
129
130        // Server Task mit korrektem Shutdown
131        let _server_task = tokio::spawn(async move {
132            tokio::select! {
133                _ = server => {
134                    log::info!("✅ Server stopped");
135                }
136                _ = shutdown_rx => {
137                    log::info!("🛑 Server shutdown requested");
138                    server_handle.stop(true).await;
139                }
140            }
141        });
142
143        // Status auf Running setzen
144        {
145            let mut info = self.info.lock().unwrap_or_else(|poisoned| {
146                log::warn!("Recovered from poisoned mutex");
147                poisoned.into_inner()
148            });
149            info.status = ServerStatus::Running;
150            info.last_modified = Some(chrono::Utc::now());
151        }
152
153        log::info!(
154            "✅ Server {} running on {}",
155            self.get_server_id(),
156            bind_addr
157        );
158
159        Ok(())
160    }
161
162    /// Stoppt den Server
163    pub async fn stop(&mut self) -> Result<()> {
164        log::info!("🛑 Stopping server {}", self.get_server_id());
165
166        // Status auf Stopping setzen
167        {
168            let mut info = self.info.lock().unwrap_or_else(|poisoned| {
169                log::warn!("Recovered from poisoned mutex");
170                poisoned.into_inner()
171            });
172            info.status = ServerStatus::Stopping;
173            info.last_modified = Some(chrono::Utc::now());
174        }
175
176        // File Watcher stoppen
177        if let Some(watcher) = self.file_watcher.take() {
178            drop(watcher);
179        }
180
181        // NEU: Server Handle stoppen
182        if let Some(handle) = self.server_handle.take() {
183            handle.stop(true).await;
184        }
185
186        // Shutdown-Signal senden
187        if let Some(shutdown_tx) = self.shutdown_tx.take() {
188            let _ = shutdown_tx.send(());
189        }
190
191        // Kurz warten für graceful shutdown
192        tokio::time::sleep(Duration::from_millis(100)).await;
193
194        // Status auf Stopped setzen
195        {
196            let mut info = self.info.lock().unwrap_or_else(|poisoned| {
197                log::warn!("Recovered from poisoned mutex");
198                poisoned.into_inner()
199            });
200            info.status = ServerStatus::Stopped;
201            info.last_modified = Some(chrono::Utc::now());
202        }
203
204        log::info!("✅ Server {} stopped", self.get_server_id());
205        Ok(())
206    }
207
208    /// Erstellt Working Directory mit Standard-Dateien
209    async fn setup_working_directory(&self) -> Result<()> {
210        let working_dir = {
211            let info = self.info.lock().unwrap_or_else(|poisoned| {
212                log::warn!("Recovered from poisoned mutex");
213                poisoned.into_inner()
214            });
215            info.working_dir.clone()
216        };
217
218        // Verzeichnis erstellen
219        tokio::fs::create_dir_all(&working_dir)
220            .await
221            .map_err(AppError::Io)?;
222
223        let static_dir = working_dir.join("static");
224        tokio::fs::create_dir_all(&static_dir)
225            .await
226            .map_err(AppError::Io)?;
227
228        // Standard-Dateien erstellen wenn sie nicht existieren
229        self.create_default_files(&static_dir).await?;
230
231        log::debug!("📁 Working directory setup: {}", working_dir.display());
232        Ok(())
233    }
234
235    /// Erstellt Standard HTML/CSS/JS Dateien
236    async fn create_default_files(&self, static_dir: &Path) -> Result<()> {
237        let server_id = self.get_server_id();
238
239        // index.html
240        let index_path = static_dir.join("index.html");
241        if !index_path.exists() {
242            let index_content = format!(
243                r#"<!DOCTYPE html>
244<html lang="en">
245<head>
246    <meta charset="UTF-8">
247    <meta name="viewport" content="width=device-width, initial-scale=1.0">
248    <title>Rush Sync Server - {}</title>
249    <link rel="stylesheet" href="style.css">
250</head>
251<body>
252    <div class="container">
253        <h1>🚀 Rush Sync Server</h1>
254        <p>Server ID: <code>{}</code></p>
255        <p>Mode: <code>{}</code></p>
256        <p>Port: <code>{}</code></p>
257
258        <div class="status">
259            <h2>✅ Server Running</h2>
260            <p>This server was created by Rush Sync Server v{}</p>
261        </div>
262
263        <div class="api-links">
264            <h3>API Endpoints:</h3>
265            <ul>
266                <li><a href="/health">Health Check</a></li>
267                <li><a href="/api/info">Server Info</a></li>
268                <li><a href="/api/status">Server Status</a></li>
269            </ul>
270        </div>
271    </div>
272
273    <script src="script.js"></script>
274</body>
275</html>"#,
276                server_id,
277                server_id,
278                self.config.mode,
279                self.config.port,
280                crate::core::constants::VERSION
281            );
282
283            tokio::fs::write(&index_path, index_content)
284                .await
285                .map_err(AppError::Io)?;
286        }
287
288        // style.css
289        let css_path = static_dir.join("style.css");
290        if !css_path.exists() {
291            let css_content = r#"/* Rush Sync Server - Default Styles */
292body {
293    font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
294    margin: 0;
295    padding: 20px;
296    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
297    color: #333;
298    min-height: 100vh;
299}
300
301.container {
302    max-width: 800px;
303    margin: 0 auto;
304    background: rgba(255, 255, 255, 0.95);
305    padding: 40px;
306    border-radius: 16px;
307    box-shadow: 0 20px 40px rgba(0,0,0,0.1);
308}
309
310h1 {
311    color: #4a5568;
312    text-align: center;
313    margin-bottom: 30px;
314    font-size: 2.5em;
315}
316
317h2 {
318    color: #2d3748;
319    border-bottom: 2px solid #e2e8f0;
320    padding-bottom: 10px;
321}
322
323code {
324    background: #f7fafc;
325    padding: 4px 8px;
326    border-radius: 4px;
327    font-family: 'Monaco', 'Consolas', monospace;
328    color: #e53e3e;
329}
330
331.status {
332    background: linear-gradient(135deg, #48bb78, #38a169);
333    color: white;
334    padding: 20px;
335    border-radius: 8px;
336    margin: 20px 0;
337}
338
339.api-links ul {
340    list-style: none;
341    padding: 0;
342}
343
344.api-links li {
345    margin: 10px 0;
346}
347
348.api-links a {
349    display: inline-block;
350    padding: 8px 16px;
351    background: #4299e1;
352    color: white;
353    text-decoration: none;
354    border-radius: 6px;
355    transition: background 0.3s ease;
356}
357
358.api-links a:hover {
359    background: #3182ce;
360}
361
362@media (max-width: 600px) {
363    body { padding: 10px; }
364    .container { padding: 20px; }
365    h1 { font-size: 2em; }
366}
367"#;
368
369            tokio::fs::write(&css_path, css_content)
370                .await
371                .map_err(AppError::Io)?;
372        }
373
374        // script.js
375        let js_path = static_dir.join("script.js");
376        if !js_path.exists() {
377            let js_content = r#"// Rush Sync Server - Default JavaScript
378
379console.log('🚀 Rush Sync Server loaded!');
380
381// Auto-refresh server status
382async function updateStatus() {
383    try {
384        const response = await fetch('/api/status');
385        const status = await response.json();
386        console.log('Server status:', status);
387    } catch (error) {
388        console.error('Failed to fetch status:', error);
389    }
390}
391
392// Update status every 5 seconds
393setInterval(updateStatus, 5000);
394
395// Initial status check
396updateStatus();
397
398// Dev-Mode: Auto-reload functionality
399if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
400    console.log('🔧 Dev mode detected - enabling auto-reload');
401
402    // Check for updates every 2 seconds in dev mode
403    setInterval(async () => {
404        try {
405            const response = await fetch('/dev/reload', { method: 'POST' });
406            if (response.ok) {
407                const result = await response.json();
408                if (result.should_reload) {
409                    console.log('🔄 Reloading due to file changes...');
410                    window.location.reload();
411                }
412            }
413        } catch (error) {
414            // Ignore errors in dev mode
415        }
416    }, 2000);
417}
418"#;
419
420            tokio::fs::write(&js_path, js_content)
421                .await
422                .map_err(AppError::Io)?;
423        }
424
425        Ok(())
426    }
427
428    /// File Watcher für Hot-Reloading einrichten
429    async fn setup_file_watcher(&mut self) -> Result<()> {
430        // TODO: Implementiere File Watcher mit notify crate
431        // Für jetzt erstmal leer lassen - können wir später ausbauen
432        log::debug!("🔍 File watcher setup (TODO: implement hot-reloading)");
433        Ok(())
434    }
435
436    /// Server-ID (ersten 8 Zeichen)
437    pub fn get_server_id(&self) -> String {
438        let info = self.info.lock().unwrap_or_else(|poisoned| {
439            log::warn!("Recovered from poisoned mutex");
440            poisoned.into_inner()
441        });
442        info.id[..8].to_uppercase().to_string()
443    }
444
445    /// Server-Status abfragen
446    pub fn get_status(&self) -> ServerStatus {
447        let info = self.info.lock().unwrap_or_else(|poisoned| {
448            log::warn!("Recovered from poisoned mutex");
449            poisoned.into_inner()
450        });
451        info.status.clone()
452    }
453
454    /// Debug-Info
455    pub fn debug_info(&self) -> String {
456        let info = self.info.lock().unwrap_or_else(|poisoned| {
457            log::warn!("Recovered from poisoned mutex");
458            poisoned.into_inner()
459        });
460        info.debug_info()
461    }
462}