diff --git a/src/mount/ops.rs b/src/mount/ops.rs index 139e125..1ae2f68 100644 --- a/src/mount/ops.rs +++ b/src/mount/ops.rs @@ -41,9 +41,11 @@ use crate::{ util::{run_cmd, run_cmd_capture, which_tool}, Error, Result, }; +use std::collections::HashMap; use std::fs::{create_dir_all, File}; use std::io::Write; use std::path::Path; +use tracing::info; const ROOT_BASE: &str = "/var/mounts"; const TARGET_SYSTEM: &str = "/var/cache/system"; @@ -52,6 +54,71 @@ const TARGET_MODULES: &str = "/var/cache/modules"; const TARGET_VM_META: &str = "/var/cache/vm-meta"; const SUBVOLS: &[&str] = &["system", "etc", "modules", "vm-meta"]; +#[derive(Debug, Clone)] +struct ExistingMount { + source: String, + fstype: String, + options: String, +} + +fn current_mounts() -> HashMap { + let mut map = HashMap::new(); + if let Ok(content) = std::fs::read_to_string("/proc/self/mountinfo") { + for line in content.lines() { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() < 7 { + continue; + } + let target = parts[4].to_string(); + let mount_options = parts[5].to_string(); + if let Some(idx) = parts.iter().position(|p| *p == "-") { + if idx + 2 < parts.len() { + let fstype = parts[idx + 1].to_string(); + let source = parts[idx + 2].to_string(); + let super_opts = if idx + 3 < parts.len() { + parts[idx + 3].to_string() + } else { + String::new() + }; + let combined_options = if super_opts.is_empty() { + mount_options.clone() + } else { + format!("{mount_options},{super_opts}") + }; + map.insert( + target, + ExistingMount { + source, + fstype, + options: combined_options, + }, + ); + } + } + } + } + map +} + +fn source_matches_uuid(existing_source: &str, uuid: &str) -> bool { + if existing_source == format!("UUID={}", uuid) { + return true; + } + if let Some(existing_uuid) = existing_source.strip_prefix("UUID=") { + return existing_uuid == uuid; + } + if existing_source.starts_with("/dev/") { + let uuid_path = Path::new("/dev/disk/by-uuid").join(uuid); + if let (Ok(existing_canon), Ok(uuid_canon)) = ( + std::fs::canonicalize(existing_source), + std::fs::canonicalize(&uuid_path), + ) { + return existing_canon == uuid_canon; + } + } + false +} + #[derive(Debug, Clone)] pub struct PlannedMount { pub uuid: String, // UUID string without prefix @@ -189,11 +256,37 @@ pub fn apply_mounts(plan: &MountPlan) -> Result> { .map_err(|e| Error::Mount(format!("failed to create dir {}: {}", sm.target, e)))?; } - let mut results: Vec = Vec::new(); + let mut results_map: HashMap = HashMap::new(); + let mut existing_mounts = current_mounts(); // Root mounts for pm in &plan.root_mounts { let source = format!("UUID={}", pm.uuid); + if let Some(existing) = existing_mounts.get(pm.target.as_str()) { + if source_matches_uuid(&existing.source, &pm.uuid) { + info!( + "mount::apply_mounts: target {} already mounted; skipping", + pm.target + ); + let existing_fstype = existing.fstype.clone(); + let existing_options = existing.options.clone(); + results_map + .entry(pm.target.clone()) + .or_insert_with(|| MountResult { + source: source.clone(), + target: pm.target.clone(), + fstype: existing_fstype, + options: existing_options, + }); + continue; + } else { + return Err(Error::Mount(format!( + "target {} already mounted by {} (expected UUID={})", + pm.target, existing.source, pm.uuid + ))); + } + } + let args = [ mount_tool.as_str(), "-t", @@ -204,12 +297,23 @@ pub fn apply_mounts(plan: &MountPlan) -> Result> { pm.target.as_str(), ]; run_cmd(&args)?; - results.push(MountResult { - source, - target: pm.target.clone(), - fstype: pm.fstype.clone(), - options: pm.options.clone(), - }); + existing_mounts.insert( + pm.target.clone(), + ExistingMount { + source: source.clone(), + fstype: pm.fstype.clone(), + options: pm.options.clone(), + }, + ); + results_map.insert( + pm.target.clone(), + MountResult { + source, + target: pm.target.clone(), + fstype: pm.fstype.clone(), + options: pm.options.clone(), + }, + ); } // Subvolume creation (create-if-missing) and mounts for the primary @@ -279,6 +383,31 @@ pub fn apply_mounts(plan: &MountPlan) -> Result> { // Subvol mounts for sm in &plan.subvol_mounts { let source = format!("UUID={}", sm.uuid); + if let Some(existing) = existing_mounts.get(sm.target.as_str()) { + if source_matches_uuid(&existing.source, &sm.uuid) { + info!( + "mount::apply_mounts: target {} already mounted; skipping", + sm.target + ); + let existing_fstype = existing.fstype.clone(); + let existing_options = existing.options.clone(); + results_map + .entry(sm.target.clone()) + .or_insert_with(|| MountResult { + source: source.clone(), + target: sm.target.clone(), + fstype: existing_fstype, + options: existing_options, + }); + continue; + } else { + return Err(Error::Mount(format!( + "target {} already mounted by {} (expected UUID={})", + sm.target, existing.source, sm.uuid + ))); + } + } + let args = [ mount_tool.as_str(), "-t", @@ -289,14 +418,28 @@ pub fn apply_mounts(plan: &MountPlan) -> Result> { sm.target.as_str(), ]; run_cmd(&args)?; - results.push(MountResult { - source, - target: sm.target.clone(), - fstype: sm.fstype.clone(), - options: sm.options.clone(), - }); + existing_mounts.insert( + sm.target.clone(), + ExistingMount { + source: source.clone(), + fstype: sm.fstype.clone(), + options: sm.options.clone(), + }, + ); + results_map.insert( + sm.target.clone(), + MountResult { + source, + target: sm.target.clone(), + fstype: sm.fstype.clone(), + options: sm.options.clone(), + }, + ); } + let mut results: Vec = results_map.into_values().collect(); + results.sort_by(|a, b| a.target.cmp(&b.target)); + Ok(results) } @@ -306,19 +449,24 @@ pub fn maybe_write_fstab(mounts: &[MountResult], cfg: &Config) -> Result<()> { return Ok(()); } - // Filter only the four subvol targets + // Partition mount results into runtime root mounts and final subvolume targets. + let mut root_entries: Vec<&MountResult> = mounts + .iter() + .filter(|m| m.target.starts_with(ROOT_BASE)) + .collect(); let wanted = [TARGET_ETC, TARGET_MODULES, TARGET_SYSTEM, TARGET_VM_META]; - let mut entries: Vec<&MountResult> = mounts + let mut subvol_entries: Vec<&MountResult> = mounts .iter() .filter(|m| wanted.contains(&m.target.as_str())) .collect(); - // Sort by target path ascending to be deterministic - entries.sort_by(|a, b| a.target.cmp(&b.target)); + // Sort by target path ascending to be deterministic (roots before subvols). + root_entries.sort_by(|a, b| a.target.cmp(&b.target)); + subvol_entries.sort_by(|a, b| a.target.cmp(&b.target)); - // Compose lines + // Compose lines: include all root mounts first, followed by the four subvol targets. let mut lines: Vec = Vec::new(); - for m in entries { + for m in root_entries.into_iter().chain(subvol_entries.into_iter()) { // m.source already "UUID=..." let line = format!( "{} {} {} {} 0 0",