hexz_cli/cmd/vm/unmount.rs
1//! Unmounting of FUSE-mounted Hexz filesystems.
2//!
3//! This command safely detaches Hexz snapshots that were mounted as filesystems
4//! using the `mount` command. It uses platform-specific unmount tools and handles
5//! error cases gracefully.
6//!
7//! # Unmount Strategy
8//!
9//! The command tries unmount methods in order:
10//!
11//! 1. **Linux**: `fusermount -u` (preferred for FUSE mounts)
12//! 2. **Fallback**: `umount` (generic unmount tool)
13//!
14//! # Usage
15//!
16//! ```bash
17//! # Unmount a previously mounted snapshot
18//! hexz vm unmount /mnt/snapshot
19//! ```
20//!
21//! # Error Handling
22//!
23//! The command handles several cases gracefully:
24//! - **Not mounted**: Returns success (already unmounted)
25//! - **Busy**: Reports error if mountpoint is in use
26//! - **Permission denied**: Reports error if insufficient privileges
27//!
28//! # Safety
29//!
30//! - Does not modify the snapshot or overlay files
31//! - Safe to run even if already unmounted
32//! - Does not affect other mounts
33//!
34//! # Overlay Persistence
35//!
36//! If the mount was created with an overlay (`--overlay`), the overlay file
37//! remains intact after unmounting and can be used for future mounts or commits.
38
39use anyhow::{Context, Result};
40use std::path::PathBuf;
41use std::process::Command;
42
43/// Unmounts a previously mounted Hexz filesystem.
44///
45/// **Architectural intent:** Attempts to detach the FUSE mount using the
46/// platform-preferred tool (`fusermount -u` on Linux), falling back to
47/// `umount` when necessary.
48///
49/// **Constraints:** The `mountpoint` must refer to a valid mount; both
50/// commands rely on external system utilities and their presence on `$PATH`.
51///
52/// **Side effects:** Spawns subprocesses to invoke unmount operations and
53/// prints status messages to stdout; on failure, returns a descriptive error.
54pub fn run(mountpoint: PathBuf) -> Result<()> {
55 let path_str = mountpoint.to_string_lossy();
56
57 if cfg!(target_os = "linux")
58 && let Ok(output) = Command::new("fusermount")
59 .arg("-u")
60 .arg(&mountpoint)
61 .output()
62 {
63 if output.status.success() {
64 println!("Successfully unmounted {}", path_str);
65 return Ok(());
66 }
67
68 let stderr = String::from_utf8_lossy(&output.stderr);
69 if stderr.contains("not found") {
70 return Ok(());
71 }
72 }
73
74 let output = Command::new("umount")
75 .arg(&mountpoint)
76 .output()
77 .context("Failed to execute unmount command")?;
78
79 if output.status.success() {
80 println!("Successfully unmounted {}", path_str);
81 Ok(())
82 } else {
83 let stderr = String::from_utf8_lossy(&output.stderr);
84 if stderr.contains("not mounted") {
85 return Ok(());
86 }
87
88 eprint!("{}", stderr);
89 anyhow::bail!("Failed to unmount {}.", path_str);
90 }
91}