Skip to main content

mold_server/
lib.rs

1pub mod auth;
2pub mod logging;
3pub mod model_cache;
4pub mod model_manager;
5pub mod queue;
6pub mod rate_limit;
7pub mod request_id;
8pub mod routes;
9pub mod state;
10
11#[cfg(test)]
12mod routes_test;
13
14use anyhow::Result;
15use axum::middleware;
16use mold_core::{Config, ModelPaths};
17use std::net::SocketAddr;
18use std::path::PathBuf;
19use tokio::net::TcpListener;
20use tower_http::cors::CorsLayer;
21use tower_http::trace::TraceLayer;
22use tracing::info;
23
24use state::QueueHandle;
25
26pub async fn run_server(bind: &str, port: u16, models_dir: PathBuf) -> Result<()> {
27    Config::install_runtime_models_dir_override(models_dir.clone());
28
29    let mut config = Config::load_or_default();
30    config.models_dir = models_dir.to_string_lossy().into_owned();
31    let model_name = config.resolved_default_model();
32
33    // Create the generation queue channel (bounded, 16 slots).
34    let (job_tx, job_rx) = tokio::sync::mpsc::channel(16);
35    let queue_handle = QueueHandle::new(job_tx);
36
37    let state = match ModelPaths::resolve(&model_name, &config) {
38        Some(paths) => {
39            info!(model = %model_name, "configured model");
40            info!(transformer = %paths.transformer.display());
41            info!(vae = %paths.vae.display());
42            if let Some(t5) = &paths.t5_encoder {
43                info!(t5 = %t5.display());
44            }
45            if let Some(clip) = &paths.clip_encoder {
46                info!(clip = %clip.display());
47            }
48            if let Some(t5_tok) = &paths.t5_tokenizer {
49                info!(t5_tok = %t5_tok.display());
50            }
51            if let Some(clip_tok) = &paths.clip_tokenizer {
52                info!(clip_tok = %clip_tok.display());
53            }
54            if let Some(clip2) = &paths.clip_encoder_2 {
55                info!(clip2 = %clip2.display());
56            }
57            if let Some(clip2_tok) = &paths.clip_tokenizer_2 {
58                info!(clip2_tok = %clip2_tok.display());
59            }
60            for (i, te) in paths.text_encoder_files.iter().enumerate() {
61                info!(text_encoder_shard = i, path = %te.display());
62            }
63            if let Some(text_tok) = &paths.text_tokenizer {
64                info!(text_tok = %text_tok.display());
65            }
66
67            let offload = std::env::var("MOLD_OFFLOAD").is_ok_and(|v| v == "1");
68            // Create shared pool before engine so the first engine can populate it.
69            let shared_pool = std::sync::Arc::new(std::sync::Mutex::new(
70                mold_inference::shared_pool::SharedPool::new(),
71            ));
72            let engine = mold_inference::create_engine_with_pool(
73                model_name,
74                paths,
75                &config,
76                mold_inference::LoadStrategy::Eager,
77                offload,
78                Some(shared_pool.clone()),
79            )?;
80            let mut state = state::AppState::new(engine, config, queue_handle);
81            state.shared_pool = shared_pool;
82            state
83        }
84        None => {
85            info!("no default model configured — models will be pulled on first request");
86            state::AppState::empty(config, queue_handle)
87        }
88    };
89
90    // Spawn the generation queue worker — processes jobs sequentially (single GPU).
91    let worker_state = state.clone();
92    tokio::spawn(queue::run_queue_worker(job_rx, worker_state));
93
94    // Ensure output directory exists and pre-generate thumbnails.
95    {
96        let config = state.config.read().await;
97        if config.is_output_disabled() {
98            tracing::warn!(
99                "image output is disabled (output_dir is empty) — \
100                 generated images will not be saved and the TUI gallery will be empty"
101            );
102        } else {
103            let output_dir = config.effective_output_dir();
104            let _ = std::fs::create_dir_all(&output_dir);
105            info!(output_dir = %output_dir.display(), "gallery output directory");
106            routes::spawn_thumbnail_warmup(&config);
107        }
108    }
109
110    // Load optional auth and rate-limit configuration from env vars.
111    let auth_state = auth::load_api_keys()?;
112    let rl_config = rate_limit::load_rate_limit_config()?;
113
114    let cors = build_cors_layer()?;
115
116    // Build the router with middleware layers.
117    // Order (outermost → innermost): CORS → Trace → RequestID → Auth → RateLimit → routes
118    // All inject + enforce layers use .layer() (not .route_layer()) so they run on
119    // ALL requests, including unmatched 404 paths — preventing auth/rate-limit bypass.
120    // Set up graceful shutdown: fires on SIGTERM or POST /api/shutdown.
121    let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
122    *state.shutdown_tx.lock().await = Some(shutdown_tx);
123
124    #[cfg(unix)]
125    {
126        let sigterm_state = state.clone();
127        tokio::spawn(async move {
128            if let Ok(mut sig) =
129                tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
130            {
131                sig.recv().await;
132                tracing::info!("received SIGTERM, initiating graceful shutdown");
133                if let Some(tx) = sigterm_state.shutdown_tx.lock().await.take() {
134                    let _ = tx.send(());
135                }
136            }
137        });
138    }
139
140    let app = routes::create_router(state)
141        .layer(middleware::from_fn(rate_limit::rate_limit_middleware))
142        .layer(middleware::from_fn_with_state(
143            rl_config,
144            rate_limit::inject_rate_limit_state,
145        ))
146        .layer(middleware::from_fn(auth::require_api_key))
147        .layer(middleware::from_fn_with_state(
148            auth_state,
149            auth::inject_auth_state,
150        ))
151        .layer(middleware::from_fn(request_id::request_id_middleware))
152        .layer(TraceLayer::new_for_http())
153        .layer(cors);
154
155    let addr: SocketAddr = format!("{bind}:{port}").parse()?;
156    let version = mold_core::build_info::version_string();
157    info!(%addr, %version, "starting mold server");
158
159    let listener = TcpListener::bind(addr).await?;
160    axum::serve(
161        listener,
162        app.into_make_service_with_connect_info::<SocketAddr>(),
163    )
164    .with_graceful_shutdown(async {
165        let _ = shutdown_rx.await;
166        tracing::info!("shutting down");
167    })
168    .await?;
169
170    Ok(())
171}
172
173fn build_cors_layer() -> Result<CorsLayer> {
174    let cors = match std::env::var("MOLD_CORS_ORIGIN") {
175        Ok(origin) if !origin.is_empty() => {
176            let origin = origin
177                .parse::<axum::http::HeaderValue>()
178                .map_err(|_| anyhow::anyhow!("invalid MOLD_CORS_ORIGIN value: {origin}"))?;
179            CorsLayer::new()
180                .allow_origin(origin)
181                .allow_methods([
182                    axum::http::Method::GET,
183                    axum::http::Method::POST,
184                    axum::http::Method::DELETE,
185                ])
186                .allow_headers(tower_http::cors::Any)
187                .expose_headers([
188                    axum::http::header::HeaderName::from_static("x-mold-seed-used"),
189                    axum::http::header::HeaderName::from_static("x-request-id"),
190                    axum::http::header::HeaderName::from_static("retry-after"),
191                ])
192        }
193        _ => CorsLayer::permissive(),
194    };
195    Ok(cors)
196}
197
198#[cfg(test)]
199mod tests {
200    use super::build_cors_layer;
201    use std::sync::Mutex;
202
203    static ENV_LOCK: Mutex<()> = Mutex::new(());
204
205    #[test]
206    fn invalid_cors_origin_returns_error() {
207        let _lock = ENV_LOCK.lock().unwrap();
208        std::env::set_var("MOLD_CORS_ORIGIN", "\nnot-a-header");
209        let result = build_cors_layer();
210        std::env::remove_var("MOLD_CORS_ORIGIN");
211        assert!(result.is_err());
212    }
213
214    #[test]
215    fn valid_cors_origin_builds_layer() {
216        let _lock = ENV_LOCK.lock().unwrap();
217        std::env::set_var("MOLD_CORS_ORIGIN", "https://example.com");
218        let result = build_cors_layer();
219        std::env::remove_var("MOLD_CORS_ORIGIN");
220        assert!(result.is_ok());
221    }
222}