diff --git a/src/filesystem/mount.rs b/src/filesystem/mount.rs index 2248bd6..fcd8e98 100644 --- a/src/filesystem/mount.rs +++ b/src/filesystem/mount.rs @@ -1,5 +1,12 @@ -use crate::log::{log, LogLevel}; +use crate::filesystem::mounted::{/* is_mounted, */ FS_MOUNT_GUARD}; +use crate::log::{LogLevel, log}; use std::process::Command; +use std::sync::{Mutex, OnceLock, RwLock}; +use std::thread::sleep; +use std::time::Duration; + +static GUARD_MOUNT: OnceLock> = OnceLock::new(); +static GUARD_UNMOUNT: OnceLock> = OnceLock::new(); /// Führt die Einbindung eines Dateisystems durch. /// @@ -8,6 +15,16 @@ use std::process::Command; /// * `network_path` - Der Netzwerkpfad zum einzubindenden Dateisystem /// * `mount_type` - Der Mount-Typ, z.B. "nfs" oder "davfs2" pub fn mount(mount_point: &str, network_path: &str, mount_type: &str) { + // 1) Globalen Write-Lock halten (blockiert parallele Statusabfragen) + let _fs_write = FS_MOUNT_GUARD + .get_or_init(|| RwLock::new(())) + .write() + .expect("mount rwlock poisoned"); + + // 2) Danach funktionsspezifischen Mutex sperren (konsistente Lock-Reihenfolge!) + let m = GUARD_MOUNT.get_or_init(|| Mutex::new(())); + let _lock = m.lock().expect("Mutex poisoned"); + log( "mount", &*format!( @@ -53,12 +70,7 @@ pub fn mount(mount_point: &str, network_path: &str, mount_type: &str) { #[cfg(not(target_os = "windows"))] { - let mount_point: &str = if mount_point.ends_with('/') { - mount_point - } else { - &*format!("{}/", mount_point) - }; - + // Verzeichnis vorbereiten let output = Command::new("mkdir") .arg("-p") .arg(mount_point) @@ -103,9 +115,28 @@ pub fn mount(mount_point: &str, network_path: &str, mount_type: &str) { .output() .expect("Failed to execute mount command"); + sleep(Duration::from_secs(1)); + if output.status.success() { log("mount", "Filesystem mounted successfully.", LogLevel::Info); } else { + let stderr = String::from_utf8_lossy(&output.stderr); + // Häufige „schon gemountet“/Busy-Indikatoren tolerant behandeln + let already_or_busy = stderr.contains("already mounted") + || stderr.contains("is busy") + || stderr.contains("Device or resource busy") + || stderr.contains("EBUSY"); + + if already_or_busy { + log( + "mount", + "Filesystem appears to be already mounted or mountpoint is busy. Treating as no-op.", + LogLevel::Info, + ); + log("mount", &*stderr, LogLevel::Debug); + return; + } + log( "mount", &*format!( @@ -114,11 +145,7 @@ pub fn mount(mount_point: &str, network_path: &str, mount_type: &str) { ), LogLevel::Error, ); - log( - "mount", - &*String::from_utf8_lossy(&output.stderr), - LogLevel::Debug, - ); + log("mount", &*stderr, LogLevel::Debug); } } } @@ -128,6 +155,16 @@ pub fn mount(mount_point: &str, network_path: &str, mount_type: &str) { /// # Parameter /// * `mount_point` - Der lokale Verzeichnispfad, von dem das Dateisystem ausgehängt werden soll pub fn unmount(mount_point: &str) { + // 1) Globalen Write-Lock halten (blockiert parallele Statusabfragen) + let _fs_write = FS_MOUNT_GUARD + .get_or_init(|| RwLock::new(())) + .write() + .expect("mount rwlock poisoned"); + + // 2) Danach funktionsspezifischen Mutex sperren (konsistente Lock-Reihenfolge!) + let m = GUARD_UNMOUNT.get_or_init(|| Mutex::new(())); + let _lock = m.lock().expect("Mutex poisoned"); + #[cfg(target_os = "windows")] { let output = Command::new("Remove-PSDrive") diff --git a/src/filesystem/mounted.rs b/src/filesystem/mounted.rs index 7f87095..a3176f4 100644 --- a/src/filesystem/mounted.rs +++ b/src/filesystem/mounted.rs @@ -1,4 +1,14 @@ -use std::process::Command; +use std::fs::File; +use std::io::{BufRead, BufReader}; +use std::path::Path; +use std::sync::{OnceLock, RwLock}; + +// Globaler RW-Lock für Mount-Operationen und Statusabfragen +pub static FS_MOUNT_GUARD: OnceLock> = OnceLock::new(); + +fn guard() -> &'static RwLock<()> { + FS_MOUNT_GUARD.get_or_init(|| RwLock::new(())) +} /// Überprüft, ob ein Dateisystem am angegebenen Mount-Point mit dem spezifizierten Mount-Typ eingebunden ist. /// @@ -10,13 +20,20 @@ use std::process::Command; /// * `true` wenn das Dateisystem mit dem angegebenen Typ eingebunden ist /// * `false` wenn das Dateisystem nicht oder mit einem anderen Typ eingebunden ist pub fn is_mounted_as(mount_point: &str, mount_type: &str) -> bool { + // Während Statusabfragen nur Read-Lock halten + let _read_guard = guard().read().expect("mount rwlock poisoned"); + #[cfg(target_os = "windows")] { + use std::process::Command; let drive_letter = &mount_point[0..1]; let output = Command::new("powershell") .args([ "-Command", - &format!("(Get-PSDrive -Name {} -PSProvider 'FileSystem').Description", drive_letter) + &format!( + "(Get-PSDrive -Name {} -PSProvider 'FileSystem').Description", + drive_letter + ), ]) .output() .expect("Failed to execute get-psdrive command"); @@ -31,19 +48,18 @@ pub fn is_mounted_as(mount_point: &str, mount_type: &str) -> bool { #[cfg(not(target_os = "windows"))] { - let output = Command::new("mount") - .output() - .expect("Failed to execute mount command"); + let norm_mp = normalize_mount_point(mount_point); - if !output.status.success() { - return false; + for entry in read_proc_mounts() { + let (mp, fstype, _opts, _src) = entry; + if mp == norm_mp { + if type_matches(mount_type, &fstype, &_opts) { + return true; + } + } } - let mount_output = String::from_utf8_lossy(&output.stdout); - - mount_output - .lines() - .any(|line| line.contains(mount_point) && line.contains(mount_type)) + false } } @@ -56,13 +72,20 @@ pub fn is_mounted_as(mount_point: &str, mount_type: &str) -> bool { /// * `true` wenn ein Dateisystem am angegebenen Pfad eingebunden ist /// * `false` wenn kein Dateisystem eingebunden ist pub fn is_mounted(mount_point: &str) -> bool { + // Während Statusabfragen nur Read-Lock halten + let _read_guard = guard().read().expect("mount rwlock poisoned"); + #[cfg(target_os = "windows")] { + use std::process::Command; let drive_letter = &mount_point[0..1]; let output = Command::new("powershell") .args([ "-Command", - &format!("(Get-PSDrive -Name {} -PSProvider 'FileSystem')", drive_letter) + &format!( + "(Get-PSDrive -Name {} -PSProvider 'FileSystem')", + drive_letter + ), ]) .output() .expect("Failed to execute get-psdrive command"); @@ -72,16 +95,73 @@ pub fn is_mounted(mount_point: &str) -> bool { #[cfg(not(target_os = "windows"))] { - let output = Command::new("mount") - .output() - .expect("Failed to execute mount command"); - - if !output.status.success() { - return false; - } - - let mount_output = String::from_utf8_lossy(&output.stdout); - - mount_output.lines().any(|line| line.contains(mount_point)) + let norm_mp = normalize_mount_point(mount_point); + read_proc_mounts() + .into_iter() + .any(|(mp, _, _, _)| mp == norm_mp) + } +} + +#[cfg(not(target_os = "windows"))] +fn read_proc_mounts() -> Vec<(String, String, String, String)> { + // Liefert Tupel: (mount_point, fstype, options, source) + let file = match File::open("/proc/mounts") { + Ok(f) => f, + Err(_) => return Vec::new(), + }; + let reader = BufReader::new(file); + let mut result = Vec::new(); + + for line in reader.lines().flatten() { + // Format /proc/mounts: + // fs_spec fs_file fs_vfstype fs_mntops fs_freq fs_passno + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() < 6 { + continue; + } + let fs_spec = parts[0].to_string(); + let fs_file = parts[1].to_string(); + let fs_vfstype = parts[2].to_string(); + let fs_mntops = parts[3].to_string(); + + result.push((fs_file, fs_vfstype, fs_mntops, fs_spec)); + } + + result +} + +#[cfg(not(target_os = "windows"))] +fn normalize_mount_point(p: &str) -> String { + // Entfernt redundante Slashes am Ende (außer bei "/") und canonicalized soweit möglich. + if p == "/" { + return "/".to_string(); + } + let trimmed = p.trim_end_matches('/'); + // Versuche, realpath zu bilden, falle sonst auf trimmed zurück + let path = Path::new(trimmed); + path.canonicalize() + .map(|p| p.to_string_lossy().to_string()) + .unwrap_or_else(|_| trimmed.to_string()) +} + +#[cfg(not(target_os = "windows"))] +fn type_matches(expected: &str, actual_fstype: &str, options: &str) -> bool { + // Normalisiere erwartete Typfamilien + match expected { + // NFS kann als nfs oder nfs4 erscheinen + "nfs" | "nfs4" => actual_fstype == "nfs" || actual_fstype == "nfs4", + + // davfs/webdav erscheint häufig als fuse.davfs (oder fuse mit helper=davfs) + "webdav" | "davfs" | "davfs2" => { + actual_fstype == "fuse.davfs" + || actual_fstype == "davfs" + || (actual_fstype == "fuse" && options.contains("helper=davfs")) + } + + // CIFS/Samba Alias + "cifs" | "smb" | "smb3" => actual_fstype == "cifs" || actual_fstype == "smb3", + + // Fallback: exakter Vergleich + other => other == actual_fstype, } }