diff --git a/project/libfuse-fs/examples/overlayfs_example.rs b/project/libfuse-fs/examples/overlayfs_example.rs index 7a8160e43..957cbe7ca 100644 --- a/project/libfuse-fs/examples/overlayfs_example.rs +++ b/project/libfuse-fs/examples/overlayfs_example.rs @@ -5,8 +5,9 @@ use clap::Parser; use libfuse_fs::overlayfs::{OverlayArgs, mount_fs}; +use libfuse_fs::util::bind_mount::{BindMount, BindMountManager}; use tokio::signal; -use tracing::debug; +use tracing::{debug, info, error}; #[derive(Parser, Debug)] #[command(author, version, about = "OverlayFS example for integration tests")] @@ -28,6 +29,9 @@ struct Args { mapping: Option, #[arg(long)] allow_other: bool, + /// Bind mounts in format "source:target" (repeatable) + #[arg(long = "bind")] + bind_mounts: Vec, } fn set_log() { @@ -44,9 +48,27 @@ async fn main() { set_log(); debug!("Starting overlay filesystem with args: {:?}", args); + // Parse bind mounts + let bind_specs: Result, _> = args + .bind_mounts + .iter() + .map(|s| BindMount::parse(s)) + .collect(); + + let bind_specs = match bind_specs { + Ok(specs) => specs, + Err(e) => { + error!("Failed to parse bind mount specifications: {}", e); + std::process::exit(1); + } + }; + + // Create bind mount manager + let bind_manager = BindMountManager::new(&args.mountpoint); + let mut mount_handle = mount_fs(OverlayArgs { name: None::, - mountpoint: args.mountpoint, + mountpoint: args.mountpoint.clone(), lowerdir: args.lowerdir, upperdir: args.upperdir, mapping: args.mapping, @@ -55,11 +77,58 @@ async fn main() { }) .await; - let handle = &mut mount_handle; + // Mount bind mounts after the overlay filesystem is mounted + if !bind_specs.is_empty() { + info!("Cleaning up any existing bind mounts from previous runs..."); + if let Err(e) = bind_manager.cleanup_existing_mounts(&bind_specs).await { + error!("Failed to cleanup existing mounts: {}", e); + } + + info!("Mounting {} bind mount(s)", bind_specs.len()); + if let Err(e) = bind_manager.mount_all(&bind_specs).await { + error!("Failed to mount bind mounts: {}", e); + // Unmount the overlay filesystem + mount_handle.unmount().await.unwrap(); + std::process::exit(1); + } + } + tokio::select! { - res = handle => res.unwrap(), + res = &mut mount_handle => { + if let Err(e) = res { + error!("Overlay filesystem error: {:?}", e); + } + info!("Cleaning up..."); + // Unmount bind mounts first + if let Err(e) = bind_manager.unmount_all().await { + error!("Failed to unmount bind mounts: {}", e); + } + }, _ = signal::ctrl_c() => { - mount_handle.unmount().await.unwrap(); + info!("Received SIGINT signal, cleaning up..."); + // Unmount bind mounts first + if let Err(e) = bind_manager.unmount_all().await { + error!("Failed to unmount bind mounts: {}", e); + } + // Then unmount the overlay filesystem + if let Err(e) = mount_handle.unmount().await { + error!("Failed to unmount overlay filesystem: {}", e); + } + } + _ = async { + use tokio::signal::unix::{signal, SignalKind}; + let mut term = signal(SignalKind::terminate()).expect("Failed to setup SIGTERM handler"); + term.recv().await + } => { + info!("Received SIGTERM signal, cleaning up..."); + // Unmount bind mounts first + if let Err(e) = bind_manager.unmount_all().await { + error!("Failed to unmount bind mounts: {}", e); + } + // Then unmount the overlay filesystem + if let Err(e) = mount_handle.unmount().await { + error!("Failed to unmount overlay filesystem: {}", e); + } } } } diff --git a/project/libfuse-fs/examples/passthrough.rs b/project/libfuse-fs/examples/passthrough.rs index bfb50c132..062fbc5bb 100644 --- a/project/libfuse-fs/examples/passthrough.rs +++ b/project/libfuse-fs/examples/passthrough.rs @@ -6,10 +6,11 @@ use clap::Parser; use libfuse_fs::passthrough::{ PassthroughArgs, new_passthroughfs_layer, newlogfs::LoggingFileSystem, }; +use libfuse_fs::util::bind_mount::{BindMount, BindMountManager}; use rfuse3::{MountOptions, raw::Session}; use std::ffi::OsString; use tokio::signal; -use tracing::debug; +use tracing::{debug, info, error}; #[derive(Parser, Debug)] #[command( @@ -32,6 +33,9 @@ struct Args { options: Option, #[arg(long)] allow_other: bool, + /// Bind mounts in format "source:target" (repeatable) + #[arg(long = "bind")] + bind_mounts: Vec, } fn set_log() { @@ -48,6 +52,24 @@ async fn main() { set_log(); debug!("Starting passthrough filesystem with args: {:?}", args); + // Parse bind mounts + let bind_specs: Result, _> = args + .bind_mounts + .iter() + .map(|s| BindMount::parse(s)) + .collect(); + + let bind_specs = match bind_specs { + Ok(specs) => specs, + Err(e) => { + error!("Failed to parse bind mount specifications: {}", e); + std::process::exit(1); + } + }; + + // Create bind mount manager + let bind_manager = BindMountManager::new(&args.mountpoint); + let fs = new_passthroughfs_layer(PassthroughArgs { root_dir: args.rootdir, mapping: args.options, @@ -81,11 +103,58 @@ async fn main() { .expect("Privileged mount failed") }; - let handle = &mut mount_handle; + // Mount bind mounts after the passthrough filesystem is mounted + if !bind_specs.is_empty() { + info!("Cleaning up any existing bind mounts from previous runs..."); + if let Err(e) = bind_manager.cleanup_existing_mounts(&bind_specs).await { + error!("Failed to cleanup existing mounts: {}", e); + } + + info!("Mounting {} bind mount(s)", bind_specs.len()); + if let Err(e) = bind_manager.mount_all(&bind_specs).await { + error!("Failed to mount bind mounts: {}", e); + // Unmount the passthrough filesystem + mount_handle.unmount().await.unwrap(); + std::process::exit(1); + } + } + tokio::select! { - res = handle => res.unwrap(), + res = &mut mount_handle => { + if let Err(e) = res { + error!("Passthrough filesystem error: {:?}", e); + } + info!("Cleaning up..."); + // Unmount bind mounts first + if let Err(e) = bind_manager.unmount_all().await { + error!("Failed to unmount bind mounts: {}", e); + } + }, _ = signal::ctrl_c() => { - mount_handle.unmount().await.unwrap(); + info!("Received SIGINT signal, cleaning up..."); + // Unmount bind mounts first + if let Err(e) = bind_manager.unmount_all().await { + error!("Failed to unmount bind mounts: {}", e); + } + // Then unmount the passthrough filesystem + if let Err(e) = mount_handle.unmount().await { + error!("Failed to unmount passthrough filesystem: {}", e); + } + } + _ = async { + use tokio::signal::unix::{signal, SignalKind}; + let mut term = signal(SignalKind::terminate()).expect("Failed to setup SIGTERM handler"); + term.recv().await + } => { + info!("Received SIGTERM signal, cleaning up..."); + // Unmount bind mounts first + if let Err(e) = bind_manager.unmount_all().await { + error!("Failed to unmount bind mounts: {}", e); + } + // Then unmount the passthrough filesystem + if let Err(e) = mount_handle.unmount().await { + error!("Failed to unmount passthrough filesystem: {}", e); + } } } } diff --git a/project/libfuse-fs/src/lib.rs b/project/libfuse-fs/src/lib.rs index 708337eb5..ad816bbc2 100644 --- a/project/libfuse-fs/src/lib.rs +++ b/project/libfuse-fs/src/lib.rs @@ -6,7 +6,7 @@ pub mod overlayfs; pub mod passthrough; mod server; pub mod unionfs; -mod util; +pub mod util; // Test utilities (only compiled during tests) #[cfg(test)] diff --git a/project/libfuse-fs/src/util/bind_mount.rs b/project/libfuse-fs/src/util/bind_mount.rs new file mode 100644 index 000000000..7b8d8225a --- /dev/null +++ b/project/libfuse-fs/src/util/bind_mount.rs @@ -0,0 +1,276 @@ +// Copyright (C) 2024 rk8s authors +// SPDX-License-Identifier: MIT OR Apache-2.0 +//! Bind mount utilities for container volume management + +use std::io::{Error, Result}; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use tokio::sync::Mutex; +use tracing::{debug, error, info}; + +/// Represents a single bind mount +#[derive(Debug, Clone)] +pub struct BindMount { + /// Source path on host + pub source: PathBuf, + /// Target path relative to mount point + pub target: PathBuf, +} + +impl BindMount { + /// Parse a bind mount specification like "proc:/proc" or "/host/path:/container/path" + pub fn parse(spec: &str) -> Result { + let parts: Vec<&str> = spec.split(':').collect(); + if parts.len() != 2 { + return Err(Error::other(format!( + "Invalid bind mount spec: '{}'. Expected format: 'source:target'", + spec + ))); + } + + let source = PathBuf::from(parts[0]); + let target = PathBuf::from(parts[1]); + + // Convert relative source paths to absolute from root + let source = if source.is_relative() { + PathBuf::from("/").join(source) + } else { + source + }; + + Ok(BindMount { source, target }) + } +} + +/// Manages multiple bind mounts with automatic cleanup +pub struct BindMountManager { + mounts: Arc>>, + mountpoint: PathBuf, +} + +#[derive(Debug)] +struct MountPoint { + target: PathBuf, + mounted: bool, +} + +impl BindMountManager { + /// Create a new bind mount manager + pub fn new>(mountpoint: P) -> Self { + Self { + mounts: Arc::new(Mutex::new(Vec::new())), + mountpoint: mountpoint.as_ref().to_path_buf(), + } + } + + /// Check if a path is currently mounted + fn is_mounted(&self, path: &Path) -> bool { + use std::fs::read_to_string; + + if let Ok(mounts) = read_to_string("/proc/mounts") { + let path_str = path.to_string_lossy(); + for line in mounts.lines() { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 2 && parts[1] == path_str { + return true; + } + } + } + false + } + + /// Clean up any existing mounts at the target paths before mounting + /// This ensures we start with a clean state even if previous run didn't clean up properly + pub async fn cleanup_existing_mounts(&self, bind_specs: &[BindMount]) -> Result<()> { + debug!("Checking for existing mounts to clean up..."); + + for bind in bind_specs { + let target_path = self.mountpoint.join(bind.target.strip_prefix("/").unwrap_or(&bind.target)); + + if self.is_mounted(&target_path) { + info!("Found existing mount at {:?}, cleaning up...", target_path); + if let Err(e) = self.do_unmount(&target_path) { + // Log but don't fail - we'll try to mount anyway + error!("Failed to cleanup existing mount {:?}: {}", target_path, e); + } else { + info!("Cleaned up existing mount at {:?}", target_path); + } + } + } + + Ok(()) + } + + /// Mount all bind mounts + pub async fn mount_all(&self, bind_specs: &[BindMount]) -> Result<()> { + let mut mounts = self.mounts.lock().await; + + for bind in bind_specs { + let target_path = self.mountpoint.join(bind.target.strip_prefix("/").unwrap_or(&bind.target)); + + // Check if already mounted (skip if so) + if self.is_mounted(&target_path) { + info!("Target {:?} is already mounted, skipping", target_path); + continue; + } + + // Check if source is a file or directory + let source_metadata = std::fs::metadata(&bind.source)?; + + if !target_path.exists() { + if source_metadata.is_file() { + // For file bind mounts, create parent directory and an empty file + if let Some(parent) = target_path.parent() { + std::fs::create_dir_all(parent)?; + debug!("Created parent directory: {:?}", parent); + } + std::fs::File::create(&target_path)?; + debug!("Created target file: {:?}", target_path); + } else { + // For directory bind mounts, create the directory + std::fs::create_dir_all(&target_path)?; + debug!("Created target directory: {:?}", target_path); + } + } + + // Perform the bind mount + self.do_mount(&bind.source, &target_path)?; + + mounts.push(MountPoint { + target: target_path.clone(), + mounted: true, + }); + + info!("Bind mounted {:?} -> {:?}", bind.source, target_path); + } + + Ok(()) + } + + /// Perform the actual bind mount using mount(2) syscall + fn do_mount(&self, source: &Path, target: &Path) -> Result<()> { + use std::ffi::CString; + + let source_cstr = CString::new(source.to_str().ok_or_else(|| { + Error::other(format!("Invalid source path: {:?}", source)) + })?) + .map_err(|e| Error::other(format!("CString error: {}", e)))?; + + let target_cstr = CString::new(target.to_str().ok_or_else(|| { + Error::other(format!("Invalid target path: {:?}", target)) + })?) + .map_err(|e| Error::other(format!("CString error: {}", e)))?; + + let fstype = CString::new("none").unwrap(); + + let ret = unsafe { + libc::mount( + source_cstr.as_ptr(), + target_cstr.as_ptr(), + fstype.as_ptr(), + libc::MS_BIND | libc::MS_REC, + std::ptr::null(), + ) + }; + + if ret != 0 { + let err = Error::last_os_error(); + error!("Failed to bind mount {:?} to {:?}: {}", source, target, err); + return Err(err); + } + + Ok(()) + } + + /// Unmount all bind mounts + pub async fn unmount_all(&self) -> Result<()> { + let mut mounts = self.mounts.lock().await; + let mut errors = Vec::new(); + + // Unmount in reverse order + while let Some(mut mount) = mounts.pop() { + if mount.mounted { + if let Err(e) = self.do_unmount(&mount.target) { + error!("Failed to unmount {:?}: {}", mount.target, e); + errors.push(e); + } else { + mount.mounted = false; + info!("Unmounted {:?}", mount.target); + } + } + } + + if !errors.is_empty() { + return Err(Error::other(format!( + "Failed to unmount {} bind mounts", + errors.len() + ))); + } + + Ok(()) + } + + /// Perform the actual unmount using umount(2) syscall + fn do_unmount(&self, target: &Path) -> Result<()> { + use std::ffi::CString; + + let target_cstr = CString::new(target.to_str().ok_or_else(|| { + Error::other(format!("Invalid target path: {:?}", target)) + })?) + .map_err(|e| Error::other(format!("CString error: {}", e)))?; + + let ret = unsafe { libc::umount2(target_cstr.as_ptr(), libc::MNT_DETACH) }; + + if ret != 0 { + let err = Error::last_os_error(); + // EINVAL or ENOENT might mean it's already unmounted + if err.raw_os_error() != Some(libc::EINVAL) + && err.raw_os_error() != Some(libc::ENOENT) + { + return Err(err); + } + } + + Ok(()) + } +} + +impl Drop for BindMountManager { + fn drop(&mut self) { + // Attempt to clean up on drop (synchronously) + let mounts = self.mounts.try_lock(); + if let Ok(mut mounts) = mounts { + while let Some(mount) = mounts.pop() { + if mount.mounted { + let _ = self.do_unmount(&mount.target); + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_bind_mount() { + let bind = BindMount::parse("proc:/proc").unwrap(); + assert_eq!(bind.source, PathBuf::from("/proc")); + assert_eq!(bind.target, PathBuf::from("/proc")); + + let bind = BindMount::parse("/host/path:/container/path").unwrap(); + assert_eq!(bind.source, PathBuf::from("/host/path")); + assert_eq!(bind.target, PathBuf::from("/container/path")); + + let bind = BindMount::parse("sys:/sys").unwrap(); + assert_eq!(bind.source, PathBuf::from("/sys")); + assert_eq!(bind.target, PathBuf::from("/sys")); + } + + #[test] + fn test_invalid_bind_mount() { + assert!(BindMount::parse("invalid").is_err()); + assert!(BindMount::parse("too:many:colons").is_err()); + } +} diff --git a/project/libfuse-fs/src/util/mod.rs b/project/libfuse-fs/src/util/mod.rs index de756d5b3..a98369faf 100644 --- a/project/libfuse-fs/src/util/mod.rs +++ b/project/libfuse-fs/src/util/mod.rs @@ -1,3 +1,4 @@ +pub mod bind_mount; pub mod mapping; pub mod open_options; diff --git a/project/libfuse-fs/tests/bind_overlay_test.sh b/project/libfuse-fs/tests/bind_overlay_test.sh new file mode 100755 index 000000000..c2bbbacf3 --- /dev/null +++ b/project/libfuse-fs/tests/bind_overlay_test.sh @@ -0,0 +1,194 @@ +#!/bin/bash +# Copyright (C) 2024 rk8s authors +# SPDX-License-Identifier: MIT OR Apache-2.0 +# Test bind mount support in overlay filesystem + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${GREEN}=== Testing OverlayFS with Bind Mounts ===${NC}" + +# Check if running as root +if [ "$EUID" -ne 0 ]; then + echo -e "${RED}ERROR: This test requires root privileges${NC}" + exit 1 +fi + +# Setup test directories +TEST_DIR="/tmp/overlay_bind_test_$$" +UPPER_DIR="$TEST_DIR/upper" +LOWER_DIR="$TEST_DIR/lower" +MOUNT_POINT="$TEST_DIR/merged" +WORK_DIR="$TEST_DIR/work" + +cleanup() { + echo -e "${YELLOW}Cleaning up...${NC}" + + # Try to unmount bind mounts + for mount in "$MOUNT_POINT/proc" "$MOUNT_POINT/sys" "$MOUNT_POINT/dev/pts" "$MOUNT_POINT/dev"; do + if mountpoint -q "$mount" 2>/dev/null; then + echo "Unmounting $mount" + umount -l "$mount" 2>/dev/null || true + fi + done + + # Unmount the overlay + if mountpoint -q "$MOUNT_POINT" 2>/dev/null; then + echo "Unmounting $MOUNT_POINT" + fusermount -u "$MOUNT_POINT" 2>/dev/null || umount -l "$MOUNT_POINT" 2>/dev/null || true + sleep 1 + fi + + # Remove test directories + rm -rf "$TEST_DIR" + echo -e "${GREEN}Cleanup completed${NC}" +} + +trap cleanup EXIT INT TERM + +# Create test directories +mkdir -p "$UPPER_DIR" "$LOWER_DIR" "$MOUNT_POINT" "$WORK_DIR" +# Don't create subdirectories - they will be created by bind mount manager + +# Create some test files in lower layer +echo "test content" > "$LOWER_DIR/test.txt" +mkdir -p "$LOWER_DIR/testdir" +echo "lower file" > "$LOWER_DIR/testdir/file.txt" + +# Find the binary +BINARY="${CARGO_TARGET_DIR:-../target}/debug/examples/overlayfs_example" +if [ ! -f "$BINARY" ]; then + BINARY="target/debug/examples/overlayfs_example" +fi + +if [ ! -f "$BINARY" ]; then + echo -e "${RED}ERROR: Cannot find overlayfs_example binary${NC}" + echo "Expected at: $BINARY" + exit 1 +fi + +echo -e "${GREEN}Found binary: $BINARY${NC}" + +# Start the filesystem with bind mounts in background +echo -e "${YELLOW}Starting OverlayFS with bind mounts...${NC}" +"$BINARY" \ + --mountpoint "$MOUNT_POINT" \ + --upperdir "$UPPER_DIR" \ + --lowerdir "$LOWER_DIR" \ + --bind "proc:/proc" \ + --bind "sys:/sys" \ + --bind "dev:/dev" \ + --bind "dev/pts:/dev/pts" \ + --privileged \ + --allow-other & + +FS_PID=$! +echo "Filesystem PID: $FS_PID" + +# Wait for mount to be ready +sleep 2 + +# Check if process is still running +if ! kill -0 $FS_PID 2>/dev/null; then + echo -e "${RED}ERROR: Filesystem process died${NC}" + wait $FS_PID + exit 1 +fi + +# Verify mount point is mounted +if ! mountpoint -q "$MOUNT_POINT"; then + echo -e "${RED}ERROR: Mount point not mounted${NC}" + kill $FS_PID 2>/dev/null || true + exit 1 +fi + +echo -e "${GREEN}✓ Filesystem mounted${NC}" + +# Test 1: Verify bind mounts are present +echo -e "${YELLOW}Test 1: Checking bind mounts...${NC}" + +if mountpoint -q "$MOUNT_POINT/proc"; then + echo -e "${GREEN}✓ /proc is bind mounted${NC}" +else + echo -e "${RED}✗ /proc is NOT bind mounted${NC}" + kill $FS_PID 2>/dev/null || true + exit 1 +fi + +if mountpoint -q "$MOUNT_POINT/sys"; then + echo -e "${GREEN}✓ /sys is bind mounted${NC}" +else + echo -e "${RED}✗ /sys is NOT bind mounted${NC}" + kill $FS_PID 2>/dev/null || true + exit 1 +fi + +if mountpoint -q "$MOUNT_POINT/dev"; then + echo -e "${GREEN}✓ /dev is bind mounted${NC}" +else + echo -e "${RED}✗ /dev is NOT bind mounted${NC}" + kill $FS_PID 2>/dev/null || true + exit 1 +fi + +# Test 2: Verify we can read from bind mounts +echo -e "${YELLOW}Test 2: Reading from bind mounts...${NC}" + +if [ -f "$MOUNT_POINT/proc/version" ]; then + PROC_VERSION=$(cat "$MOUNT_POINT/proc/version") + echo -e "${GREEN}✓ Can read /proc/version: ${PROC_VERSION:0:50}...${NC}" +else + echo -e "${RED}✗ Cannot read /proc/version${NC}" + kill $FS_PID 2>/dev/null || true + exit 1 +fi + +# Test 3: Verify overlay functionality still works +echo -e "${YELLOW}Test 3: Testing overlay functionality...${NC}" + +if [ -f "$MOUNT_POINT/test.txt" ]; then + echo -e "${GREEN}✓ Can see lower layer files${NC}" +else + echo -e "${RED}✗ Cannot see lower layer files${NC}" + kill $FS_PID 2>/dev/null || true + exit 1 +fi + +# Write to a file (should go to upper layer) +echo "new content" > "$MOUNT_POINT/newfile.txt" +if [ -f "$UPPER_DIR/newfile.txt" ]; then + echo -e "${GREEN}✓ Writes go to upper layer${NC}" +else + echo -e "${RED}✗ Writes not going to upper layer${NC}" + kill $FS_PID 2>/dev/null || true + exit 1 +fi + +# Test 4: Test graceful shutdown +echo -e "${YELLOW}Test 4: Testing graceful shutdown...${NC}" +kill -TERM $FS_PID +wait $FS_PID 2>/dev/null || true +sleep 1 + +# Verify bind mounts were unmounted +UNMOUNTED=true +for mount in "$MOUNT_POINT/proc" "$MOUNT_POINT/sys" "$MOUNT_POINT/dev"; do + if mountpoint -q "$mount" 2>/dev/null; then + echo -e "${RED}✗ $mount still mounted after shutdown${NC}" + UNMOUNTED=false + fi +done + +if [ "$UNMOUNTED" = true ]; then + echo -e "${GREEN}✓ All bind mounts cleaned up${NC}" +else + echo -e "${RED}✗ Some bind mounts not cleaned up${NC}" + exit 1 +fi + +echo -e "${GREEN}=== All tests passed! ===${NC}" diff --git a/project/libfuse-fs/tests/bind_passthrough_test.sh b/project/libfuse-fs/tests/bind_passthrough_test.sh new file mode 100755 index 000000000..eb91318fc --- /dev/null +++ b/project/libfuse-fs/tests/bind_passthrough_test.sh @@ -0,0 +1,198 @@ +#!/bin/bash +# Copyright (C) 2024 rk8s authors +# SPDX-License-Identifier: MIT OR Apache-2.0 +# Test bind mount support in passthrough filesystem + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${GREEN}=== Testing PassthroughFS with Bind Mounts ===${NC}" + +# Check if running as root +if [ "$EUID" -ne 0 ]; then + echo -e "${RED}ERROR: This test requires root privileges${NC}" + exit 1 +fi + +# Setup test directories +TEST_DIR="/tmp/passthrough_bind_test_$$" +ROOT_DIR="$TEST_DIR/root" +MOUNT_POINT="$TEST_DIR/merged" + +cleanup() { + echo -e "${YELLOW}Cleaning up...${NC}" + + # Try to unmount bind mounts + for mount in "$MOUNT_POINT/proc" "$MOUNT_POINT/sys" "$MOUNT_POINT/dev/pts" "$MOUNT_POINT/dev"; do + if mountpoint -q "$mount" 2>/dev/null; then + echo "Unmounting $mount" + umount -l "$mount" 2>/dev/null || true + fi + done + + # Unmount the passthrough filesystem + if mountpoint -q "$MOUNT_POINT" 2>/dev/null; then + echo "Unmounting $MOUNT_POINT" + fusermount -u "$MOUNT_POINT" 2>/dev/null || umount -l "$MOUNT_POINT" 2>/dev/null || true + sleep 1 + fi + + # Remove test directories + rm -rf "$TEST_DIR" + echo -e "${GREEN}Cleanup completed${NC}" +} + +trap cleanup EXIT INT TERM + +# Create test directories +mkdir -p "$ROOT_DIR" "$MOUNT_POINT" +# Don't create subdirectories - they will be created by bind mount manager + +# Create some test files +echo "test content" > "$ROOT_DIR/test.txt" +mkdir -p "$ROOT_DIR/testdir" +echo "test file" > "$ROOT_DIR/testdir/file.txt" + +# Find the binary +BINARY="${CARGO_TARGET_DIR:-../target}/debug/examples/passthrough" +if [ ! -f "$BINARY" ]; then + BINARY="target/debug/examples/passthrough" +fi + +if [ ! -f "$BINARY" ]; then + echo -e "${RED}ERROR: Cannot find passthrough binary${NC}" + echo "Expected at: $BINARY" + exit 1 +fi + +echo -e "${GREEN}Found binary: $BINARY${NC}" + +# Start the filesystem with bind mounts in background +echo -e "${YELLOW}Starting PassthroughFS with bind mounts...${NC}" +"$BINARY" \ + --mountpoint "$MOUNT_POINT" \ + --rootdir "$ROOT_DIR" \ + --bind "proc:/proc" \ + --bind "sys:/sys" \ + --bind "dev:/dev" \ + --bind "dev/pts:/dev/pts" \ + --privileged \ + --allow-other & + +FS_PID=$! +echo "Filesystem PID: $FS_PID" + +# Wait for mount to be ready +sleep 2 + +# Check if process is still running +if ! kill -0 $FS_PID 2>/dev/null; then + echo -e "${RED}ERROR: Filesystem process died${NC}" + wait $FS_PID + exit 1 +fi + +# Verify mount point is mounted +if ! mountpoint -q "$MOUNT_POINT"; then + echo -e "${RED}ERROR: Mount point not mounted${NC}" + kill $FS_PID 2>/dev/null || true + exit 1 +fi + +echo -e "${GREEN}✓ Filesystem mounted${NC}" + +# Test 1: Verify bind mounts are present +echo -e "${YELLOW}Test 1: Checking bind mounts...${NC}" + +if mountpoint -q "$MOUNT_POINT/proc"; then + echo -e "${GREEN}✓ /proc is bind mounted${NC}" +else + echo -e "${RED}✗ /proc is NOT bind mounted${NC}" + kill $FS_PID 2>/dev/null || true + exit 1 +fi + +if mountpoint -q "$MOUNT_POINT/sys"; then + echo -e "${GREEN}✓ /sys is bind mounted${NC}" +else + echo -e "${RED}✗ /sys is NOT bind mounted${NC}" + kill $FS_PID 2>/dev/null || true + exit 1 +fi + +if mountpoint -q "$MOUNT_POINT/dev"; then + echo -e "${GREEN}✓ /dev is bind mounted${NC}" +else + echo -e "${RED}✗ /dev is NOT bind mounted${NC}" + kill $FS_PID 2>/dev/null || true + exit 1 +fi + +# Test 2: Verify we can read from bind mounts +echo -e "${YELLOW}Test 2: Reading from bind mounts...${NC}" + +if [ -f "$MOUNT_POINT/proc/version" ]; then + PROC_VERSION=$(cat "$MOUNT_POINT/proc/version") + echo -e "${GREEN}✓ Can read /proc/version: ${PROC_VERSION:0:50}...${NC}" +else + echo -e "${RED}✗ Cannot read /proc/version${NC}" + kill $FS_PID 2>/dev/null || true + exit 1 +fi + +# Test 3: Verify passthrough functionality still works +echo -e "${YELLOW}Test 3: Testing passthrough functionality...${NC}" + +if [ -f "$MOUNT_POINT/test.txt" ]; then + CONTENT=$(cat "$MOUNT_POINT/test.txt") + if [ "$CONTENT" = "test content" ]; then + echo -e "${GREEN}✓ Can read root directory files${NC}" + else + echo -e "${RED}✗ Content mismatch${NC}" + kill $FS_PID 2>/dev/null || true + exit 1 + fi +else + echo -e "${RED}✗ Cannot see root directory files${NC}" + kill $FS_PID 2>/dev/null || true + exit 1 +fi + +# Write to a file +echo "new content" > "$MOUNT_POINT/newfile.txt" +if [ -f "$ROOT_DIR/newfile.txt" ]; then + echo -e "${GREEN}✓ Writes work correctly${NC}" +else + echo -e "${RED}✗ Writes not working${NC}" + kill $FS_PID 2>/dev/null || true + exit 1 +fi + +# Test 4: Test graceful shutdown +echo -e "${YELLOW}Test 4: Testing graceful shutdown...${NC}" +kill -TERM $FS_PID +wait $FS_PID 2>/dev/null || true +sleep 1 + +# Verify bind mounts were unmounted +UNMOUNTED=true +for mount in "$MOUNT_POINT/proc" "$MOUNT_POINT/sys" "$MOUNT_POINT/dev"; do + if mountpoint -q "$mount" 2>/dev/null; then + echo -e "${RED}✗ $mount still mounted after shutdown${NC}" + UNMOUNTED=false + fi +done + +if [ "$UNMOUNTED" = true ]; then + echo -e "${GREEN}✓ All bind mounts cleaned up${NC}" +else + echo -e "${RED}✗ Some bind mounts not cleaned up${NC}" + exit 1 +fi + +echo -e "${GREEN}=== All tests passed! ===${NC}"