768 lines
32 KiB
Rust
768 lines
32 KiB
Rust
use arc_swap::ArcSwap;
|
||
use clap::Parser;
|
||
use std::sync::Arc;
|
||
use std::sync::atomic::{AtomicBool, Ordering};
|
||
use std::time::Duration;
|
||
use tracing::{error, info, warn};
|
||
|
||
use hyper::server::conn::http1;
|
||
use hyper_util::rt::TokioIo;
|
||
use hyper_util::service::TowerToHyperService;
|
||
|
||
// 🆕 使用共享的遥测模块
|
||
use rcoder_telemetry::{TelemetryConfig, TelemetryGuard};
|
||
|
||
mod config;
|
||
mod config_watcher;
|
||
mod handler;
|
||
|
||
mod cleanup_task;
|
||
mod middleware;
|
||
mod router;
|
||
mod service;
|
||
mod utils;
|
||
|
||
use rcoder::*;
|
||
|
||
use config::{CliArgs, load_config_with_args};
|
||
use rcoder_proxy::{PingoraServerManager, ProxyConfig};
|
||
use router::AppState;
|
||
use service::{
|
||
ContainerStatusCheckerConfig, ContainerSyncConfig, VncSyncConfig,
|
||
start_container_status_checker, start_container_sync_task, start_vnc_sync_task,
|
||
};
|
||
|
||
// 导入统一的容器停止模块
|
||
use docker_manager::container_stop;
|
||
use docker_manager::runtime_selection::RuntimeType;
|
||
|
||
// 路由创建函数已移动到 handler 模块
|
||
|
||
#[tokio::main]
|
||
async fn main() -> anyhow::Result<()> {
|
||
// ✅ 初始化 Rustls CryptoProvider(必须在最前面,在任何可能使用 TLS 的代码之前)
|
||
rustls::crypto::ring::default_provider()
|
||
.install_default()
|
||
.expect("Failed to install rustls crypto provider");
|
||
|
||
// 解析命令行参数(移到最前面,以便尽早加载配置)
|
||
let cli_args = CliArgs::parse();
|
||
|
||
// 加载配置(包含命令行参数)
|
||
let config = load_config_with_args(cli_args)?;
|
||
|
||
// 🆕 Initializing telemetry system(使用 rcoder-telemetry,包含控制台 + 文件日志)
|
||
// 使用配置文件中的日志保留天数,与容器日志清理保持一致
|
||
let file_log_config = rcoder_telemetry::FileLogConfig::new("logs", "rcoder")
|
||
.with_max_files(config.cleanup_config.log_cleanup.log_retention_days as usize);
|
||
|
||
let telemetry_config =
|
||
TelemetryConfig::from_env("rcoder").with_file_log_config(file_log_config);
|
||
let telemetry: TelemetryGuard = rcoder_telemetry::init(telemetry_config).await?;
|
||
let telemetry = Arc::new(telemetry);
|
||
|
||
info!("Starting rcoder - AI-powered development platform");
|
||
info!(
|
||
"📋 Log config: keeping log files for {} days",
|
||
config.cleanup_config.log_cleanup.log_retention_days
|
||
);
|
||
|
||
// 创建项目工作目录
|
||
tokio::fs::create_dir_all(&config.projects_dir).await?;
|
||
info!("Projects directory: {:?}", config.projects_dir);
|
||
|
||
// 🔄 初始化宿主机路径解析器(自动检测模式)
|
||
// K8s 模式下不需要 Docker socket,跳过路径解析器初始化
|
||
info!("starting to detect mount path...");
|
||
let runtime_type = docker_manager::runtime_selection::RuntimeType::from_env();
|
||
|
||
if runtime_type == docker_manager::runtime_selection::RuntimeType::Kubernetes {
|
||
info!("[K8S] Kubernetes runtime mode, skipping Docker socket path resolver");
|
||
} else {
|
||
let docker_socket_path = std::env::var("DOCKER_SOCKET_PATH").unwrap_or_else(|_| {
|
||
info!("DOCKER_SOCKET_PATH not set, using default: /var/run/docker.sock");
|
||
"/var/run/docker.sock".to_string()
|
||
});
|
||
|
||
info!("Docker socket: {}", docker_socket_path);
|
||
|
||
let _path_resolver =
|
||
match utils::HostPathResolver::new_with_docker_socket(Some(docker_socket_path.clone()))
|
||
.await
|
||
{
|
||
Ok(resolver) => {
|
||
info!("path resolver initialized successfully");
|
||
info!(
|
||
" Container workspace: {:?}",
|
||
resolver.container_workspace_base()
|
||
);
|
||
info!(
|
||
"work directory: {:?}",
|
||
resolver.host_workspace_base()
|
||
);
|
||
Some(resolver)
|
||
}
|
||
Err(e) => {
|
||
error!("path resolver initialization failed: {}", e);
|
||
error!("please check config:");
|
||
error!("1. Docker socket path: {}", docker_socket_path);
|
||
error!("2. Docker socket already mounted in container");
|
||
error!("3. container has Docker API access");
|
||
error!("4. project work directory mounted");
|
||
|
||
// 显示详细的错误信息和解决建议
|
||
show_docker_configuration_help(&docker_socket_path);
|
||
|
||
// 返回错误,停止启动
|
||
return Err(anyhow::anyhow!("Container self-check failed, unable to initialize path resolver"));
|
||
}
|
||
};
|
||
}
|
||
|
||
// 🧹 启动时清理上次可能遗留的容器
|
||
// 先使用正确配置初始化全局 DockerManager
|
||
info!("initialize Docker Manager (with config)...");
|
||
|
||
// 从应用配置创建 DockerManagerConfig
|
||
let docker_manager_config = if let Some(docker_config) = &config.docker_config {
|
||
info!("using Docker config, merging config");
|
||
let mut default_config = docker_manager::DockerManagerConfig::default();
|
||
|
||
// 合并应用配置中的多镜像配置
|
||
let app_multi_config = docker_config.get_multi_image_config();
|
||
default_config.multi_image_config = app_multi_config;
|
||
|
||
// 应用其他配置
|
||
default_config.auto_cleanup = docker_config
|
||
.auto_cleanup
|
||
.unwrap_or(default_config.auto_cleanup);
|
||
if let Some(ttl) = docker_config.container_ttl_seconds {
|
||
default_config.container_ttl_seconds = Some(ttl);
|
||
}
|
||
|
||
// 应用网络基础名称配置
|
||
info!(
|
||
"🔍 [DEBUG] docker_config.network_base_name = {:?}",
|
||
docker_config.network_base_name
|
||
);
|
||
if let Some(ref network_base_name) = docker_config.network_base_name {
|
||
info!("using config: {}", network_base_name);
|
||
default_config.network_base_name = network_base_name.clone();
|
||
} else {
|
||
info!(
|
||
"⚠️ No network_base_name in config, using default: {}",
|
||
default_config.network_base_name
|
||
);
|
||
}
|
||
|
||
// 🔧 应用超时配置
|
||
if let Some(timeout) = docker_config.api_timeout_seconds {
|
||
default_config.api_timeout_seconds = timeout;
|
||
info!("using config: API timeout: {} seconds", timeout);
|
||
}
|
||
if let Some(timeout) = docker_config.api_timeout_quick_seconds {
|
||
default_config.api_timeout_quick_seconds = timeout;
|
||
info!("using config: timeout: {} seconds", timeout);
|
||
}
|
||
|
||
// 🔧 应用缓存 TTL 配置
|
||
if let Some(ttl) = docker_config.cache_status_ttl_seconds {
|
||
default_config.cache_status_ttl_seconds = ttl;
|
||
info!(
|
||
"using config: status cache TTL: {} seconds",
|
||
ttl
|
||
);
|
||
}
|
||
if let Some(ttl) = docker_config.cache_network_ttl_seconds {
|
||
default_config.cache_network_ttl_seconds = ttl;
|
||
info!("using config: network cache TTL: {} seconds", ttl);
|
||
}
|
||
if let Some(capacity) = docker_config.cache_max_capacity {
|
||
default_config.cache_max_capacity = capacity as u64;
|
||
info!("using config: cache max capacity: {}", capacity);
|
||
}
|
||
|
||
default_config
|
||
} else {
|
||
info!("⚠️ no Docker config, using default config");
|
||
docker_manager::DockerManagerConfig::default()
|
||
};
|
||
|
||
// 使用自定义配置初始化全局 DockerManager
|
||
if let Err(e) =
|
||
docker_manager::global::init_global_docker_manager_with_config(docker_manager_config).await
|
||
{
|
||
error!("Docker Manager initializefailed: {}", e);
|
||
return Err(anyhow::anyhow!("Docker Manager initialization failed: {}", e));
|
||
}
|
||
|
||
info!("checking cleanup for container (enabled)...");
|
||
if config.cleanup_config.enabled {
|
||
match docker_manager::runtime::RuntimeManager::runtime_type() {
|
||
RuntimeType::Docker => {
|
||
let docker_manager = match docker_manager::global::get_global_docker_manager().await {
|
||
Ok(dm) => {
|
||
info!("Docker Manager initialized successfully (with config)");
|
||
dm
|
||
}
|
||
Err(e) => {
|
||
error!("get Docker Manager failed: {}", e);
|
||
return Err(anyhow::anyhow!("Failed to get Docker Manager: {}", e));
|
||
}
|
||
};
|
||
|
||
let multi_image_config = if let Some(docker_config) = &config.docker_config {
|
||
docker_config.get_multi_image_config()
|
||
} else {
|
||
shared_types::create_default_multi_image_config()
|
||
};
|
||
|
||
match container_stop::startup_cleanup_all_enabled_services(
|
||
&docker_manager,
|
||
&multi_image_config,
|
||
)
|
||
.await
|
||
{
|
||
Ok(result) => {
|
||
let enabled_services = shared_types::get_enabled_service_types(&multi_image_config);
|
||
if result.successfully_removed > 0 {
|
||
info!(
|
||
"✅ Startup cleanup completed, removed {} leftover containers (covering {} service types)",
|
||
result.successfully_removed,
|
||
enabled_services.len()
|
||
);
|
||
} else {
|
||
info!("no containers to cleanup");
|
||
}
|
||
|
||
if result.failed_removals > 0 {
|
||
warn!(
|
||
"container cleanup failed: failed count={}",
|
||
result.failed_removals
|
||
);
|
||
for failure in &result.failed_removals_details {
|
||
warn!(
|
||
" - Container {} ({}): {}",
|
||
failure.container_id, failure.container_name, failure.error_message
|
||
);
|
||
}
|
||
}
|
||
}
|
||
Err(e) => {
|
||
warn!("container cleanup failed: {}, cleanup skipped", e);
|
||
}
|
||
}
|
||
}
|
||
RuntimeType::Kubernetes => {
|
||
match docker_manager::runtime::RuntimeManager::get().await {
|
||
Ok(runtime) => {
|
||
if let Err(e) = runtime.cleanup_all().await {
|
||
warn!("k8s startup cleanup failed: {}", e);
|
||
} else {
|
||
info!("k8s startup cleanup completed");
|
||
}
|
||
}
|
||
Err(e) => warn!("failed to get runtime for k8s startup cleanup: {}", e),
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
info!("Container cleanup task already started (cleanup_config.enabled=false)");
|
||
}
|
||
|
||
// 从配置文件读取清理配置
|
||
let cleanup_config = cleanup_task::CleanupConfig {
|
||
idle_timeout: Duration::from_secs(config.cleanup_config.idle_timeout_seconds),
|
||
cleanup_interval: Duration::from_secs(config.cleanup_config.cleanup_interval_seconds),
|
||
docker_stop_timeout: Duration::from_secs(config.cleanup_config.docker_stop_timeout_seconds),
|
||
container_protection_duration: Duration::from_secs(
|
||
config.cleanup_config.container_protection_seconds,
|
||
),
|
||
active_window: Duration::from_secs(5 * 60),
|
||
log_dir: config.cleanup_config.log_cleanup.log_dir.clone(),
|
||
log_retention_duration: Duration::from_secs(
|
||
config.cleanup_config.log_cleanup.log_retention_days * 24 * 60 * 60,
|
||
),
|
||
};
|
||
info!(
|
||
"🧹 Cleanup config: idle_timeout={}s, cleanup_interval={}s, docker_stop_timeout={}s, container_protection={}s, log_dir={}, log_retention={}days",
|
||
config.cleanup_config.idle_timeout_seconds,
|
||
config.cleanup_config.cleanup_interval_seconds,
|
||
config.cleanup_config.docker_stop_timeout_seconds,
|
||
config.cleanup_config.container_protection_seconds,
|
||
config.cleanup_config.log_cleanup.log_dir,
|
||
config.cleanup_config.log_cleanup.log_retention_days
|
||
);
|
||
|
||
// proxy_manager 不需要直接访问 app_state,通过参数传递即可
|
||
|
||
// 🆕 创建 API Key 配置的共享引用(用于热更新)
|
||
// 使用 ArcSwap 实现无锁读取,提升并发性能
|
||
let api_key_config = Arc::new(ArcSwap::from_pointee(config.api_key_auth.clone()));
|
||
|
||
// 启动代理服务(如果启用)
|
||
let (proxy_handle, pingora_service_opt, _pingora_shutdown_tx) = if let Some(proxy_config) =
|
||
&config.proxy_config
|
||
{
|
||
info!(
|
||
"Starting Pingora reverse proxy service, listening on port: {}",
|
||
proxy_config.listen_port
|
||
);
|
||
info!(
|
||
"Proxy route format: /proxy/{{port}}{{/path}} - e.g.: /proxy/{}/health",
|
||
config.port
|
||
);
|
||
|
||
// 添加调试日志
|
||
info!("🔧 [Pingora] startinginitialize Pingora config...");
|
||
info!("🔧 [Pingora] listenport: {}", proxy_config.listen_port);
|
||
info!(
|
||
"🔧 [Pingora] Default backend port: {}",
|
||
proxy_config.default_backend_port
|
||
);
|
||
info!("🔧 [Pingora] backend host: {}", proxy_config.backend_host);
|
||
|
||
let pingora_config = ProxyConfig {
|
||
listen_port: proxy_config.listen_port,
|
||
default_backend_port: proxy_config.default_backend_port,
|
||
backend_host: proxy_config.backend_host.clone(),
|
||
port_param: proxy_config.port_param.clone(),
|
||
config_file: None,
|
||
verbose: false,
|
||
};
|
||
|
||
info!("[Pingora] Pingora configcreatedsucceeded");
|
||
|
||
// 创建 Pingora 服务器管理器,并提取服务引用用于指标读取
|
||
info!("🔧 [Pingora] created PingoraServerManager...");
|
||
let mut server_manager = PingoraServerManager::new(pingora_config)
|
||
.with_api_key_config(Arc::clone(&api_key_config)); // 🆕 传递 API Key 配置
|
||
let pingora_service = server_manager.service();
|
||
info!("[Pingora] PingoraServerManager createdsucceeded");
|
||
info!("[Pingora] API Key config already loaded (no updates)");
|
||
|
||
// 启动健康检查循环(按配置)
|
||
if proxy_config.health_check.enabled {
|
||
let hc = &proxy_config.health_check;
|
||
info!(
|
||
"🔧 [Pingora] Starting health check loop: interval={}s, timeout={}s",
|
||
hc.interval_seconds, hc.timeout_seconds
|
||
);
|
||
pingora_service
|
||
.start_health_check_loop(hc.interval_seconds, (hc.timeout_seconds * 1000) as u64);
|
||
info!("[Pingora] health check already started");
|
||
}
|
||
|
||
// 启动 Pingora 服务器(如果启动失败,直接退出程序)
|
||
info!("[Pingora] starting Pingora server...");
|
||
let (pingora_shutdown_tx, pingora_shutdown_rx) = tokio::sync::oneshot::channel();
|
||
let handle = tokio::spawn(async move {
|
||
info!("📍 [Pingora] calling server_manager.start()...");
|
||
if let Err(e) = server_manager.start(pingora_shutdown_rx).await {
|
||
error!(
|
||
"[Pingora] Pingora proxy start failed, error: {:?}",
|
||
e
|
||
);
|
||
std::process::exit(1);
|
||
}
|
||
info!("[Pingora] server started");
|
||
});
|
||
|
||
info!("[Pingora] already started");
|
||
|
||
(
|
||
Some(handle),
|
||
Some(pingora_service),
|
||
Some(pingora_shutdown_tx),
|
||
)
|
||
} else {
|
||
info!("⚠️ [Pingora] proxy_config notconfig, skip Pingora started");
|
||
(None, None, None)
|
||
};
|
||
|
||
// 设置 Ctrl+C 信号处理
|
||
let shutdown_tx = setup_signal_handlers();
|
||
let shutdown_rx = shutdown_tx.subscribe();
|
||
|
||
// 🆕 启动配置文件监控(支持 API Key 热更新)
|
||
// 保持 watcher 的所有权,防止被提前 drop
|
||
let config_path = std::path::PathBuf::from(crate::config::CONFIG_FILE);
|
||
let _config_watcher =
|
||
match crate::config_watcher::ConfigWatcher::new(config_path, Arc::clone(&api_key_config)) {
|
||
Ok(watcher) => {
|
||
info!("Config file watcher already started, API Key updated");
|
||
Some(watcher)
|
||
}
|
||
Err(e) => {
|
||
warn!(
|
||
"config file watcher start failed: {}, API Key updated",
|
||
e
|
||
);
|
||
None
|
||
}
|
||
};
|
||
|
||
// 获取容器前缀(从配置读取,用于 pod_count 和 pod_list)
|
||
let docker_config = config.docker_config.as_ref()
|
||
.ok_or_else(|| anyhow::anyhow!("Docker config is required for container prefix"))?;
|
||
let multi_config = docker_config.get_multi_image_config();
|
||
let selector = docker_manager::image_selector::ImageSelector::new(multi_config);
|
||
// 使用 block_in_place 在同步上下文中获取异步配置
|
||
let (container_prefix_rcoder, container_prefix_computer) = tokio::task::block_in_place(|| {
|
||
tokio::runtime::Handle::current().block_on(async {
|
||
let rcoder_prefix = selector
|
||
.get_service_config(&shared_types::ServiceType::RCoder)
|
||
.await
|
||
.expect("Failed to get RCoder service config")
|
||
.container_prefix()
|
||
.to_string();
|
||
let computer_prefix = selector
|
||
.get_service_config(&shared_types::ServiceType::ComputerAgentRunner)
|
||
.await
|
||
.expect("Failed to get ComputerAgentRunner service config")
|
||
.container_prefix()
|
||
.to_string();
|
||
(rcoder_prefix, computer_prefix)
|
||
})
|
||
});
|
||
|
||
let state = Arc::new(AppState::new(
|
||
config.clone(),
|
||
pingora_service_opt,
|
||
api_key_config,
|
||
container_prefix_rcoder,
|
||
container_prefix_computer,
|
||
)?);
|
||
|
||
// 在主异步运行时中启动清理任务(如果启用)
|
||
let _cleanup_handle = if config.cleanup_config.enabled {
|
||
let cleanup_config_clone = cleanup_config.clone();
|
||
let state_for_cleanup = state.clone();
|
||
Some(
|
||
cleanup_task::start_cleanup_task(cleanup_config_clone, state_for_cleanup)
|
||
.await
|
||
.map_err(|e| anyhow::anyhow!("Failed to start cleanup task: {}", e))?,
|
||
)
|
||
} else {
|
||
info!("Container cleanup task already started (cleanup_config.enabled=false)");
|
||
None
|
||
};
|
||
|
||
// 启动容器状态检查任务(防止长时间任务的容器被误杀)
|
||
// 🆕 使用增强的配置,包含失败计数器和智能跳过机制
|
||
let status_checker_config = ContainerStatusCheckerConfig {
|
||
check_interval: Duration::from_secs(30), // 每 30 秒检查一次
|
||
query_timeout: Duration::from_secs(5), // 查询超时 5 秒
|
||
failure_threshold: 3, // 连续失败 3 次后跳过
|
||
skip_duration: Duration::from_secs(5 * 60), // 跳过 5 分钟
|
||
health_reset_interval: Duration::from_secs(30 * 60), // 30 分钟清理一次
|
||
};
|
||
let _status_checker_handle =
|
||
start_container_status_checker(status_checker_config, state.clone());
|
||
info!(
|
||
"Container status checker already started (interval: 30s, will skip Docker on failure)"
|
||
);
|
||
|
||
// 启动容器状态同步任务(定期检测被外部删除的容器)
|
||
let container_sync_config = ContainerSyncConfig {
|
||
sync_interval: Duration::from_secs(60), // 每 60 秒同步一次
|
||
};
|
||
let _container_sync_handle = start_container_sync_task(
|
||
container_sync_config,
|
||
state.grpc_pool.clone(),
|
||
);
|
||
info!(
|
||
"Container status sync already started (interval: 60s, detect container)"
|
||
);
|
||
|
||
// 🆕 启动 VNC 后端同步任务(定期从 Docker 同步容器 IP 到 Pingora)
|
||
if let Some(ref pingora_service) = state.pingora_service {
|
||
let vnc_sync_config = VncSyncConfig {
|
||
sync_interval: Duration::from_secs(5), // 每 5 秒同步一次
|
||
};
|
||
let _vnc_sync_handle = start_vnc_sync_task(
|
||
pingora_service.clone(),
|
||
vnc_sync_config,
|
||
state.container_prefix_rcoder.clone(),
|
||
state.container_prefix_computer.clone(),
|
||
);
|
||
info!(
|
||
"VNC sync already started (interval: 5s, sync Docker container IP)"
|
||
);
|
||
}
|
||
|
||
// 创建路由(传入遥测 guard 用于 /metrics 端点)
|
||
let app = router::create_router(state.clone(), Some(telemetry.clone()));
|
||
|
||
// 启动 HTTP 服务器
|
||
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", config.port))
|
||
.await
|
||
.map_err(|e| anyhow::anyhow!("HTTP server failed to bind port {}: {}", config.port, e))?;
|
||
|
||
info!("Server starting on port {}", config.port);
|
||
info!("API endpoints:");
|
||
info!(" POST /chat - Send chat message to AI agent (legacy)");
|
||
info!(" GET /progress/:session_id - SSE progress stream for AI tasks (unified stream)");
|
||
info!(" GET /health - Health check");
|
||
info!(" NOTE: Plan data is delivered via the unified /progress/{{session_id}} SSE stream");
|
||
|
||
if let Some(proxy_config) = &config.proxy_config {
|
||
info!("Pingora proxy already started");
|
||
info!("📡 listenport: {}", proxy_config.listen_port);
|
||
info!("route: /proxy/{{port}}{{/path}} - example: /proxy/3000/api/users");
|
||
info!("format: query port parameter to proxy request");
|
||
info!("💡 example:");
|
||
info!(
|
||
" http://localhost:{}/proxy/{}/health → http://127.0.0.1:{}/health",
|
||
proxy_config.listen_port, config.port, config.port
|
||
);
|
||
info!(
|
||
" http://localhost:{}/proxy/{}/health → http://127.0.0.1:{}/health",
|
||
proxy_config.listen_port, config.port, config.port
|
||
);
|
||
info!(
|
||
" http://localhost:{}/proxy/9000/health → http://127.0.0.1:9000/health (dynamic discovery)",
|
||
proxy_config.listen_port
|
||
);
|
||
}
|
||
|
||
// 启动服务器,支持优雅关闭
|
||
// 使用自定义 Hyper 配置增加请求头大小限制(默认 8KB -> 128KB)
|
||
info!("🔧 config HTTP max_buf_size = 128KB (to prevent HTTP 431 error)");
|
||
|
||
let app = app.into_make_service();
|
||
let mut shutdown_rx_clone = shutdown_tx.subscribe();
|
||
|
||
// 启动自定义 HTTP 服务器
|
||
let server_handle = tokio::spawn(async move {
|
||
loop {
|
||
tokio::select! {
|
||
// 等待关闭信号
|
||
_ = shutdown_rx_clone.recv() => {
|
||
info!("🛑 HTTP server closed");
|
||
break;
|
||
}
|
||
// 接受新连接
|
||
result = listener.accept() => {
|
||
match result {
|
||
Ok((stream, addr)) => {
|
||
let mut app_clone = app.clone();
|
||
|
||
tokio::spawn(async move {
|
||
// 配置 HTTP1,增加 header 大小限制
|
||
let mut http_builder = http1::Builder::new();
|
||
http_builder
|
||
.max_buf_size(128 * 1024) // 128KB buffer(默认约 8KB)
|
||
.preserve_header_case(true)
|
||
.title_case_headers(false);
|
||
|
||
let io = TokioIo::new(stream);
|
||
|
||
// 使用 tower::Service 调用 MakeService
|
||
use tower::Service;
|
||
match std::future::poll_fn(|cx| {
|
||
Service::<std::net::SocketAddr>::poll_ready(&mut app_clone, cx)
|
||
}).await {
|
||
Ok(()) => {
|
||
match Service::<std::net::SocketAddr>::call(&mut app_clone, addr).await {
|
||
Ok(service) => {
|
||
let hyper_service = TowerToHyperService::new(service);
|
||
if let Err(e) = http_builder.serve_connection(io, hyper_service).await {
|
||
if !e.to_string().contains("connection closed")
|
||
&& !e.to_string().contains("early eof") {
|
||
tracing::debug!("HTTP connectionerror ({}): {}", addr, e);
|
||
}
|
||
}
|
||
}
|
||
Err(_) => {
|
||
// Infallible 类型,不会发生
|
||
}
|
||
}
|
||
}
|
||
Err(e) => {
|
||
tracing::error!("server error: {}", e);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
Err(e) => {
|
||
error!("connection failed: {}", e);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
// 等待服务器关闭
|
||
let _ = shutdown_signal(shutdown_rx).await;
|
||
server_handle.abort();
|
||
|
||
// 等待代理服务完成
|
||
if let Some(handle) = proxy_handle {
|
||
handle.await?;
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// 显示 Docker 配置帮助信息
|
||
fn show_docker_configuration_help(socket_path: &str) {
|
||
error!("📋 Docker config help:");
|
||
error!("");
|
||
error!("add to docker-compose.yml config:");
|
||
error!("");
|
||
error!("services:");
|
||
error!(" rcoder:");
|
||
error!(" environment:");
|
||
error!(" - DOCKER_SOCKET_PATH={}", socket_path);
|
||
error!(" volumes:");
|
||
error!(" - {}:/var/run/docker.sock:ro", socket_path);
|
||
error!(" - ./data/rcoder/project_workspace:/app/project_workspace");
|
||
error!("");
|
||
error!("🔧 Docker socket path:");
|
||
error!(" Linux: /var/run/docker.sock");
|
||
error!(" macOS + Docker Desktop: /var/run/docker.sock");
|
||
error!(" Rootless Docker: /run/user/$UID/docker.sock");
|
||
error!("");
|
||
error!("🛠️ troubleshooting:");
|
||
error!("1. check Docker: docker ps");
|
||
error!("2. check socket file exists: ls -l {}", socket_path);
|
||
error!("3. check docker group: groups $USER | grep docker");
|
||
error!(
|
||
" 4. Test Docker API: curl --unix-socket {} http://localhost/info",
|
||
socket_path
|
||
);
|
||
error!("");
|
||
error!("socket exists, rcoder container may not have access");
|
||
}
|
||
|
||
/// 设置信号处理器
|
||
fn setup_signal_handlers() -> tokio::sync::broadcast::Sender<()> {
|
||
let (shutdown_tx, _) = tokio::sync::broadcast::channel(1);
|
||
|
||
// 设置全局关闭标志
|
||
static SHUTDOWN_INITIATED: AtomicBool = AtomicBool::new(false);
|
||
|
||
// 注册 Ctrl+C 信号处理
|
||
#[cfg(unix)]
|
||
{
|
||
use tokio::signal::unix::{SignalKind, signal};
|
||
|
||
let shutdown_tx_clone = shutdown_tx.clone();
|
||
tokio::spawn(async move {
|
||
// 注册信号处理器,如果失败则记录警告并优雅降级
|
||
let sigint_result = signal(SignalKind::interrupt());
|
||
let sigterm_result = signal(SignalKind::terminate());
|
||
|
||
match (sigint_result, sigterm_result) {
|
||
(Ok(mut sigint), Ok(mut sigterm)) => {
|
||
tokio::select! {
|
||
_ = sigint.recv() => {
|
||
if !SHUTDOWN_INITIATED.swap(true, Ordering::SeqCst) {
|
||
info!(" received SIGINT (Ctrl+C), starting graceful shutdown...");
|
||
let _ = shutdown_tx_clone.send(());
|
||
}
|
||
}
|
||
_ = sigterm.recv() => {
|
||
if !SHUTDOWN_INITIATED.swap(true, Ordering::SeqCst) {
|
||
info!(" received SIGTERM, starting graceful shutdown...");
|
||
let _ = shutdown_tx_clone.send(());
|
||
}
|
||
}
|
||
}
|
||
}
|
||
(Err(e), _) | (_, Err(e)) => {
|
||
warn!(" unix signal handler failed: {}, shutdown may not be graceful", e);
|
||
// 注册失败不影响程序运行,仍可通过其他方式关闭(如 tokio::signal::ctrl_c)
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
#[cfg(not(unix))]
|
||
{
|
||
let shutdown_tx_clone = shutdown_tx.clone();
|
||
tokio::spawn(async move {
|
||
use tokio::signal;
|
||
|
||
if let Ok(()) = signal::ctrl_c().await {
|
||
if !SHUTDOWN_INITIATED.swap(true, Ordering::SeqCst) {
|
||
info!(" received Ctrl+C, starting graceful shutdown...");
|
||
let _ = shutdown_tx_clone.send(());
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
shutdown_tx
|
||
}
|
||
|
||
/// 优雅关闭信号处理
|
||
async fn shutdown_signal(mut shutdown_rx: tokio::sync::broadcast::Receiver<()>) {
|
||
// 等待关闭信号
|
||
let _ = shutdown_rx.recv().await;
|
||
|
||
info!("starting graceful shutdown...");
|
||
|
||
// 执行容器清理
|
||
if let Err(e) = cleanup_all_containers().await {
|
||
error!("container cleanup failed: {}", e);
|
||
} else {
|
||
info!("container cleanup completed");
|
||
}
|
||
|
||
info!("🛑 RCoder graceful shutdown completed");
|
||
}
|
||
|
||
/// 清理所有动态创建的容器
|
||
async fn cleanup_all_containers() -> anyhow::Result<()> {
|
||
info!("🧹 starting cleanup of dynamically created containers...");
|
||
|
||
match docker_manager::runtime::RuntimeManager::runtime_type() {
|
||
RuntimeType::Docker => {
|
||
let docker_manager = docker_manager::global::get_global_docker_manager()
|
||
.await
|
||
.map_err(|e| anyhow::anyhow!("Failed to get global DockerManager: {}", e))?;
|
||
|
||
let multi_image_config = shared_types::create_default_multi_image_config();
|
||
match container_stop::startup_cleanup_all_enabled_services(
|
||
&docker_manager,
|
||
&multi_image_config,
|
||
)
|
||
.await
|
||
{
|
||
Ok(result) => {
|
||
if result.successfully_removed > 0 {
|
||
info!(
|
||
"🧹 Cleaned up {} containers (all enabled services)",
|
||
result.successfully_removed
|
||
);
|
||
}
|
||
|
||
if result.failed_removals > 0 {
|
||
warn!(
|
||
"container cleanup failed: failed count={}",
|
||
result.failed_removals
|
||
);
|
||
}
|
||
}
|
||
Err(e) => {
|
||
warn!("container cleanup error: {}", e);
|
||
}
|
||
}
|
||
}
|
||
RuntimeType::Kubernetes => {
|
||
let runtime = docker_manager::runtime::RuntimeManager::get()
|
||
.await
|
||
.map_err(|e| anyhow::anyhow!("Failed to get runtime: {}", e))?;
|
||
runtime
|
||
.cleanup_all()
|
||
.await
|
||
.map_err(|e| anyhow::anyhow!("Failed to cleanup runtime resources: {}", e))?;
|
||
}
|
||
}
|
||
|
||
Ok(())
|
||
}
|