pub mod nfs; use clap::Parser; use furumi_client_core::FurumiClient; use nfsserve::tcp::{NFSTcp, NFSTcpListener}; use std::path::PathBuf; use std::process::Command; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use tracing::info; #[derive(Parser, Debug)] #[command(version, about = "Furumi-ng: mount remote filesystem via encrypted gRPC + NFS (macOS)")] struct Args { /// Server address (IP:PORT, will use https:// by default unless --no-tls is specified) #[arg(short, long, env = "FURUMI_SERVER", default_value = "0.0.0.0:50051")] server: String, /// Authentication Bearer token (leave empty if auth is disabled on server) #[arg(short, long, env = "FURUMI_TOKEN", default_value = "")] token: String, /// Mount point directory #[arg(short, long, env = "FURUMI_MOUNT")] mount: PathBuf, /// Disable TLS encryption #[arg(long, default_value_t = false)] no_tls: bool, } fn main() -> Result<(), Box> { tracing_subscriber::fmt::init(); let args = Args::parse(); if !args.mount.exists() || !args.mount.is_dir() { eprintln!( "Error: Mount point {:?} does not exist or is not a directory", args.mount ); std::process::exit(1); } let rt = tokio::runtime::Builder::new_multi_thread() .enable_all() .build()?; let full_addr = if args.no_tls { if args.server.starts_with("http://") || args.server.starts_with("https://") { args.server.clone() } else { format!("http://{}", args.server) } } else if args.server.starts_with("http://") || args.server.starts_with("https://") { args.server.clone() } else { format!("https://{}", args.server) }; let client = rt.block_on(async { FurumiClient::connect(&full_addr, &args.token).await })?; let furumi_nfs = nfs::FurumiNfs::new(client); let mount_point = args.mount.clone(); let mount_point_umount = args.mount.clone(); // Set up signal handler for graceful shutdown let running = Arc::new(AtomicBool::new(true)); let r = running.clone(); ctrlc::set_handler(move || { eprintln!("\nReceived signal, unmounting..."); r.store(false, Ordering::SeqCst); })?; rt.block_on(async move { // Bind NFS server to a random available port on localhost let listener = NFSTcpListener::bind("127.0.0.1:0", furumi_nfs) .await .expect("Failed to bind NFS listener"); let port = listener.get_listen_port(); info!("NFS server listening on 127.0.0.1:{}", port); // Start handling NFS connections BEFORE mounting — // mount_nfs needs to talk to the server during mount. let handle = tokio::spawn(async move { if let Err(e) = listener.handle_forever().await { eprintln!("NFS server error: {}", e); } }); // Mount via macOS native mount_nfs let mount_path = mount_point.to_string_lossy().to_string(); let opts = format!( "rdonly,locallocks,noresvport,nfsvers=3,tcp,rsize=1048576,port={},mountport={}", port, port ); let status = Command::new("mount_nfs") .args(["-o", &opts, "127.0.0.1:/", &mount_path]) .status() .expect("Failed to execute mount_nfs"); if !status.success() { handle.abort(); eprintln!("Error: mount_nfs failed with status {}", status); std::process::exit(1); } println!("Mounted Furumi-ng to {:?}", mount_path); // Wait for shutdown signal while running.load(Ordering::SeqCst) { tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; } // Unmount let _ = Command::new("diskutil") .arg("unmount") .arg("force") .arg(mount_point_umount.to_string_lossy().as_ref()) .status(); handle.abort(); println!("Unmounted successfully."); }); Ok(()) }