//! 魂镜安全沙箱服务入口。 //! //! 定位见 docs/architecture/service-boundaries.md:Rust 承载的安全与本地可靠能力, //! 隶属于「百工 Tool Runtime」的执行隔离层,通过 HTTP 微服务形态供 Python 后端调用。 //! 跨语言契约与安全边界见 docs/architecture/service-boundaries.md。 //! //! 容器隔离 + 预热池设计见 docs/superpowers/specs/2026-07-05-sandbox-container-warmpool-design.md。 mod api; mod auth; mod codemode; mod config; mod docker_exec; mod error; mod executor; mod fileio; mod metrics; mod rlimits; mod warm_pool; use std::path::Path; use std::sync::Arc; use anyhow::Context; use config::Config; use executor::Executor; use tokio::sync::Semaphore; use tracing_subscriber::{fmt, prelude::*, EnvFilter}; use warm_pool::WarmPool; /// 贯穿 handler 的共享状态。 pub struct AppState { pub config: Arc, pub executor: Arc, pub semaphore: Arc, pub warm_pool: Option>, } #[tokio::main] async fn main() -> anyhow::Result<()> { // 本地 cargo run 时从项目根 .env 注入环境变量;容器内 .dockerignore 已排除 .env, // dotenv_iter() 找不到文件返回 Err,if let Ok 静默跳过,直接用 compose environment 注入的值。 if let Ok(iter) = dotenvy::dotenv_iter() { for item in iter.flatten() { let (k, v) = item; if std::env::var_os(&k).is_none() { std::env::set_var(&k, v); } } } // 先读配置:此时 tracing 尚未初始化,失败经 `?` 返回由 Rust runtime 打到 stderr。 let config = Arc::new(Config::from_env().context("读取沙箱配置失败")?); // 初始化 tracing:stdout(彩色,控制台实时可见)+ 文件(sandbox.log 按日轮转,持久化)。 if let Err(e) = std::fs::create_dir_all(&config.log_dir) { eprintln!("警告:创建日志目录失败 {:?}:{e}(将仅输出到控制台)", config.log_dir); } init_logging(&config.log_dir); tracing::info!( host = %config.host, port = config.port, base = ?config.base_workdir, log_dir = ?config.log_dir, max_timeout_ms = config.max_timeout.as_millis(), max_concurrent = config.max_concurrent, auth_enabled = config.auth_enabled, docker_enabled = config.docker_enabled, "启动魂镜安全沙箱服务", ); tokio::fs::create_dir_all(&config.base_workdir) .await .with_context(|| format!("创建沙箱工作目录失败: {:?}", config.base_workdir))?; // 清理上次异常退出残留的临时工作目录(tempfile::TempDir 命名为 .tmpXXXXXX) cleanup_stale_tempdirs(&config.base_workdir).await; // 初始化预热池(Docker 不可用时降级为 None) let warm_pool: Option> = if config.docker_enabled { // 解析工作目录为绝对路径(Docker bind mount 要求绝对路径) let workdir_abs = config .base_workdir .canonicalize() .unwrap_or_else(|_| config.base_workdir.clone()) .to_str() .unwrap_or("/app/sandbox-work") .to_string(); match WarmPool::try_new(config.warm_pool.clone(), workdir_abs) .await { Some(pool) => { // 后台预热(不阻塞启动) let pool_clone = pool.clone(); tokio::spawn(async move { pool_clone.warmup_all().await; }); // 后台健康检查 let pool_clone = pool.clone(); tokio::spawn(async move { pool_clone.health_check_loop().await; }); // 后台空闲回收 let pool_clone = pool.clone(); tokio::spawn(async move { pool_clone.idle_eviction_loop().await; }); tracing::info!("预热池已初始化,容器模式已启用"); Some(pool) } None => { tracing::warn!("Docker 不可用,容器模式降级为进程模式"); None } } } else { tracing::info!("容器模式未启用(SANDBOX_DOCKER_ENABLED=false),仅使用进程隔离"); None }; let warm_pool_for_shutdown = warm_pool.clone(); let state = Arc::new(AppState { warm_pool, config: config.clone(), executor: Arc::new(Executor::new(config.clone())?), semaphore: Arc::new(Semaphore::new(config.max_concurrent)), }); let app = api::router(state); let addr = format!("{}:{}", config.host, config.port); let listener = tokio::net::TcpListener::bind(&addr) .await .with_context(|| format!("监听 {addr} 失败"))?; tracing::info!("安全沙箱服务监听 {addr}(GET /healthz,POST /v1/exec)"); axum::serve(listener, app) .with_graceful_shutdown(shutdown_signal(warm_pool_for_shutdown)) .await .context("axum 服务退出异常")?; tracing::info!("安全沙箱服务已停止"); Ok(()) } /// 装配 tracing subscriber:同一 EnvFilter 同时扇出到 stdout(彩色)与日志文件(无颜色)。 /// 文件按日轮转写入 `/sandbox.log`(当天无后缀,历史文件追加日期后缀)。 fn init_logging(log_dir: &Path) { let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); let file_writer = tracing_appender::rolling::daily(log_dir, "sandbox.log"); tracing_subscriber::registry() .with(filter) .with(fmt::layer().with_writer(std::io::stdout)) .with(fmt::layer().with_writer(file_writer).with_ansi(false)) .init(); } /// 清理 base 下 tempfile 残留的临时目录(`.tmpXXXXXX`),防御异常退出后的目录泄漏。 async fn cleanup_stale_tempdirs(base: &Path) { let mut entries = match tokio::fs::read_dir(base).await { Ok(e) => e, Err(e) => { tracing::warn!(error = %e, "读取沙箱工作目录失败,跳过残留清理"); return; } }; let mut cleaned = 0u32; while let Ok(Some(entry)) = entries.next_entry().await { let name = entry.file_name(); if name.to_string_lossy().starts_with(".tmp") { let path = entry.path(); match tokio::fs::remove_dir_all(&path).await { Ok(_) => cleaned += 1, Err(e) => tracing::warn!(path = %path.display(), error = %e, "清理残留临时目录失败"), } } } if cleaned > 0 { tracing::info!(cleaned, "已清理上次运行残留的临时工作目录"); } } /// 优雅关闭:捕获 Ctrl-C(开发)与 SIGTERM(容器编排),回收预热池容器。 async fn shutdown_signal(warm_pool: Option>) { let ctrl_c = async { let _ = tokio::signal::ctrl_c().await; }; #[cfg(unix)] let terminate = async { match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) { Ok(mut s) => { s.recv().await; } Err(_) => std::future::pending::<()>().await, } }; #[cfg(not(unix))] let terminate = std::future::pending::<()>(); tokio::select! { _ = ctrl_c => { tracing::info!("收到中断信号(Ctrl-C),开始优雅关闭"); if let Some(pool) = warm_pool { pool.shutdown().await; tracing::info!("预热池容器已全部回收"); } } _ = terminate => { tracing::info!("收到终止信号(SIGTERM),开始优雅关闭"); if let Some(pool) = warm_pool { pool.shutdown().await; tracing::info!("预热池容器已全部回收"); } } } }