Skip to main content

sz_rust_cli/cmd/
serve.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2024-2026 SZ-Rust Team
3//
4
5//! `serve` 命令 — 启动 HTTP 服务(可选加载 admin 插件)
6//!
7//! 对齐 PHP `php think run`,封装 `sz_rust_core::server::serve_with_graceful_shutdown`。
8//!
9//! ## 用法
10//!
11//! ```bash
12//! # 基础服务(不加载 admin 插件)
13//! sz-rust serve
14//!
15//! # 加载 admin 插件
16//! sz-rust serve --with-admin --addr 0.0.0.0:8080
17//!
18//! # 生产级配置
19//! sz-rust serve --with-admin --workers 4 --grace-timeout 30 --health --access-log
20//! ```
21
22use std::path::PathBuf;
23use std::sync::Arc;
24
25use axum::Router;
26use sz_rust_addons_admin::AdminAddonPlugin;
27use sz_rust_addons_loader::capability_hook::CapabilityHook;
28use sz_rust_capability::CapabilityRegistry;
29use sz_rust_core::config::AppConfig;
30use sz_rust_core::container::App;
31use sz_rust_orm_facade::{Connection, ConnectionFactory, DbError, Pool, PoolConfig};
32
33use crate::error::CliError;
34
35mod access_log;
36mod runtime;
37mod signal;
38mod watcher;
39
40pub use runtime::{build_runtime, resolve_workers, validate_workers};
41
42/// serve 命令参数集合
43///
44/// 由 clap `Command::Serve` 变体字段映射构造,用于解耦 CLI 解析与业务逻辑。
45#[derive(Debug, Clone)]
46pub struct ServeArgs {
47    /// 启用 admin 插件(加载 /api/admin/* 路由 + Capability 注册)
48    pub with_admin: bool,
49    /// 启用 tenant_middleware(从 X-Tenant-Id Header 提取租户 ID 并设置 TenantContext)
50    pub with_tenant: bool,
51    /// 启用 data_scope_middleware(从请求 extensions 提取 DataScopeUserContext 并注入 DataScopeContext)
52    pub with_data_scope: bool,
53    /// 监听地址(默认 0.0.0.0:8080)
54    pub addr: String,
55    /// 启用配置热重载(监听 config/ 目录文件变更)
56    pub watch_config: bool,
57    /// worker 线程数(None 时用配置文件值或 CPU 核心数)
58    pub workers: Option<u16>,
59    /// 优雅关闭超时秒数(None 时用配置文件值或默认 30)
60    pub grace_timeout: Option<u16>,
61    /// TLS 证书文件路径
62    pub tls_cert: Option<PathBuf>,
63    /// TLS 私钥文件路径
64    pub tls_key: Option<PathBuf>,
65    /// 启用访问日志中间件
66    pub access_log: bool,
67    /// 启用健康检查端点(默认 true)
68    pub health: bool,
69}
70
71impl ServeArgs {
72    /// 校验参数合法性
73    pub fn validate(&self) -> Result<(), CliError> {
74        if let Some(w) = self.workers {
75            if w == 0 {
76                return Err(CliError::Generic("worker 数量必须 >= 1".to_string()));
77            }
78            if w > 1024 {
79                return Err(CliError::Generic("worker 数量超过上限 1024".to_string()));
80            }
81        }
82        if let Some(t) = self.grace_timeout {
83            if t > 300 {
84                return Err(CliError::Generic("优雅关闭超时超过上限 300 秒".to_string()));
85            }
86        }
87        if self.tls_cert.is_some() != self.tls_key.is_some() {
88            return Err(CliError::Generic(
89                "--tls-cert 和 --tls-key 必须同时提供或同时缺失".to_string(),
90            ));
91        }
92        Ok(())
93    }
94}
95
96/// AnyPool 连接工厂包装器
97///
98/// 将 `sz_orm_sqlx::any_driver::AnyPool` 适配为 `sz_orm_core::ConnectionFactory`,
99/// 使其可用于创建 `sz_orm_core::Pool`。
100struct AnyPoolConnectionFactory(sz_orm_sqlx::any_driver::AnyPool);
101
102#[async_trait::async_trait]
103impl ConnectionFactory for AnyPoolConnectionFactory {
104    async fn create(&self) -> Result<Box<dyn Connection>, DbError> {
105        let conn = self
106            .0
107            .create()
108            .await
109            .map_err(|e| DbError::ConnectionError(format!("AnyPool create failed: {e}")))?;
110        Ok(Box::new(conn))
111    }
112}
113
114/// 构建 tenant_middleware 路由层
115///
116/// 纯函数:不启动服务、不打印日志、不 panic。
117/// `with_tenant` 为 true 时叠加 `tenant_middleware`(从 X-Tenant-Id Header 提取租户 ID)。
118pub fn build_router_with_tenant(router: Router, with_tenant: bool) -> Router {
119    if with_tenant {
120        router.layer(axum::middleware::from_fn(
121            sz_rust_core::multi_tenant::tenant_middleware,
122        ))
123    } else {
124        router
125    }
126}
127
128/// 构建 data_scope_middleware 路由层
129///
130/// 纯函数:不启动服务、不打印日志、不 panic。
131/// `with_data_scope` 为 true 时叠加 `data_scope_middleware`(注入数据权限上下文)。
132pub fn build_router_with_data_scope(router: Router, with_data_scope: bool) -> Router {
133    if with_data_scope {
134        let state = sz_rust_middleware_facade::data_scope::DataScopeMiddlewareState {
135            field_scope_registry: Arc::new(
136                sz_rust_orm_facade::data_scope::field_scope::registry::FieldScopePolicyRegistry::new(),
137            ),
138        };
139        router.layer(axum::middleware::from_fn_with_state(
140            state,
141            sz_rust_middleware_facade::data_scope::data_scope_middleware,
142        ))
143    } else {
144        router
145    }
146}
147
148/// 构建 admin 插件路由并注册 Capability
149///
150/// 纯函数:不启动服务、不打印日志、不 panic。
151/// 可被单元测试独立调用。
152pub fn build_router_with_admin(pool: Arc<Pool>, admin_roles: Vec<String>) -> (Router, usize) {
153    let plugin = AdminAddonPlugin::new(pool, admin_roles);
154    let admin_router = plugin.router();
155    let base_router = Router::new().route("/", axum::routing::get(|| async { "SZ-Rust" }));
156    let merged_router = base_router.merge(admin_router);
157
158    let hook = plugin.capability_hook();
159    let registry = CapabilityRegistry::new();
160    let registered = hook.register_capabilities(&registry).unwrap_or_default();
161
162    (merged_router, registered.len())
163}
164
165/// 从 AppConfig 构建数据库连接池
166async fn acquire_pool(config: &AppConfig) -> Result<Arc<Pool>, CliError> {
167    let db_name = &config.database.default;
168    let conn_config = config
169        .database
170        .connections
171        .get(db_name)
172        .ok_or_else(|| CliError::Generic(format!("数据库连接 '{db_name}' 未配置")))?;
173
174    let db_url = build_db_url(conn_config);
175    let any_pool = sz_orm_sqlx::any_driver::AnyPool::connect(&db_url)
176        .await
177        .map_err(|e| CliError::Generic(format!("数据库连接失败: {e}")))?;
178
179    let factory: Arc<dyn ConnectionFactory> = Arc::new(AnyPoolConnectionFactory(any_pool));
180    let pool = Pool::new(PoolConfig::default(), factory)
181        .map_err(|e| CliError::Generic(format!("连接池创建失败: {e}")))?;
182    Ok(Arc::new(pool))
183}
184
185/// 从 DatabaseConnection 配置构建数据库 URL
186fn build_db_url(conn: &sz_rust_core::config::DatabaseConnection) -> String {
187    let driver = match conn.r#type.as_str() {
188        "mysql" => "mysql",
189        "postgres" | "pgsql" => "postgres",
190        "sqlite" => "sqlite",
191        other => other,
192    };
193    format!(
194        "{driver}://{}:{}@{}:{}/{}",
195        conn.username, conn.password, conn.hostname, conn.hostport, conn.database
196    )
197}
198
199/// 读取管理员角色列表
200///
201/// 从环境变量 `SZ_RUST_ADMIN_ROLES`(逗号分隔)读取,缺失时回退到默认值。
202fn acquire_admin_roles() -> Vec<String> {
203    std::env::var("SZ_RUST_ADMIN_ROLES")
204        .ok()
205        .and_then(|s| {
206            let roles: Vec<String> = s.split(',').map(|r| r.trim().to_string()).collect();
207            if roles.is_empty() {
208                None
209            } else {
210                Some(roles)
211            }
212        })
213        .unwrap_or_else(|| {
214            tracing::warn!("SZ_RUST_ADMIN_ROLES 未设置,使用默认角色 [super_admin]");
215            vec!["super_admin".to_string()]
216        })
217}
218
219/// 执行 serve 命令(同步入口)
220///
221/// 构建指定 worker 数的 multi-thread runtime,在 runtime 上 block_on 执行 async 逻辑。
222/// 调用方应在 `spawn_blocking` 线程上调用此函数,避免 runtime 嵌套。
223pub fn execute(args: ServeArgs) -> Result<i32, CliError> {
224    args.validate()?;
225
226    let config_dir = std::env::var("SZ_RUST_CONFIG_DIR")
227        .map(std::path::PathBuf::from)
228        .unwrap_or_else(|_| std::path::PathBuf::from("config"));
229
230    let config = {
231        let tmp_rt = tokio::runtime::Builder::new_current_thread()
232            .enable_all()
233            .build()
234            .map_err(|e| CliError::Generic(format!("临时 runtime 构建失败: {e}")))?;
235        tmp_rt.block_on(async {
236            AppConfig::load_from_dir(&config_dir)
237                .await
238                .unwrap_or_else(|e| {
239                    tracing::warn!("加载配置失败(使用默认配置): {e}");
240                    AppConfig::default()
241                })
242        })
243    };
244
245    let workers = resolve_workers(args.workers, config.server.workers);
246    tracing::info!("使用 {workers} 个 worker 线程");
247
248    let runtime = build_runtime(workers)?;
249    runtime.block_on(execute_async(args, config, config_dir))
250}
251
252/// serve 命令的 async 内部逻辑
253async fn execute_async(
254    args: ServeArgs,
255    config: AppConfig,
256    config_dir: std::path::PathBuf,
257) -> Result<i32, CliError> {
258    if args.watch_config {
259        let (reload_tx, reload_rx) = tokio::sync::mpsc::channel::<std::path::PathBuf>(16);
260        match watcher::ConfigWatcher::start(&config_dir, reload_tx) {
261            Ok(_) => {
262                tracing::info!("配置热重载已启用,监听目录: {}", config_dir.display());
263                watcher::spawn_reload_coordinator(reload_rx, config_dir.clone());
264            }
265            Err(e) => {
266                tracing::warn!("配置热重载启动失败,降级为不启用: {e}");
267            }
268        }
269    }
270
271    let (reload_signal_tx, mut reload_signal_rx) = tokio::sync::mpsc::channel::<()>(1);
272    let (loglevel_tx, mut loglevel_rx) = tokio::sync::mpsc::channel::<()>(1);
273    signal::install_runtime_signals(reload_signal_tx, loglevel_tx);
274    let signal_config_dir = config_dir.clone();
275    tokio::spawn(async move {
276        while reload_signal_rx.recv().await.is_some() {
277            match watcher::reload_config(&signal_config_dir).await {
278                Ok(_) => tracing::info!("信号触发配置重载成功(数据库/路由变更需重启生效)"),
279                Err(e) => tracing::error!("信号触发配置重载失败,保留旧配置: {e}"),
280            }
281        }
282    });
283    tokio::spawn(async move {
284        let mut current_level = tracing::Level::INFO;
285        while loglevel_rx.recv().await.is_some() {
286            current_level = signal::log_level_cycle(current_level);
287            tracing::info!("日志级别切换为 {current_level}");
288        }
289    });
290
291    let _app = App::init(config.clone());
292
293    let router = if args.with_admin {
294        let pool = acquire_pool(&config).await?;
295        let admin_roles = acquire_admin_roles();
296        let (router, cap_count) = build_router_with_admin(pool, admin_roles);
297        tracing::info!("Admin 插件已加载:{cap_count} 个 Capability 已注册");
298        router
299    } else {
300        Router::new().route("/", axum::routing::get(|| async { "SZ-Rust" }))
301    };
302
303    let router = if args.health {
304        tracing::info!(
305            "健康检查端点已启用:GET /health/ (liveness) + GET /health/ready (readiness)"
306        );
307        router.merge(sz_rust_core::health::default_health_router())
308    } else {
309        router
310    };
311
312    let router = build_router_with_tenant(router, args.with_tenant);
313    if args.with_tenant {
314        tracing::info!("tenant_middleware 已启用(X-Tenant-Id Header 提取)");
315    }
316
317    let router = build_router_with_data_scope(router, args.with_data_scope);
318    if args.with_data_scope {
319        tracing::info!("data_scope_middleware 已启用(数据权限上下文注入)");
320    }
321
322    let router = if args.access_log {
323        tracing::info!("访问日志中间件已启用");
324        router.layer(axum::middleware::from_fn(access_log::access_log_handler))
325    } else {
326        router
327    };
328
329    let grace_timeout = args.grace_timeout.unwrap_or(config.server.grace_timeout);
330    let timeout = std::time::Duration::from_secs(grace_timeout as u64);
331
332    if let (Some(cert), Some(key)) = (&args.tls_cert, &args.tls_key) {
333        tracing::info!(
334            "HTTPS 服务启动于 {}(TLS 证书: {},优雅关闭超时 {}s)",
335            args.addr,
336            cert.display(),
337            grace_timeout
338        );
339        let serve_tls =
340            sz_rust_core::h2::serve_h2_with_graceful_shutdown(router, &args.addr, cert, key);
341        match tokio::time::timeout(timeout, serve_tls).await {
342            Ok(result) => {
343                result.map_err(|e| CliError::Generic(format!("TLS 服务错误: {e}")))?;
344            }
345            Err(_) => {
346                tracing::warn!("TLS 优雅关闭超时,强制中断剩余连接");
347            }
348        }
349    } else {
350        tracing::info!(
351            "HTTP 服务启动于 {}(优雅关闭超时 {}s)",
352            args.addr,
353            grace_timeout
354        );
355        sz_rust_core::server::serve_with_graceful_shutdown_timeout(router, &args.addr, timeout)
356            .await
357            .map_err(CliError::from)?;
358    }
359    Ok(0)
360}
361#[cfg(test)]
362mod tests {
363    use super::*;
364
365    fn default_args() -> ServeArgs {
366        ServeArgs {
367            with_admin: false,
368            with_tenant: false,
369            with_data_scope: false,
370            addr: "0.0.0.0:8080".to_string(),
371            watch_config: false,
372            workers: None,
373            grace_timeout: None,
374            tls_cert: None,
375            tls_key: None,
376            access_log: false,
377            health: true,
378        }
379    }
380
381    #[test]
382    fn test_validate_ok() {
383        assert!(default_args().validate().is_ok());
384    }
385
386    #[test]
387    fn test_validate_workers_zero() {
388        let mut args = default_args();
389        args.workers = Some(0);
390        assert!(args.validate().is_err());
391    }
392
393    #[test]
394    fn test_validate_workers_too_many() {
395        let mut args = default_args();
396        args.workers = Some(1025);
397        assert!(args.validate().is_err());
398    }
399
400    #[test]
401    fn test_validate_workers_max_ok() {
402        let mut args = default_args();
403        args.workers = Some(1024);
404        assert!(args.validate().is_ok());
405    }
406
407    #[test]
408    fn test_validate_grace_timeout_too_large() {
409        let mut args = default_args();
410        args.grace_timeout = Some(301);
411        assert!(args.validate().is_err());
412    }
413
414    #[test]
415    fn test_validate_grace_timeout_max_ok() {
416        let mut args = default_args();
417        args.grace_timeout = Some(300);
418        assert!(args.validate().is_ok());
419    }
420
421    #[test]
422    fn test_validate_tls_cert_only() {
423        let mut args = default_args();
424        args.tls_cert = Some(PathBuf::from("/tmp/cert.pem"));
425        assert!(args.validate().is_err());
426    }
427
428    #[test]
429    fn test_validate_tls_key_only() {
430        let mut args = default_args();
431        args.tls_key = Some(PathBuf::from("/tmp/key.pem"));
432        assert!(args.validate().is_err());
433    }
434
435    #[test]
436    fn test_validate_tls_both_ok() {
437        let mut args = default_args();
438        args.tls_cert = Some(PathBuf::from("/tmp/cert.pem"));
439        args.tls_key = Some(PathBuf::from("/tmp/key.pem"));
440        assert!(args.validate().is_ok());
441    }
442
443    #[tokio::test]
444    async fn test_build_router_with_tenant_disabled() {
445        use tower::ServiceExt;
446        let router = build_router_with_tenant(
447            Router::new().route("/", axum::routing::get(|| async { "ok" })),
448            false,
449        );
450        // 关闭开关时不得挂载 tenant_middleware:缺 X-Tenant-Id 头也应直通 200
451        // (若层被误挂载,缺头请求会被拒为 400,见 enabled 对照)
452        let resp = router
453            .oneshot(
454                axum::http::Request::builder()
455                    .method("GET")
456                    .uri("/")
457                    .body(axum::body::Body::empty())
458                    .unwrap(),
459            )
460            .await
461            .unwrap();
462        assert_eq!(resp.status(), axum::http::StatusCode::OK);
463    }
464
465    #[tokio::test]
466    async fn test_build_router_with_tenant_enabled() {
467        use tower::ServiceExt;
468        let router = build_router_with_tenant(
469            Router::new().route("/", axum::routing::get(|| async { "ok" })),
470            true,
471        );
472        // 开启后缺 X-Tenant-Id 头应被 tenant_middleware 拒为 400
473        // (对照 tests/serve_tenant_e2e.rs serve_without_tenant_header_returns_400)
474        let resp = router
475            .oneshot(
476                axum::http::Request::builder()
477                    .method("GET")
478                    .uri("/")
479                    .body(axum::body::Body::empty())
480                    .unwrap(),
481            )
482            .await
483            .unwrap();
484        assert_eq!(resp.status(), axum::http::StatusCode::BAD_REQUEST);
485    }
486
487    #[tokio::test]
488    async fn test_build_router_with_data_scope_disabled() {
489        use tower::ServiceExt;
490        let router = build_router_with_data_scope(
491            Router::new().route("/", axum::routing::get(|| async { "ok" })),
492            false,
493        );
494        let resp = router
495            .oneshot(
496                axum::http::Request::builder()
497                    .method("GET")
498                    .uri("/")
499                    .body(axum::body::Body::empty())
500                    .unwrap(),
501            )
502            .await
503            .unwrap();
504        assert_eq!(resp.status(), axum::http::StatusCode::OK);
505    }
506
507    #[tokio::test]
508    async fn test_build_router_with_data_scope_enabled() {
509        use tower::ServiceExt;
510        let router = build_router_with_data_scope(
511            Router::new().route("/", axum::routing::get(|| async { "ok" })),
512            true,
513        );
514        // 开启后携带 DataScopeUserContext 的请求应通过中间件直达处理器
515        // (对照 tests/serve_data_scope_e2e.rs serve_with_data_scope_injects_context_from_user_context)
516        let mut req = axum::http::Request::builder()
517            .method("GET")
518            .uri("/")
519            .body(axum::body::Body::empty())
520            .unwrap();
521        req.extensions_mut().insert(
522            sz_rust_middleware_facade::data_scope::DataScopeUserContext::new(10).with_dept(5),
523        );
524        let resp = router.oneshot(req).await.unwrap();
525        assert_eq!(resp.status(), axum::http::StatusCode::OK);
526    }
527
528    fn make_conn(
529        r#type: &str,
530        hostname: &str,
531        port: u16,
532        database: &str,
533        username: &str,
534        password: &str,
535    ) -> sz_rust_core::config::DatabaseConnection {
536        sz_rust_core::config::DatabaseConnection {
537            r#type: r#type.to_string(),
538            hostname: hostname.to_string(),
539            database: database.to_string(),
540            username: username.to_string(),
541            password: password.to_string(),
542            hostport: port,
543            charset: "utf8mb4".to_string(),
544            prefix: String::new(),
545            deploy: 0,
546            rw_separate: false,
547            fields_strict: true,
548            break_reconnect: true,
549        }
550    }
551
552    #[test]
553    fn test_build_db_url_mysql() {
554        let conn = make_conn("mysql", "localhost", 3306, "testdb", "root", "pass");
555        let url = build_db_url(&conn);
556        assert_eq!(url, "mysql://root:pass@localhost:3306/testdb");
557    }
558
559    #[test]
560    fn test_build_db_url_postgres() {
561        let conn = make_conn("postgres", "localhost", 5432, "testdb", "user", "pass");
562        let url = build_db_url(&conn);
563        assert_eq!(url, "postgres://user:pass@localhost:5432/testdb");
564    }
565
566    #[test]
567    fn test_build_db_url_pgsql_alias() {
568        let conn = make_conn("pgsql", "localhost", 5432, "testdb", "user", "pass");
569        let url = build_db_url(&conn);
570        assert_eq!(url, "postgres://user:pass@localhost:5432/testdb");
571    }
572
573    #[test]
574    fn test_build_db_url_sqlite() {
575        let conn = make_conn("sqlite", "localhost", 0, "test.db", "", "");
576        let url = build_db_url(&conn);
577        assert_eq!(url, "sqlite://:@localhost:0/test.db");
578    }
579
580    #[test]
581    fn test_build_db_url_unknown_driver() {
582        let conn = make_conn("custom_driver", "host", 1234, "db", "u", "p");
583        let url = build_db_url(&conn);
584        assert_eq!(url, "custom_driver://u:p@host:1234/db");
585    }
586
587    #[test]
588    fn test_acquire_admin_roles_default() {
589        let _lock = super::super::test_support::acquire_global_lock();
590        std::env::remove_var("SZ_RUST_ADMIN_ROLES");
591        let roles = acquire_admin_roles();
592        assert_eq!(roles, vec!["super_admin".to_string()]);
593    }
594
595    #[test]
596    fn test_acquire_admin_roles_from_env() {
597        let _lock = super::super::test_support::acquire_global_lock();
598        std::env::set_var("SZ_RUST_ADMIN_ROLES", "admin,super_admin,guest");
599        let roles = acquire_admin_roles();
600        assert_eq!(roles, vec!["admin", "super_admin", "guest"]);
601        std::env::remove_var("SZ_RUST_ADMIN_ROLES");
602    }
603
604    #[test]
605    fn test_acquire_admin_roles_single() {
606        let _lock = super::super::test_support::acquire_global_lock();
607        std::env::set_var("SZ_RUST_ADMIN_ROLES", "only_one");
608        let roles = acquire_admin_roles();
609        assert_eq!(roles, vec!["only_one".to_string()]);
610        std::env::remove_var("SZ_RUST_ADMIN_ROLES");
611    }
612}