use std::path::Path; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; use tokio::sync::Mutex; use tracing::error; use crate::commands::write_config::INTERNAL_CONFIG_FOLDER_NAME; use crate::commands::{run::run_cmd, validate::ValidateCommand}; pub struct ReloadCommand { is_reloading: Mutex, } struct ReloadResetGuard<'a> { guard: tokio::sync::MutexGuard<'a, bool>, } impl<'a> Drop for ReloadResetGuard<'a> { fn drop(&mut self) { *self.guard = false; } } impl Default for ReloadCommand { fn default() -> Self { Self { is_reloading: Mutex::new(false), } } } impl ReloadCommand { pub async fn validate_and_reload( &self, config_name: &str, timestamp: u64, validate_cmd: Arc, ) -> Result<(i32, String), Box> { // ensure the written fragment exists validate_cmd.validate(config_name, timestamp).await?; // Now atomically swap the YANPM.conf symlink to point to the new fragment // so nginx -t validates the composed main config. If validation fails, // attempt to restore the previous symlink. let filename = crate::commands::run::to_file_name(config_name, timestamp)?; let nginx_dir = validate_cmd.nginx_config_dir(); let symlink_path = nginx_dir.join("YANPM.conf"); let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos(); let tmp_name = format!("YANPM.conf.tmp.{}.{}", std::process::id(), now); let tmp_path = nginx_dir.join(&tmp_name); // prepare relative target: INTERNAL_CONFIG_FOLDER_NAME/ let rel_target = Path::new(INTERNAL_CONFIG_FOLDER_NAME).join(&filename); // read previous target if exists let previous_target = std::fs::read_link(&symlink_path).ok(); // Acquire reload guard before mutating the symlink to avoid races let reloading_lock = self.is_reloading.lock().await; if *reloading_lock { return Err("Reload already in progress".into()); } // set flag to true and ensure it is reset on drop let mut mut_guard = reloading_lock; *mut_guard = true; let _reset_guard = ReloadResetGuard { guard: mut_guard }; // create temporary symlink and atomically rename into place std::os::unix::fs::symlink(&rel_target, &tmp_path)?; tokio::fs::rename(&tmp_path, &symlink_path).await?; // validate composed main config now that symlink points to new fragment if let Err(e) = validate_cmd.validate_all().await { // restore previous symlink state while still holding the guard if let Some(prev) = previous_target { let restore_tmp = nginx_dir.join(format!("YANPM.conf.restore.{}.{}", std::process::id(), now)); std::os::unix::fs::symlink(&prev, &restore_tmp)?; if let Err(err) = tokio::fs::rename(&restore_tmp, &symlink_path).await { error!( "Failed to restore previous YANPM.conf symlink after validation error: {}", err ); } } else if let Err(err) = tokio::fs::remove_file(&symlink_path).await { error!( "Failed to remove YANPM.conf symlink after validation error: {}", err ); } return Err(e); } // reload the running nginx master process (no -c) so it reloads its configured main config // Prefer the restricted sudo wrapper if available, fall back to direct nginx reload. // TODO: allow configuring the path to the wrapper match run_cmd("sudo", &["-n", "/usr/local/sbin/yanpm-nginx-reload"], 10).await { Ok(res) => Ok(res), Err(e) => { error!( "sudo reload wrapper failed, falling back to direct nginx reload: {}", e ); run_cmd("nginx", &["-s", "reload"], 10).await } } } }