初始化提交

This commit is contained in:
chenxin committed 2026-08-21 16:00:13 +08:00
commit 887c1f449e
963 files changed
+166869

No files matched your search

+221
View File
@@ -0,0 +1,221 @@
//! 魂镜安全沙箱服务入口。
//!
//! 定位见 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<Config>,
pub executor: Arc<Executor>,
pub semaphore: Arc<Semaphore>,
pub warm_pool: Option<Arc<WarmPool>>,
}
#[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<Arc<WarmPool>> = 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(彩色)与日志文件(无颜色)。
/// 文件按日轮转写入 `<log_dir>/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<Arc<WarmPool>>) {
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!("预热池容器已全部回收");
}
}
}
}