use std::path::PathBuf;
use std::sync::mpsc;
use std::sync::Arc;
use clap::Parser;
use log::{error, info};
use mtp_mount::daemon::dryrun::{DryRun, DryRunCommand};
use mtp_mount::daemon::supervisor::{Command, Supervisor, SupervisorConfig};
use mtp_mount::daemon::unmount::clean_stale_mounts;
use mtp_mount::daemon::usb::{spawn_dry_run_watch, spawn_hotplug_watch, UsbSource};
use mtp_mount::daemon::{mount_root_from_env, RUNTIME_SUBDIR};
use mtp_mount::hints::{indent, BUSY_HINT, PERMISSION_HINT};
use mtp_mount::spool;
#[derive(Parser, Debug)]
#[command(name = "mtp-mountd", version, about, after_long_help = long_help())]
struct Cli {
#[arg(long, value_name = "PATH")]
mount_root: Option<PathBuf>,
#[arg(long, value_name = "PATH")]
spool_dir: Option<PathBuf>,
#[arg(short, long)]
read_only: bool,
#[arg(long)]
dry_run: bool,
}
fn long_help() -> String {
format!(
"\
WHAT IT DOES:
Watches for MTP devices and mounts each one it finds under the mount root,
in a directory named after the device's serial number:
$XDG_RUNTIME_DIR/{RUNTIME_SUBDIR}/<serial>/
Devices that report no serial number get usb-<vendor>-<product>-<port>
instead, which lasts as long as the device stays in the same port.
A device with more than one storage (internal memory plus an SD card, say)
gets ONE mount, with each storage as a subdirectory under it.
Unplug a device and its mount goes away right then, so nothing is left for
a file manager to hang on. Plug it back in and it's mounted again.
RUNNING IT:
As a systemd --user service (the intended way):
systemctl --user enable --now mtp-mountd
In a terminal, to watch what it's doing:
RUST_LOG=info mtp-mountd
CHECKING A DEVICE WITHOUT MOUNTING IT (--dry-run):
mtp-mountd --dry-run
Watches for devices and prints what it WOULD do: the fields each device
reports, the mount key derived from them, and the path it would mount at.
Nothing is mounted, no directory is created, and the device is never
opened, so this works on a machine with no FUSE at all.
Plug the device in, wait for the PLUGGED IN block, then unplug it. The
UNPLUGGED block says whether its key MATCHES the arrival. It has to: the
key is how a departure finds the mount to take down, so a device whose
two keys disagree would leave its mount behind. Do that a few times and
read the summary. --spool-dir and -r do nothing in this mode.
TROUBLESHOOTING:
Nothing gets mounted
Make sure the phone is unlocked, USB mode is set to \"File Transfer\"
(not \"Charging only\"), and the USB debugging prompt is accepted.
Run with RUST_LOG=debug to see what the daemon sees.
\"interface is busy\" in the log
{busy}
\"Permission denied\" on /dev/bus/usb
{permission}
NOTES:
Files are uploaded to the device when you close them, not on each write.
While a file is open for writing it's spooled to disk under your cache
directory (--spool-dir overrides it), so uploads bigger than RAM work.
MTP doesn't support partial writes, hardlinks, symlinks, or chmod.
There's no reconnect window: a device that goes away is unmounted at once,
and hotplug mounts it again when it comes back. Waiting instead would
freeze every process touching the mount for the length of the window.",
busy = indent(BUSY_HINT, " "),
permission = indent(PERMISSION_HINT, " "),
)
}
fn main() {
env_logger::init();
let cli = Cli::parse();
let mount_root = match mount_root_from_env(cli.mount_root.as_deref()) {
Ok(root) => root,
Err(e) => {
eprintln!("{e}");
std::process::exit(1);
}
};
if cli.dry_run {
run_dry_run(mount_root);
return;
}
let spool_dir = match spool::spool_dir_from_env(cli.spool_dir.as_deref())
.and_then(|dir| spool::prepare_spool_dir(&dir).map(|()| dir))
{
Ok(dir) => dir,
Err(e) => {
eprintln!("{e}");
std::process::exit(1);
}
};
for path in clean_stale_mounts(&mount_root) {
info!("Cleaned up a stale mount at {}", path.display());
}
let rt = match tokio::runtime::Runtime::new() {
Ok(rt) => rt,
Err(e) => {
eprintln!("Can't start the async runtime: {e}");
std::process::exit(1);
}
};
let handle = rt.handle().clone();
let (commands, inbox) = mpsc::channel();
if let Err(e) = spawn_hotplug_watch(&handle, commands.clone()) {
eprintln!("Can't watch for USB devices: {e}");
std::process::exit(1);
}
{
let commands = commands.clone();
spawn_signal_handler(&handle, move |signal| {
let _ = commands.send(Command::Stop(format!("got {signal}")));
});
}
info!(
"Watching for MTP devices; mounts appear under {}",
mount_root.display()
);
let supervisor = Supervisor::new(
SupervisorConfig::new(mount_root, spool_dir, cli.read_only),
Arc::new(UsbSource),
handle,
commands,
);
supervisor.run(inbox);
info!("Stopped.");
}
fn run_dry_run(mount_root: PathBuf) {
let rt = match tokio::runtime::Runtime::new() {
Ok(rt) => rt,
Err(e) => {
eprintln!("Can't start the async runtime: {e}");
std::process::exit(1);
}
};
let handle = rt.handle().clone();
let (events, inbox) = mpsc::channel();
if let Err(e) = spawn_dry_run_watch(&handle, events.clone()) {
eprintln!("Can't watch for USB devices: {e}");
std::process::exit(1);
}
spawn_signal_handler(&handle, move |signal| {
let _ = events.send(DryRunCommand::Stop(format!("got {signal}")));
});
DryRun::new(mount_root).run(inbox);
}
fn spawn_signal_handler<F>(rt: &tokio::runtime::Handle, on_signal: F)
where
F: FnOnce(&str) + Send + 'static,
{
rt.spawn(async move {
use tokio::signal::unix::{signal, SignalKind};
let mut terminate = match signal(SignalKind::terminate()) {
Ok(stream) => stream,
Err(e) => {
error!("Can't listen for SIGTERM: {e}");
return;
}
};
let mut interrupt = match signal(SignalKind::interrupt()) {
Ok(stream) => stream,
Err(e) => {
error!("Can't listen for SIGINT: {e}");
return;
}
};
let signal_name = tokio::select! {
_ = terminate.recv() => "SIGTERM",
_ = interrupt.recv() => "SIGINT",
};
on_signal(signal_name);
});
}