use anyhow::{Context, Result};
use std::path::Path;
use tracing::{debug, info, warn};
#[derive(Debug, Clone)]
pub struct PreflightConfig {
pub check_ports: Vec<u16>,
pub min_disk_space_gb: u64,
pub require_gpu: bool,
pub redis_url: String,
}
impl Default for PreflightConfig {
fn default() -> Self {
Self {
check_ports: vec![],
min_disk_space_gb: 1,
require_gpu: false,
redis_url: "redis://localhost:6379".to_string(),
}
}
}
pub async fn run_preflight_checks(config: &PreflightConfig) -> Result<()> {
info!("🔍 Running pre-flight checks...");
check_redis_connectivity(&config.redis_url).await?;
check_disk_space(config.min_disk_space_gb)?;
check_ports(&config.check_ports)?;
check_permissions()?;
if config.require_gpu {
check_gpu_availability()?;
}
info!("✅ All pre-flight checks passed");
Ok(())
}
async fn check_redis_connectivity(redis_url: &str) -> Result<()> {
let client = redis::Client::open(redis_url).with_context(|| {
format!(
"❌ Pre-flight check failed: Cannot connect to Redis at {}\n\n\
Possible solutions:\n\
1. Start Redis: docker compose up -d redis\n\
2. Check REDIS_URL environment variable\n\
3. Skip this check with: --skip-preflight (not recommended)\n",
redis_url
)
})?;
let mut conn = client.get_multiplexed_async_connection().await.with_context(|| {
format!(
"❌ Pre-flight check failed: Redis not reachable at {}\n\n\
Possible solutions:\n\
1. Verify Redis is running: docker compose ps\n\
2. Check network connectivity\n\
3. Skip this check with: --skip-preflight (not recommended)\n",
redis_url
)
})?;
redis::cmd("PING")
.query_async::<String>(&mut conn)
.await
.with_context(|| {
format!(
"❌ Pre-flight check failed: Redis ping failed at {}\n\n\
Possible solutions:\n\
1. Check Redis server health: docker compose logs redis\n\
2. Restart Redis: docker compose restart redis\n\
3. Skip this check with: --skip-preflight (not recommended)\n",
redis_url
)
})?;
debug!("✅ Redis connectivity: OK");
Ok(())
}
fn check_disk_space(min_gb: u64) -> Result<()> {
#[cfg(target_os = "linux")]
{
check_disk_space_unix(min_gb)
}
#[cfg(target_os = "macos")]
{
check_disk_space_unix(min_gb)
}
#[cfg(target_os = "windows")]
{
check_disk_space_windows(min_gb)
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
warn!("⚠️ Disk space check not supported on this platform");
Ok(())
}
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn check_disk_space_unix(min_gb: u64) -> Result<()> {
use std::ffi::CString;
use std::mem::MaybeUninit;
let path = CString::new(".").unwrap();
let mut stat: MaybeUninit<libc::statvfs> = MaybeUninit::uninit();
let result = unsafe { libc::statvfs(path.as_ptr(), stat.as_mut_ptr()) };
if result != 0 {
warn!("⚠️ Could not check disk space (continuing anyway)");
return Ok(());
}
let stat = unsafe { stat.assume_init() };
#[allow(clippy::unnecessary_cast)]
let available_bytes = stat.f_bavail as u64 * stat.f_frsize as u64;
let available_gb = available_bytes / (1024 * 1024 * 1024);
let min_bytes = min_gb * 1024 * 1024 * 1024;
if available_bytes < min_bytes {
anyhow::bail!(
"❌ Pre-flight check failed: Insufficient disk space\n\n\
Available: {:.2} GB\n\
Required: {} GB\n\n\
Possible solutions:\n\
1. Free up disk space (delete unused files, clear caches)\n\
2. Use a different working directory with more space\n\
3. Skip this check with: --skip-preflight (not recommended)\n\
4. Adjust minimum in node config: \"preflight\": {{ \"min_disk_space_gb\": <lower-value> }}\n",
available_gb as f64,
min_gb
);
}
debug!(
"✅ Disk space: {:.2} GB available (>= {} GB required)",
available_gb as f64, min_gb
);
Ok(())
}
#[cfg(target_os = "windows")]
fn check_disk_space_windows(min_gb: u64) -> Result<()> {
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
let path: Vec<u16> = OsStr::new(".").encode_wide().chain(Some(0)).collect();
let mut free_bytes: u64 = 0;
let mut total_bytes: u64 = 0;
let mut total_free_bytes: u64 = 0;
let result = unsafe {
winapi::um::fileapi::GetDiskFreeSpaceExW(
path.as_ptr(),
&mut free_bytes as *mut u64 as *mut _,
&mut total_bytes as *mut u64 as *mut _,
&mut total_free_bytes as *mut u64 as *mut _,
)
};
if result == 0 {
warn!("⚠️ Could not check disk space (continuing anyway)");
return Ok(());
}
let available_gb = free_bytes / (1024 * 1024 * 1024);
let min_bytes = min_gb * 1024 * 1024 * 1024;
if free_bytes < min_bytes {
anyhow::bail!(
"❌ Pre-flight check failed: Insufficient disk space\n\n\
Available: {:.2} GB\n\
Required: {} GB\n\n\
Possible solutions:\n\
1. Free up disk space (delete unused files, clear caches)\n\
2. Use a different working directory with more space\n\
3. Skip this check with: --skip-preflight (not recommended)\n\
4. Adjust minimum in node config: \"preflight\": {{ \"min_disk_space_gb\": <lower-value> }}\n",
available_gb as f64,
min_gb
);
}
debug!(
"✅ Disk space: {:.2} GB available (>= {} GB required)",
available_gb as f64, min_gb
);
Ok(())
}
fn check_ports(ports: &[u16]) -> Result<()> {
if ports.is_empty() {
return Ok(());
}
for &port in ports {
match std::net::TcpListener::bind(("127.0.0.1", port)) {
Ok(_) => {}
Err(_) => {
#[cfg(unix)]
let find_cmd = format!("lsof -i :{}", port);
#[cfg(windows)]
let find_cmd = format!("netstat -ano | findstr :{}", port);
anyhow::bail!(
"❌ Pre-flight check failed: Port {} already in use\n\n\
Possible solutions:\n\
1. Stop the process using port {}\n\
2. Change the port in your node configuration\n\
3. Skip this check with: --skip-preflight (not recommended)\n\n\
Find process using port: {}\n",
port,
port,
find_cmd
);
}
}
}
debug!("✅ Ports: {:?} free", ports);
Ok(())
}
fn check_permissions() -> Result<()> {
let logs_dir = Path::new("logs");
if !logs_dir.exists() {
std::fs::create_dir_all(logs_dir).with_context(|| {
"❌ Pre-flight check failed: Cannot create logs/ directory\n\n\
Possible solutions:\n\
1. Check file system permissions in current directory\n\
2. Run with appropriate user permissions\n\
3. Skip this check with: --skip-preflight (not recommended)\n"
})?;
}
let test_file = logs_dir.join(".write_test");
std::fs::write(&test_file, "test").with_context(|| {
"❌ Pre-flight check failed: Cannot write to logs/ directory\n\n\
Possible solutions:\n\
1. Check directory permissions: ls -la logs/\n\
2. Ensure write access: chmod u+w logs/\n\
3. Run with appropriate user permissions\n\
4. Skip this check with: --skip-preflight (not recommended)\n"
})?;
let _ = std::fs::remove_file(&test_file);
let models_dir = Path::new("models");
if models_dir.exists() {
std::fs::read_dir(models_dir).with_context(|| {
"❌ Pre-flight check failed: Cannot read models/ directory\n\n\
Possible solutions:\n\
1. Check directory permissions: ls -la models/\n\
2. Ensure read access: chmod u+r models/\n\
3. Run with appropriate user permissions\n\
4. Skip this check with: --skip-preflight (not recommended)\n"
})?;
}
debug!("✅ Permissions: logs/ writable, models/ readable");
Ok(())
}
fn check_gpu_availability() -> Result<()> {
if std::env::var("CUDA_VISIBLE_DEVICES").is_ok() {
info!("✅ GPU: CUDA detected (CUDA_VISIBLE_DEVICES set)");
return Ok(());
}
#[cfg(unix)]
{
if which::which("nvidia-smi").is_ok() {
info!("✅ GPU: NVIDIA GPU detected (nvidia-smi found)");
return Ok(());
}
}
#[cfg(target_os = "macos")]
{
if std::env::consts::ARCH == "aarch64" {
info!("✅ GPU: Metal detected (Apple Silicon)");
return Ok(());
}
}
anyhow::bail!(
"❌ Pre-flight check failed: GPU required but not detected\n\n\
Possible solutions:\n\
1. For NVIDIA GPUs: Set CUDA_VISIBLE_DEVICES environment variable\n\
2. For NVIDIA GPUs: Install NVIDIA drivers and verify nvidia-smi works\n\
3. For Apple Silicon: This check should pass automatically on M-series Macs\n\
4. Remove --require-gpu flag if GPU is not needed\n\
5. Skip this check with: --skip-preflight (not recommended)\n\n\
Note: This is a simple detection check. For full GPU support, ensure\n\
appropriate ML frameworks (CUDA, Metal, etc.) are properly configured.\n"
);
}