mount an atproto PDS repository as a FUSE filesystem

Add -d mode for agents and don't wait for newline #14

merged opened by danabra.mov targeting main

Claude Code is struggling a lot with using pdsfs. This is the changes it suggests to make pdsfs actually usable.


Summary#

Add daemon mode (-d flag) and change foreground mode to use ctrl-c instead of "hit enter".

Motivation: The "hit enter to unmount" pattern doesn't work well with scripts or automated tools - stdin may be closed or unavailable. The ctrl-c pattern is more standard for long-running foreground processes.

Changes:

  • Add -d/--daemon flag that backgrounds the mount process and exits the parent when the mount is ready (follows the same pattern as rclone mount --daemon)
  • Change foreground mode from "hit enter" to ctrl-c
  • Update readme to reflect new behavior

Usage:

# Foreground (ctrl-c to unmount)
pdsfs oppi.li

# Daemon mode (parent exits when mount is ready)
pdsfs -d oppi.li && ls mnt/

The daemon mode uses a Unix socket pair for the child to signal readiness to the parent, with a 120-second timeout.

Labels

None yet.

Participants 2
AT URI
at://did:plc:fpruhuo22xkm5o7ttr2ktxdo/sh.tangled.repo.pull/3mcrvkoo6g522
+89 -22
Diff #0
+11
Cargo.lock
··· 2314 2314 source = "registry+https://github.com/rust-lang/crates.io-index" 2315 2315 checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 2316 2316 2317 + [[package]] 2318 + name = "signal-hook-registry" 2319 + version = "1.4.8" 2320 + source = "registry+https://github.com/rust-lang/crates.io-index" 2321 + checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" 2322 + dependencies = [ 2323 + "errno", 2324 + "libc", 2325 + ] 2326 + 2317 2327 [[package]] 2318 2328 name = "slab" 2319 2329 version = "0.4.10" ··· 2532 2542 "libc", 2533 2543 "mio", 2534 2544 "pin-project-lite", 2545 + "signal-hook-registry", 2535 2546 "slab", 2536 2547 "socket2 0.5.10", 2537 2548 "tokio-macros",
+1 -1
Cargo.toml
··· 23 23 serde_ipld_dagcbor = "0.6.3" 24 24 serde_json = "1.0.141" 25 25 thiserror = "2.0.12" 26 - tokio = { version = "1.46.1", features = ["fs", "sync", "rt-multi-thread"] } 26 + tokio = { version = "1.46.1", features = ["fs", "sync", "rt-multi-thread", "signal"] } 27 27 tokio-tungstenite = { version = "0.24", features = ["native-tls"] } 28 28 xdg = "3.0.0"
+10 -2
readme.txt
··· 12 12 13 13 ฮป pdsfs oppi.li 14 14 mounted at "mnt" 15 - hit enter to unmount and exit... 15 + ctrl-c to unmount 16 + 17 + 18 + pass -d to run in daemon mode. the parent process will exit 19 + once the mount is ready, useful for scripting: 20 + 21 + 22 + ฮป pdsfs -d oppi.li && ls mnt/ 23 + did:plc:qfpnj4og54vl56wngdriaxug/ 16 24 17 25 18 26 oppi.li is my handle in the atmosphere. the tool does some ··· 100 108 download complete for...did:plc:3danwc67lo7obz2fmdg6jxcr 101 109 download complete for...did:plc:wshs7t2adsemcrrd4snkeqli 102 110 mounted at "mnt" 103 - hit enter to unmount and exit... 111 + ctrl-c to unmount 104 112 105 113 106 114 # -- in a separate shell --
+67 -19
src/main.rs
··· 13 13 use futures::{StreamExt, stream}; 14 14 use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; 15 15 use std::{ 16 - io::{Cursor, Write}, 16 + io::{Cursor, Read as _, Write as _}, 17 + os::unix::net::UnixStream, 17 18 path::PathBuf, 18 19 process::Command, 19 20 sync::Arc, 20 21 }; 21 22 22 23 fn main() { 23 - let rt = tokio::runtime::Runtime::new().unwrap(); 24 24 let matches = clap::command!() 25 25 .arg( 26 26 clap::Arg::new("handles") ··· 35 35 .action(clap::ArgAction::Set) 36 36 .value_parser(clap::value_parser!(PathBuf)), 37 37 ) 38 + .arg( 39 + clap::Arg::new("daemon") 40 + .short('d') 41 + .long("daemon") 42 + .action(clap::ArgAction::SetTrue) 43 + .help("Run in background and exit parent when mount is ready"), 44 + ) 45 + .arg( 46 + clap::Arg::new("__child") 47 + .long("__child") 48 + .hide(true) 49 + .action(clap::ArgAction::Set), 50 + ) 38 51 .get_matches(); 39 - let handles = matches 40 - .get_many::<String>("handles") 41 - .unwrap() 42 - .cloned() 43 - .collect::<Vec<_>>(); 44 - let mountpoint = matches 45 - .get_one::<PathBuf>("mountpoint") 46 - .map(ToOwned::to_owned) 47 - .unwrap_or(PathBuf::from("mnt")); 52 + 53 + let handles: Vec<String> = matches.get_many::<String>("handles").unwrap().cloned().collect(); 54 + let mountpoint = matches.get_one::<PathBuf>("mountpoint").cloned() 55 + .unwrap_or_else(|| PathBuf::from("mnt")); 56 + 57 + // Daemon mode: spawn child and wait for ready signal 58 + if matches.get_flag("daemon") { 59 + let (mut rx, tx) = UnixStream::pair().unwrap(); 60 + rx.set_read_timeout(Some(std::time::Duration::from_secs(120))).ok(); 61 + 62 + use std::os::unix::io::AsRawFd; 63 + let fd = tx.as_raw_fd(); 64 + let exe = std::env::current_exe().unwrap(); 65 + 66 + unsafe { 67 + use std::os::unix::process::CommandExt; 68 + let mut cmd = Command::new(&exe); 69 + cmd.args(std::env::args().skip(1).filter(|a| a != "-d" && a != "--daemon")); 70 + cmd.arg("--__child").arg(fd.to_string()); 71 + cmd.pre_exec(move || { 72 + libc::fcntl(fd, libc::F_SETFD, 0); // clear close-on-exec 73 + libc::setsid(); 74 + Ok(()) 75 + }); 76 + cmd.spawn().expect("Failed to spawn daemon"); 77 + } 78 + drop(tx); 79 + 80 + let mut buf = [0u8; 1]; 81 + match rx.read_exact(&mut buf) { 82 + Ok(_) => std::process::exit(0), 83 + Err(e) => { eprintln!("Daemon failed: {}", e); std::process::exit(1); } 84 + } 85 + } 86 + 87 + // Child mode: signal parent when ready 88 + let signal_fd: Option<i32> = matches.get_one::<String>("__child") 89 + .and_then(|s| s.parse().ok()); 90 + 91 + let rt = tokio::runtime::Runtime::new().unwrap(); 48 92 49 93 // Clean up any stale mount before proceeding 50 94 cleanup_stale_mount(&mountpoint); ··· 128 172 }); 129 173 } 130 174 131 - println!("mounted at {mountpoint:?}"); 132 - print!("hit enter to unmount and exit..."); 133 - std::io::stdout().flush().unwrap(); 134 - 135 - // Wait for user input 136 - let mut input = String::new(); 137 - std::io::stdin().read_line(&mut input).unwrap(); 175 + // Signal parent if in daemon mode 176 + if let Some(fd) = signal_fd { 177 + use std::os::unix::io::FromRawFd; 178 + let mut sock = unsafe { UnixStream::from_raw_fd(fd) }; 179 + let _ = sock.write_all(b"R"); 180 + } else { 181 + eprintln!("mounted at {mountpoint:?}"); 182 + eprintln!("ctrl-c to unmount"); 183 + } 138 184 139 - println!("unmounted {mountpoint:?}"); 185 + // Wait for ctrl-c 186 + rt.block_on(tokio::signal::ctrl_c()).ok(); 187 + eprintln!("unmounted {mountpoint:?}"); 140 188 } 141 189 142 190 async fn cached_download(

Submissions

sign up or login to add to the discussion
danabra.mov submitted #0
oppi.li

makes sense!

pull request successfully merged