rush_sync_server/server/
instance.rs1use 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
19pub 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>, pub(crate) file_watcher: Option<RecommendedWatcher>,
26}
27
28impl ServerInstance {
29 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 pub async fn start(&mut self) -> Result<()> {
51 {
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 self.config.validate()?;
63
64 self.setup_working_directory().await?;
66
67 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 let (shutdown_tx, shutdown_rx) = oneshot::channel();
81 self.shutdown_tx = Some(shutdown_tx);
82
83 let server = HttpServer::new(move || {
85 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 .wrap(Condition::new(config.debug_logs, Logger::default()))
98 .wrap(cors)
100 .wrap(middleware::ServerInfoMiddleware::new(Arc::clone(
102 &server_info,
103 )))
104 .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 .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 .service(
119 Files::new("/", static_dir.clone())
120 .index_file("index.html")
121 .use_last_modified(true),
122 )
123 });
124
125 let server = server.run();
127 let server_handle = server.handle();
128 self.server_handle = Some(server_handle.clone());
129
130 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 {
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 pub async fn stop(&mut self) -> Result<()> {
164 log::info!("🛑 Stopping server {}", self.get_server_id());
165
166 {
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 if let Some(watcher) = self.file_watcher.take() {
178 drop(watcher);
179 }
180
181 if let Some(handle) = self.server_handle.take() {
183 handle.stop(true).await;
184 }
185
186 if let Some(shutdown_tx) = self.shutdown_tx.take() {
188 let _ = shutdown_tx.send(());
189 }
190
191 tokio::time::sleep(Duration::from_millis(100)).await;
193
194 {
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 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 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 self.create_default_files(&static_dir).await?;
230
231 log::debug!("📁 Working directory setup: {}", working_dir.display());
232 Ok(())
233 }
234
235 async fn create_default_files(&self, static_dir: &Path) -> Result<()> {
237 let server_id = self.get_server_id();
238
239 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 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 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 async fn setup_file_watcher(&mut self) -> Result<()> {
430 log::debug!("🔍 File watcher setup (TODO: implement hot-reloading)");
433 Ok(())
434 }
435
436 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 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 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}