liblitho 0.2.0

cli tool to flash/clone the images to storage devices
Documentation
//! Linux volume unmount preparation.

use crate::devices::{format_device_busy_error, DeviceError};
use crate::platform::traits::{DeviceSafety, VolumeOps};
use std::process::Command;

use super::LinuxPlatform;

/// No long-lived lock on Linux after unmount.
pub type LinuxVolumeGuard = ();

impl VolumeOps for LinuxPlatform {
    type Guard = LinuxVolumeGuard;

    fn prepare_for_io(device_path: &str, auto_unmount: bool) -> Result<Self::Guard, DeviceError> {
        let mounts = LinuxPlatform::list_mounts(device_path)?;
        if mounts.is_empty() {
            return Ok(());
        }
        if !auto_unmount {
            return Err(DeviceError::busy(format_device_busy_error(
                device_path, &mounts,
            )));
        }

        Self::unmount_volumes(device_path)?;

        let remaining = LinuxPlatform::list_mounts(device_path)?;
        if !remaining.is_empty() {
            return Err(DeviceError::query(format!(
                "Could not unmount all volumes on {device_path}: {}",
                remaining
                    .iter()
                    .map(|m| format!("{} on {}", m.source, m.mount_point))
                    .collect::<Vec<_>>()
                    .join(", ")
            )));
        }

        Ok(())
    }

    fn unmount_volumes(device_path: &str) -> Result<Vec<String>, DeviceError> {
        let mounts = LinuxPlatform::list_mounts(device_path)?;
        if mounts.is_empty() {
            return Ok(Vec::new());
        }

        let mut unmounted = Vec::new();
        let mut failures = Vec::new();

        for mount in mounts {
            match umount_partition(&mount.source) {
                Ok(()) => unmounted.push(format!("{} on {}", mount.source, mount.mount_point)),
                Err(err) => failures.push(format!("{}: {err}", mount.source)),
            }
        }

        if !failures.is_empty() {
            let mut message = format!("Could not unmount all volumes on {device_path}");
            if !unmounted.is_empty() {
                message.push_str(&format!(" (unmounted: {})", unmounted.join(", ")));
            }
            message.push_str(&format!(". Still busy: {}", failures.join("; ")));
            return Err(DeviceError::unmount(message));
        }

        Ok(unmounted)
    }
}

fn umount_partition(source: &str) -> Result<(), DeviceError> {
    let output = Command::new("umount")
        .arg(source)
        .output()
        .map_err(|e| DeviceError::query(format!("Failed to run umount: {e}")))?;

    if output.status.success() {
        return Ok(());
    }

    let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
    Err(DeviceError::unmount(if stderr.is_empty() {
        format!("umount {source} failed (exit {:?})", output.status.code())
    } else {
        format!("umount {source}: {stderr}")
    }))
}