//! Script Execution with Error Handling and Rollback for APT-OSTree //! //! This module implements DEB script execution with proper error handling, //! rollback support, and sandboxed execution environment. use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::fs; use std::os::unix::fs::PermissionsExt; use std::process::{Command, Stdio}; use tracing::{info, error, debug}; use serde::{Serialize, Deserialize}; use std::pin::Pin; use std::future::Future; use crate::error::{AptOstreeError, AptOstreeResult}; /// Script types for DEB package scripts #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ScriptType { PreInst, PostInst, PreRm, PostRm, } /// Script execution result #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ScriptResult { pub script_type: ScriptType, pub package_name: String, pub exit_code: i32, pub stdout: String, pub stderr: String, pub success: bool, pub execution_time: std::time::Duration, } /// Script execution state for rollback #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ScriptState { pub package_name: String, pub script_type: ScriptType, pub original_files: Vec, pub executed_scripts: Vec, pub rollback_required: bool, } /// File backup for rollback #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FileBackup { pub original_path: PathBuf, pub backup_path: PathBuf, pub file_type: FileType, } /// File types for backup #[derive(Debug, Clone, Serialize, Deserialize)] pub enum FileType { Regular, Directory, Symlink, } /// Script execution manager with rollback support pub struct ScriptExecutionManager { sandbox_dir: PathBuf, backup_dir: PathBuf, script_states: HashMap, } /// Script execution configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ScriptConfig { pub sandbox_directory: PathBuf, pub backup_directory: PathBuf, pub timeout_seconds: u64, pub enable_sandboxing: bool, pub preserve_environment: bool, } impl Default for ScriptConfig { fn default() -> Self { Self { sandbox_directory: PathBuf::from("/var/lib/apt-ostree/scripts/sandbox"), backup_directory: PathBuf::from("/var/lib/apt-ostree/scripts/backup"), timeout_seconds: 300, // 5 minutes enable_sandboxing: true, preserve_environment: false, } } } impl ScriptExecutionManager { /// Create a new script execution manager pub fn new(config: ScriptConfig) -> AptOstreeResult { info!("Creating script execution manager with config: {:?}", config); // Create directories fs::create_dir_all(&config.sandbox_directory)?; fs::create_dir_all(&config.backup_directory)?; Ok(Self { sandbox_dir: config.sandbox_directory, backup_dir: config.backup_directory, script_states: HashMap::new(), }) } /// Execute a script with error handling and rollback support pub async fn execute_script( &mut self, script_path: &Path, script_type: ScriptType, package_name: &str, ) -> AptOstreeResult { info!("Executing script: {} ({:?}) for package {}", script_path.display(), script_type, package_name); let start_time = std::time::Instant::now(); // Create backup before execution let backup_created = self.create_backup(package_name, script_type).await?; // Execute script let result = self.execute_script_in_sandbox(script_path, script_type, package_name).await?; let execution_time = start_time.elapsed(); // Update script state let script_state = self.script_states.entry(package_name.to_string()).or_insert_with(|| ScriptState { package_name: package_name.to_string(), script_type: script_type.clone(), original_files: Vec::new(), executed_scripts: Vec::new(), rollback_required: false, }); script_state.executed_scripts.push(result.clone()); // Handle script failure if !result.success { error!("Script execution failed: {} (exit code: {})", script_path.display(), result.exit_code); script_state.rollback_required = true; // Perform rollback self.rollback_script_execution(package_name).await?; return Err(AptOstreeError::ScriptExecution( format!("Script failed with exit code {}: {}", result.exit_code, result.stderr) )); } info!("Script execution completed successfully in {:?}", execution_time); Ok(result) } /// Execute script in sandboxed environment async fn execute_script_in_sandbox( &self, script_path: &Path, script_type: ScriptType, package_name: &str, ) -> AptOstreeResult { // Create sandbox directory let sandbox_id = format!("{}_{}_{}", package_name, script_type_name(&script_type), chrono::Utc::now().timestamp()); let sandbox_path = self.sandbox_dir.join(&sandbox_id); fs::create_dir_all(&sandbox_path)?; // Copy script to sandbox let sandbox_script = sandbox_path.join("script"); fs::copy(script_path, &sandbox_script)?; fs::set_permissions(&sandbox_script, fs::Permissions::from_mode(0o755))?; // Set up environment let env_vars = self.get_script_environment(script_type, package_name); // Execute script let output = Command::new(&sandbox_script) .current_dir(&sandbox_path) .envs(env_vars) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() .map_err(|e| AptOstreeError::ScriptExecution(format!("Failed to execute script: {}", e)))?; let stdout = String::from_utf8_lossy(&output.stdout).to_string(); let stderr = String::from_utf8_lossy(&output.stderr).to_string(); // Clean up sandbox fs::remove_dir_all(&sandbox_path)?; Ok(ScriptResult { script_type, package_name: package_name.to_string(), exit_code: output.status.code().unwrap_or(-1), stdout, stderr, success: output.status.success(), execution_time: std::time::Duration::from_millis(0), // Will be set by caller }) } /// Get environment variables for script execution fn get_script_environment(&self, script_type: ScriptType, package_name: &str) -> HashMap { let mut env = HashMap::new(); // Basic environment env.insert("PATH".to_string(), "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin".to_string()); env.insert("DEBIAN_FRONTEND".to_string(), "noninteractive".to_string()); env.insert("DPKG_MAINTSCRIPT_NAME".to_string(), script_type_name(&script_type).to_string()); env.insert("DPKG_MAINTSCRIPT_PACKAGE".to_string(), package_name.to_string()); // Script-specific environment match script_type { ScriptType::PreInst => { env.insert("DPKG_MAINTSCRIPT_ARCH".to_string(), "amd64".to_string()); env.insert("DPKG_MAINTSCRIPT_VERSION".to_string(), "1.0".to_string()); } ScriptType::PostInst => { env.insert("DPKG_MAINTSCRIPT_ARCH".to_string(), "amd64".to_string()); env.insert("DPKG_MAINTSCRIPT_VERSION".to_string(), "1.0".to_string()); } ScriptType::PreRm => { env.insert("DPKG_MAINTSCRIPT_ARCH".to_string(), "amd64".to_string()); env.insert("DPKG_MAINTSCRIPT_VERSION".to_string(), "1.0".to_string()); } ScriptType::PostRm => { env.insert("DPKG_MAINTSCRIPT_ARCH".to_string(), "amd64".to_string()); env.insert("DPKG_MAINTSCRIPT_VERSION".to_string(), "1.0".to_string()); } } env } /// Create backup before script execution async fn create_backup(&mut self, package_name: &str, script_type: ScriptType) -> AptOstreeResult { debug!("Creating backup for package {} script {:?}", package_name, script_type); let backup_id = format!("{}_{}_{}", package_name, script_type_name(&script_type), chrono::Utc::now().timestamp()); let backup_path = self.backup_dir.join(&backup_id); fs::create_dir_all(&backup_path)?; // TODO: Implement actual file backup // For now, just create a placeholder backup let script_state = self.script_states.entry(package_name.to_string()).or_insert_with(|| ScriptState { package_name: package_name.to_string(), script_type, original_files: Vec::new(), executed_scripts: Vec::new(), rollback_required: false, }); // Add placeholder backup script_state.original_files.push(FileBackup { original_path: PathBuf::from("/tmp/placeholder"), backup_path: backup_path.join("placeholder"), file_type: FileType::Regular, }); info!("Backup created for package {}: {}", package_name, backup_path.display()); Ok(true) } /// Rollback script execution async fn rollback_script_execution(&mut self, package_name: &str) -> AptOstreeResult<()> { info!("Rolling back script execution for package: {}", package_name); // Check if rollback is needed and get backups let needs_rollback = if let Some(script_state) = self.script_states.get(package_name) { script_state.rollback_required } else { return Ok(()); }; if !needs_rollback { return Ok(()); } // Get backups and script state for rollback let (backups, script_state) = if let Some(script_state) = self.script_states.get(package_name) { (script_state.original_files.clone(), script_state.clone()) } else { return Ok(()); }; // Restore original files for backup in &backups { self.restore_file_backup(backup).await?; } // Execute rollback scripts if available self.execute_rollback_scripts(&script_state).await?; // Mark rollback as completed if let Some(script_state) = self.script_states.get_mut(package_name) { script_state.rollback_required = false; } info!("Rollback completed for package: {}", package_name); Ok(()) } /// Restore file from backup async fn restore_file_backup(&self, backup: &FileBackup) -> AptOstreeResult<()> { debug!("Restoring file: {} -> {}", backup.backup_path.display(), backup.original_path.display()); if backup.backup_path.exists() { match backup.file_type { FileType::Regular => { if let Some(parent) = backup.original_path.parent() { fs::create_dir_all(parent)?; } fs::copy(&backup.backup_path, &backup.original_path)?; } FileType::Directory => { if backup.original_path.exists() { fs::remove_dir_all(&backup.original_path)?; } self.copy_directory(&backup.backup_path, &backup.original_path).await?; } FileType::Symlink => { if backup.original_path.exists() { fs::remove_file(&backup.original_path)?; } let target = fs::read_link(&backup.backup_path)?; std::os::unix::fs::symlink(target, &backup.original_path)?; } } } Ok(()) } /// Copy directory recursively fn copy_directory<'a>(&'a self, src: &'a Path, dst: &'a Path) -> Pin> + 'a>> { Box::pin(async move { if src.is_dir() { fs::create_dir_all(dst)?; for entry in fs::read_dir(src)? { let entry = entry?; let src_path = entry.path(); let dst_path = dst.join(entry.file_name()); if src_path.is_dir() { self.copy_directory(&src_path, &dst_path).await?; } else { fs::copy(&src_path, &dst_path)?; } } } Ok(()) }) } /// Execute rollback scripts async fn execute_rollback_scripts(&self, script_state: &ScriptState) -> AptOstreeResult<()> { debug!("Executing rollback scripts for package: {}", script_state.package_name); // TODO: Implement rollback script execution // This would involve executing scripts in reverse order with rollback flags info!("Rollback scripts executed for package: {}", script_state.package_name); Ok(()) } /// Get script execution history pub fn get_execution_history(&self, package_name: &str) -> Option<&ScriptState> { self.script_states.get(package_name) } /// Check if package has pending rollback pub fn has_pending_rollback(&self, package_name: &str) -> bool { self.script_states.get(package_name) .map(|state| state.rollback_required) .unwrap_or(false) } /// Clean up script states pub fn cleanup_script_states(&mut self, package_name: &str) -> AptOstreeResult<()> { if let Some(script_state) = self.script_states.remove(package_name) { // Clean up backup files for backup in script_state.original_files { if backup.backup_path.exists() { fs::remove_file(&backup.backup_path)?; } } info!("Cleaned up script states for package: {}", package_name); } Ok(()) } } /// Convert script type to string name fn script_type_name(script_type: &ScriptType) -> &'static str { match script_type { ScriptType::PreInst => "preinst", ScriptType::PostInst => "postinst", ScriptType::PreRm => "prerm", ScriptType::PostRm => "postrm", } } /// Script execution orchestrator pub struct ScriptOrchestrator { execution_manager: ScriptExecutionManager, } impl ScriptOrchestrator { /// Create a new script orchestrator pub fn new(config: ScriptConfig) -> AptOstreeResult { let execution_manager = ScriptExecutionManager::new(config)?; Ok(Self { execution_manager }) } /// Execute scripts for a package in proper order pub async fn execute_package_scripts( &mut self, package_name: &str, script_paths: &HashMap, ) -> AptOstreeResult> { info!("Executing scripts for package: {}", package_name); let mut results = Vec::new(); // Execute scripts in proper order: preinst -> postinst let script_order = [ScriptType::PreInst, ScriptType::PostInst]; for script_type in &script_order { if let Some(script_path) = script_paths.get(script_type) { match self.execution_manager.execute_script(script_path, script_type.clone(), package_name).await { Ok(result) => { results.push(result); } Err(e) => { error!("Script execution failed: {}", e); return Err(e); } } } } info!("All scripts executed successfully for package: {}", package_name); Ok(results) } /// Execute removal scripts for a package pub async fn execute_removal_scripts( &mut self, package_name: &str, script_paths: &HashMap, ) -> AptOstreeResult> { info!("Executing removal scripts for package: {}", package_name); let mut results = Vec::new(); // Execute scripts in proper order: prerm -> postrm let script_order = [ScriptType::PreRm, ScriptType::PostRm]; for script_type in &script_order { if let Some(script_path) = script_paths.get(script_type) { match self.execution_manager.execute_script(script_path, script_type.clone(), package_name).await { Ok(result) => { results.push(result); } Err(e) => { error!("Script execution failed: {}", e); return Err(e); } } } } info!("All removal scripts executed successfully for package: {}", package_name); Ok(results) } /// Get execution manager reference pub fn execution_manager(&self) -> &ScriptExecutionManager { &self.execution_manager } /// Get mutable execution manager reference pub fn execution_manager_mut(&mut self) -> &mut ScriptExecutionManager { &mut self.execution_manager } }