1use crate::Router;
2use crate::lifecycle::{ApplicationLifecycle, apply_lifecycle};
3use crate::scheduler::{Scheduler, SchedulerHandle};
4use crate::server::dylib_loader::load_dylib_router;
5use crate::server::hotswap::HotSwapService;
6use crate::server::server_middleware::zstd_static_middleware;
7#[cfg(feature = "orm")]
8use rullst_orm::Orm;
9use std::collections::HashMap;
10use std::net::SocketAddr;
11use std::sync::{Arc, Mutex, RwLock};
12
13#[derive(Debug, thiserror::Error)]
15#[non_exhaustive]
16pub enum ServerError {
17 #[error("server configuration error: {0}")]
19 Configuration(String),
20
21 #[error("database initialization failed: {0}")]
23 Database(String),
24
25 #[error("scheduler lifecycle failed: {0}")]
27 Scheduler(#[from] crate::scheduler::SchedulerError),
28
29 #[error("traffic shield lifecycle failed: {0}")]
31 TrafficShield(#[from] crate::resilience::TrafficShieldError),
32
33 #[error("application lifecycle failed: {0}")]
35 Lifecycle(#[from] crate::lifecycle::ApplicationLifecycleError),
36
37 #[error("invalid server listen address `{host}:{port}`")]
39 InvalidAddress {
40 host: String,
42 port: u16,
44 },
45
46 #[error("hot reload is available only in local development debug builds")]
48 HotReloadDisabled,
49
50 #[error("hot reload failed: {0}")]
52 HotReload(String),
53
54 #[error("hot reload configuration error: {0}")]
56 HotReloadConfiguration(String),
57
58 #[error("server I/O error: {0}")]
60 Io(#[from] std::io::Error),
61}
62
63#[non_exhaustive]
64pub struct Server {
83 pub(crate) router: Router,
84 pub(crate) db_url: Option<String>,
85 pub(crate) scheduler: Option<Scheduler>,
86 pub(crate) hot_reload_lib: Option<String>,
87 pub(crate) shield: Option<crate::resilience::TrafficShield>,
88 pub(crate) limiter: Option<crate::resilience::RateLimiter>,
89 pub(crate) lifecycle: Option<ApplicationLifecycle>,
90 pub(crate) machine_endpoints: Option<crate::security::MachineEndpointPolicy>,
91}
92
93impl Server {
94 pub fn new(router: Router) -> Self {
97 Server {
98 router,
99 db_url: None,
100 scheduler: None,
101 hot_reload_lib: None,
102 shield: None,
103 limiter: None,
104 lifecycle: None,
105 machine_endpoints: None,
106 }
107 }
108
109 pub fn new_hot<S: Into<String>>(lib_path: S) -> Self {
113 Server {
114 router: Router::new(),
115 db_url: None,
116 scheduler: None,
117 hot_reload_lib: Some(lib_path.into()),
118 shield: None,
119 limiter: None,
120 lifecycle: None,
121 machine_endpoints: None,
122 }
123 }
124
125 pub fn with_machine_endpoints(
127 mut self,
128 policy: crate::security::MachineEndpointPolicy,
129 ) -> Self {
130 self.machine_endpoints = Some(policy);
131 self
132 }
133
134 pub fn with_db<S: Into<String>>(mut self, db_url: S) -> Self {
140 self.db_url = Some(db_url.into());
141 self
142 }
143
144 pub fn schedule(mut self, scheduler: Scheduler) -> Self {
164 self.scheduler = Some(scheduler);
165 self
166 }
167
168 pub fn shield(mut self, shield: crate::resilience::TrafficShield) -> Self {
170 self.shield = Some(shield);
171 self
172 }
173
174 pub fn rate_limit(mut self, limiter: crate::resilience::RateLimiter) -> Self {
176 self.limiter = Some(limiter);
177 self
178 }
179
180 pub fn with_lifecycle(mut self, lifecycle: ApplicationLifecycle) -> Self {
188 self.lifecycle = Some(lifecycle);
189 self
190 }
191
192 #[cfg_attr(mutants, mutants::skip)]
194 pub async fn run(self, port: u16) -> Result<(), ServerError> {
195 self.run_with_shutdown(port, shutdown_signal()).await
196 }
197
198 #[cfg_attr(mutants, mutants::skip)]
204 pub async fn run_with_shutdown<F>(self, port: u16, shutdown: F) -> Result<(), ServerError>
205 where
206 F: std::future::Future<Output = ()> + Send + 'static,
207 {
208 let lifecycle = self.lifecycle.clone();
209 let result = self.run_inner(port, shutdown).await;
210 if result.is_err()
211 && let Some(lifecycle) = lifecycle
212 {
213 lifecycle.mark_stopped();
214 }
215 result
216 }
217
218 async fn run_inner<F>(mut self, port: u16, shutdown: F) -> Result<(), ServerError>
219 where
220 F: std::future::Future<Output = ()> + Send + 'static,
221 {
222 let dotenv = Self::load_dotenv_values().await?;
223 #[cfg(feature = "orm")]
224 let _ = crate::artisan::check_and_run_artisan(vec![], vec![]).await;
225 let _ = crate::telemetry::init_telemetry();
226 let app_config = Self::load_config().await?;
227 let environment = resolve_environment(&app_config, &dotenv)?;
228
229 self.init_database(&app_config, &dotenv).await?;
230 let addr = Self::setup_networking(port, app_config.app.port, environment, &dotenv)?;
231 let scheduler_handle = self.start_scheduler()?;
232 let shield_lifecycle = self.start_traffic_shield()?;
233
234 let server_result = if let Some(lib_path) = self.hot_reload_lib.take() {
235 self.run_hot_reload(lib_path, addr, environment, shutdown)
236 .await
237 } else {
238 self.run_static(app_config, addr, environment, shutdown)
239 .await
240 };
241
242 if let Some(shield) = shield_lifecycle {
243 shield.shutdown();
244 }
245 let scheduler_result = match scheduler_handle {
246 Some(handle) => handle.shutdown().await.map_err(ServerError::from),
247 None => Ok(()),
248 };
249
250 match server_result {
251 Err(error) => Err(error),
252 Ok(()) => scheduler_result,
253 }
254 }
255
256 #[cfg_attr(mutants, mutants::skip)]
257 async fn load_dotenv_values() -> Result<HashMap<String, String>, ServerError> {
258 if !std::path::Path::new(".env").exists() {
259 return Ok(HashMap::new());
260 }
261
262 let content = tokio::fs::read_to_string(".env").await?;
263 dotenvy::from_read_iter(content.as_bytes())
264 .map(|entry| entry.map_err(|error| ServerError::Configuration(error.to_string())))
265 .collect()
266 }
267
268 #[cfg_attr(mutants, mutants::skip)]
269 async fn load_config() -> Result<crate::config::RullstConfig, ServerError> {
270 let app_config = if std::path::Path::new("Rullst.toml").exists() {
271 crate::config::RullstConfig::load_from_file("Rullst.toml")
272 .await
273 .map_err(|error| ServerError::Configuration(error.to_string()))?
274 } else {
275 crate::config::RullstConfig::new()
276 };
277
278 app_config
279 .validate()
280 .map_err(|error| ServerError::Configuration(error.to_string()))?;
281 let _ = crate::config::RullstConfig::set_global(app_config.clone());
282 Ok(app_config)
283 }
284
285 #[cfg(feature = "orm")]
286 #[cfg_attr(mutants, mutants::skip)]
287 async fn init_database(
288 &mut self,
289 app_config: &crate::config::RullstConfig,
290 dotenv: &HashMap<String, String>,
291 ) -> Result<(), ServerError> {
292 if rullst_orm::Orm::try_pool().is_ok() {
293 return Ok(());
294 }
295
296 if self.db_url.is_none() {
297 if let Some(env_db_url) = read_optional_environment_variable("DATABASE_URL")? {
298 self.db_url = Some(env_db_url);
299 } else if let Some(dotenv_db_url) = dotenv.get("DATABASE_URL") {
300 self.db_url = Some(dotenv_db_url.clone());
301 } else if let Some(ref url) = app_config.database.url {
302 self.db_url = Some(url.clone());
303 }
304 }
305
306 if let Some(db_url) = &self.db_url {
307 println!("Initializing Orm database pool...");
308 Orm::init(db_url)
309 .await
310 .map_err(|error| ServerError::Database(error.to_string()))?;
311 println!("Database initialized successfully.");
312 }
313
314 Ok(())
315 }
316
317 #[cfg(not(feature = "orm"))]
318 #[cfg_attr(mutants, mutants::skip)]
319 async fn init_database(
320 &mut self,
321 app_config: &crate::config::RullstConfig,
322 dotenv: &HashMap<String, String>,
323 ) -> Result<(), ServerError> {
324 let database_requested = self.db_url.is_some()
325 || app_config.database.url.is_some()
326 || dotenv.contains_key("DATABASE_URL")
327 || read_optional_environment_variable("DATABASE_URL")?.is_some();
328
329 if database_requested {
330 return Err(ServerError::Database(
331 "rullst-core was compiled without the `orm` feature".to_string(),
332 ));
333 }
334
335 Ok(())
336 }
337
338 #[cfg_attr(mutants, mutants::skip)]
339 fn start_scheduler(&mut self) -> Result<Option<SchedulerHandle>, ServerError> {
340 self.scheduler
341 .take()
342 .map(Scheduler::start)
343 .transpose()
344 .map_err(ServerError::from)
345 }
346
347 #[cfg_attr(mutants, mutants::skip)]
348 fn start_traffic_shield(
349 &self,
350 ) -> Result<Option<crate::resilience::TrafficShield>, ServerError> {
351 let Some(shield) = self.shield.clone() else {
352 return Ok(None);
353 };
354 shield.start().map_err(ServerError::from)?;
355 Ok(Some(shield))
356 }
357
358 #[cfg_attr(mutants, mutants::skip)]
359 fn setup_networking(
360 fallback_port: u16,
361 configured_port: Option<u16>,
362 environment: crate::config::Environment,
363 dotenv: &HashMap<String, String>,
364 ) -> Result<SocketAddr, ServerError> {
365 let host_str = read_optional_environment_variable("HOST")?
366 .or(read_optional_environment_variable("RULLST_HOST")?)
367 .or_else(|| dotenv.get("HOST").cloned())
368 .or_else(|| dotenv.get("RULLST_HOST").cloned())
369 .unwrap_or_else(|| {
370 if environment.requires_secure_defaults() {
371 "0.0.0.0".to_string()
372 } else {
373 "127.0.0.1".to_string()
374 }
375 });
376
377 let env_port_value =
378 read_optional_environment_variable("PORT")?.or_else(|| dotenv.get("PORT").cloned());
379 let env_port = match env_port_value {
380 Some(value) => Some(value.parse::<u16>().map_err(|_| {
381 ServerError::Configuration(format!("PORT must be a valid u16, got `{value}`"))
382 })?),
383 None => None,
384 };
385 let port = env_port.or(configured_port).unwrap_or(fallback_port);
386
387 let addr: SocketAddr =
388 format!("{host_str}:{port}")
389 .parse()
390 .map_err(|_| ServerError::InvalidAddress {
391 host: host_str,
392 port,
393 })?;
394
395 if environment.allows_development_tools() && addr.ip().is_unspecified() {
396 eprintln!(
397 "⚠️ Rullst Dev: Self-Healing Console mounted on /_rullst/*\n\
398 Set RULLST_ENV=production to disable before deploying."
399 );
400 }
401
402 Ok(addr)
403 }
404
405 #[cfg_attr(mutants, mutants::skip)]
406 async fn run_hot_reload<F>(
407 self,
408 lib_path: String,
409 addr: SocketAddr,
410 environment: crate::config::Environment,
411 shutdown: F,
412 ) -> Result<(), ServerError>
413 where
414 F: std::future::Future<Output = ()> + Send + 'static,
415 {
416 if !cfg!(debug_assertions) || !environment.allows_development_tools() {
417 return Err(ServerError::HotReloadDisabled);
418 }
419
420 let is_dev = true;
421 let reload_token = resolve_hot_reload_token()?;
422
423 println!(
424 "\x1b[36mRullst: initializing the authenticated development-library reload boundary...\x1b[0m"
425 );
426
427 let (initial_router, library) = match load_dylib_router(&lib_path, is_dev) {
428 Ok(r) => r,
429 Err(e) => {
430 println!(
431 "\x1b[31m❌ Failed to load initial dylib: {}. Make sure the dynamic library was compiled by running 'cargo build --lib'.\x1b[0m",
432 e
433 );
434 return Err(ServerError::HotReload(e.to_string()));
435 }
436 };
437
438 let current_router = Arc::new(RwLock::new(initial_router));
439 let active_libraries = Arc::new(Mutex::new(vec![library]));
440 let (hmr_sender, _receiver) = tokio::sync::broadcast::channel(32);
441
442 let hotswap_service = HotSwapService {
443 current_router: current_router.clone(),
444 active_libraries: active_libraries.clone(),
445 hmr_sender,
446 reload_lock: Arc::new(tokio::sync::Mutex::new(())),
447 reload_token,
448 lib_path: lib_path.clone(),
449 is_dev,
450 shield: self.shield,
451 limiter: self.limiter,
452 lifecycle: self.lifecycle.clone(),
453 };
454
455 println!(
456 "Rullst framework serving on http://{} (authenticated development hot reload)",
457 addr
458 );
459 println!(
460 "🚀 Visit: http://localhost:{} to see the result!",
461 addr.port()
462 );
463
464 let listener = tokio::net::TcpListener::bind(addr).await?;
465 mark_lifecycle_ready(self.lifecycle.as_ref())?;
466 let lifecycle = self.lifecycle.clone();
467 let result = axum::serve(listener, hotswap_service)
468 .with_graceful_shutdown(shutdown_with_lifecycle(shutdown, lifecycle.clone()))
469 .await
470 .map_err(ServerError::from);
471 mark_lifecycle_stopped(lifecycle.as_ref());
472 result
473 }
474
475 #[cfg_attr(mutants, mutants::skip)]
476 async fn run_static<F>(
477 self,
478 app_config: crate::config::RullstConfig,
479 addr: SocketAddr,
480 environment: crate::config::Environment,
481 shutdown: F,
482 ) -> Result<(), ServerError>
483 where
484 F: std::future::Future<Output = ()> + Send + 'static,
485 {
486 let is_dev = environment.allows_development_tools();
487 let mut app = self.router.into_axum();
488 app = super::dev_reload::mount(app, is_dev, std::env::var("RULLST_DEV_GENERATION").ok());
489
490 app = app.layer(axum::middleware::from_fn(
491 |req: axum::extract::Request, next: axum::middleware::Next| async move {
492 let method = req.method().to_string();
493 let path = req.uri().path().to_string();
494 let start = std::time::Instant::now();
495 let res = next.run(req).await;
496 let status = res.status().as_u16();
497 let elapsed = start.elapsed().as_secs_f64() * 1000.0;
498 if !path.starts_with("/_rullst_hmr") {
499 println!(
500 "[HTTP] {} {} -> {} ({:.2} ms)",
501 method, path, status, elapsed
502 );
503 }
504 res
505 },
506 ));
507
508 if std::path::Path::new("static").exists() {
509 app = app
510 .nest_service(
511 "/static",
512 tower_http::services::ServeDir::new("static").precompressed_br(),
513 )
514 .layer(axum::middleware::from_fn(zstd_static_middleware));
515 }
516
517 if is_dev {
518 app = app
519 .route(
520 "/_rullst/explain",
521 axum::routing::get(crate::error_console::handle_explain),
522 )
523 .route(
524 "/_rullst/autofix",
525 axum::routing::post(crate::error_console::handle_autofix),
526 )
527 .layer(axum::middleware::from_fn(
528 crate::error_console::catch_panic_middleware,
529 ));
530 }
531
532 if let Some(limiter) = self.limiter {
533 app = app.layer(axum::middleware::from_fn(move |req, next| {
534 crate::resilience::rate_limit_middleware(limiter.clone(), req, next)
535 }));
536 }
537
538 if let Some(shield) = self.shield {
539 app = app.layer(axum::middleware::from_fn(move |req, next| {
540 crate::resilience::backpressure_middleware(shield.clone(), req, next)
541 }));
542 }
543
544 if let Some(lifecycle) = self.lifecycle.clone() {
545 app = apply_lifecycle(app, lifecycle);
546 }
547
548 app = if let Some(policy) = self.machine_endpoints {
549 crate::security::apply_security_baseline_with_machine_endpoints(
550 app,
551 app_config.security,
552 environment,
553 policy,
554 )
555 } else {
556 crate::security::apply_security_baseline(app, app_config.security, environment)
557 }
558 .map_err(|error| ServerError::Configuration(error.to_string()))?;
559
560 println!("Rullst framework serving on http://{}", addr);
561 println!(
562 "🚀 Visit: http://localhost:{} to see the result!",
563 addr.port()
564 );
565
566 let listener = tokio::net::TcpListener::bind(addr).await?;
567 mark_lifecycle_ready(self.lifecycle.as_ref())?;
568 let lifecycle = self.lifecycle.clone();
569 let result = axum::serve(
570 listener,
571 app.into_make_service_with_connect_info::<std::net::SocketAddr>(),
572 )
573 .with_graceful_shutdown(shutdown_with_lifecycle(shutdown, lifecycle.clone()))
574 .await
575 .map_err(ServerError::from);
576 mark_lifecycle_stopped(lifecycle.as_ref());
577 result
578 }
579}
580
581fn read_optional_environment_variable(name: &str) -> Result<Option<String>, ServerError> {
582 match std::env::var(name) {
583 Ok(value) => Ok(Some(value)),
584 Err(std::env::VarError::NotPresent) => Ok(None),
585 Err(std::env::VarError::NotUnicode(_)) => Err(ServerError::Configuration(format!(
586 "{name} is not valid Unicode"
587 ))),
588 }
589}
590
591fn resolve_hot_reload_token() -> Result<Arc<str>, ServerError> {
592 let token = read_optional_environment_variable("RULLST_HMR_TOKEN")?.ok_or_else(|| {
593 ServerError::HotReloadConfiguration(
594 "RULLST_HMR_TOKEN is missing; start hot reload through `cargo rullst dev` or `cargo rullst dash`"
595 .to_string(),
596 )
597 })?;
598 if token.len() != 64 || !token.bytes().all(|byte| byte.is_ascii_hexdigit()) {
599 return Err(ServerError::HotReloadConfiguration(
600 "RULLST_HMR_TOKEN must contain exactly 64 hexadecimal characters".to_string(),
601 ));
602 }
603 Ok(Arc::from(token))
604}
605
606fn resolve_environment(
607 config: &crate::config::RullstConfig,
608 dotenv: &HashMap<String, String>,
609) -> Result<crate::config::Environment, ServerError> {
610 let rullst_env = read_optional_environment_variable("RULLST_ENV")?;
611 let app_env = read_optional_environment_variable("APP_ENV")?;
612 let fallback = dotenv
613 .get("RULLST_ENV")
614 .or_else(|| dotenv.get("APP_ENV"))
615 .map(String::as_str)
616 .or(config.app.env.as_deref());
617
618 crate::config::Environment::resolve(rullst_env.as_deref(), app_env.as_deref(), fallback)
619 .map_err(|error| ServerError::Configuration(error.to_string()))
620}
621
622#[cfg_attr(mutants, mutants::skip)]
624pub async fn shutdown_signal() {
625 let ctrl_c = async {
626 let _ = tokio::signal::ctrl_c().await;
627 };
628
629 #[cfg(unix)]
630 let terminate = async {
631 if let Ok(mut stream) =
632 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
633 {
634 stream.recv().await;
635 } else {
636 std::future::pending::<()>().await;
637 }
638 };
639
640 #[cfg(not(unix))]
641 let terminate = std::future::pending::<()>();
642
643 tokio::select! {
644 _ = ctrl_c => {
645 println!("\n🛑 [Rullst Shutdown] Received SIGINT (Ctrl+C). Draining in-flight requests...");
646 },
647 _ = terminate => {
648 println!("\n🛑 [Rullst Shutdown] Received SIGTERM. Draining in-flight requests...");
649 },
650 }
651}
652
653async fn shutdown_with_lifecycle<F>(shutdown: F, lifecycle: Option<ApplicationLifecycle>)
654where
655 F: std::future::Future<Output = ()>,
656{
657 shutdown.await;
658 if let Some(lifecycle) = lifecycle {
659 let _ = lifecycle.begin_draining();
660 }
661}
662
663fn mark_lifecycle_ready(lifecycle: Option<&ApplicationLifecycle>) -> Result<(), ServerError> {
664 match lifecycle {
665 Some(lifecycle) => lifecycle.mark_ready().map_err(ServerError::from),
666 None => Ok(()),
667 }
668}
669
670fn mark_lifecycle_stopped(lifecycle: Option<&ApplicationLifecycle>) {
671 if let Some(lifecycle) = lifecycle {
672 lifecycle.mark_stopped();
673 }
674}
675
676#[cfg(test)]
677#[path = "builder_tests.rs"]
678mod tests;