use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{Receiver, Sender};
use std::sync::Arc;
use std::time::Duration;
use log::{debug, error, info, warn};
use crate::daemon::unmount::{force_unmount, wait_until_unmounted};
use crate::device::{DeviceOpener, UnplugSwitch};
use crate::fs::{MtpFs, MtpFsConfig};
use crate::hints::open_failure_hint;
use crate::reconnect::ReconnectPolicy;
pub const DEFAULT_UNMOUNT_TIMEOUT: Duration = Duration::from_secs(10);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeviceIdent {
pub key: String,
pub label: String,
pub serial: Option<String>,
}
#[derive(Debug, Clone)]
pub enum DeviceChange {
Arrived(DeviceIdent),
Left(DeviceIdent),
}
#[derive(Debug, Clone)]
pub enum Command {
Device(DeviceChange),
GiveUp { key: String, reason: String },
Stop(String),
}
pub trait DeviceSource: Send + Sync {
fn opener(&self, ident: &DeviceIdent) -> Arc<dyn DeviceOpener>;
}
pub struct SupervisorConfig {
pub mount_root: PathBuf,
pub spool_dir: PathBuf,
pub read_only: bool,
pub unmount_timeout: Duration,
}
impl SupervisorConfig {
pub fn new(mount_root: PathBuf, spool_dir: PathBuf, read_only: bool) -> Self {
Self {
mount_root,
spool_dir,
read_only,
unmount_timeout: DEFAULT_UNMOUNT_TIMEOUT,
}
}
}
struct ActiveMount {
path: PathBuf,
label: String,
session: fuser::BackgroundSession,
watcher_stop: Arc<AtomicBool>,
}
pub struct Supervisor {
config: SupervisorConfig,
source: Arc<dyn DeviceSource>,
rt: tokio::runtime::Handle,
commands: Sender<Command>,
mounts: HashMap<String, ActiveMount>,
}
impl Supervisor {
pub fn new(
config: SupervisorConfig,
source: Arc<dyn DeviceSource>,
rt: tokio::runtime::Handle,
commands: Sender<Command>,
) -> Self {
Self {
config,
source,
rt,
commands,
mounts: HashMap::new(),
}
}
pub fn run(mut self, commands: Receiver<Command>) {
if let Err(e) = std::fs::create_dir_all(&self.config.mount_root) {
error!(
"Can't create the mount root {}: {e}",
self.config.mount_root.display()
);
}
let stop_reason = loop {
match commands.recv() {
Ok(Command::Device(DeviceChange::Arrived(ident))) => self.mount(ident),
Ok(Command::Device(DeviceChange::Left(ident))) => {
self.unmount(&ident.key, "the device was unplugged")
}
Ok(Command::GiveUp { key, reason }) => self.unmount(&key, &reason),
Ok(Command::Stop(reason)) => break reason,
Err(_) => break "every event source is gone".to_string(),
}
};
info!("Shutting down: {stop_reason}");
self.unmount_all();
}
pub fn mounted_keys(&self) -> Vec<String> {
let mut keys: Vec<String> = self.mounts.keys().cloned().collect();
keys.sort();
keys
}
fn mount(&mut self, ident: DeviceIdent) {
if let Some(existing) = self.mounts.get(&ident.key) {
warn!(
"{} is already mounted at {}; ignoring this arrival. \
Two devices reporting the same serial number look identical from here.",
ident.label,
existing.path.display()
);
return;
}
let path = self.config.mount_root.join(&ident.key);
if let Err(e) = std::fs::create_dir_all(&path) {
error!("Can't create the mount point {}: {e}", path.display());
return;
}
let opener = self.source.opener(&ident);
let device = match opener.open(&self.rt) {
Ok(device) => device,
Err(e) => {
error!("Can't open {}: {e}", ident.label);
if let Some(hint) = open_failure_hint(&e) {
error!("{hint}");
}
let _ = std::fs::remove_dir(&path);
return;
}
};
let mtp_fs = MtpFs::new(
device,
opener,
self.rt.clone(),
MtpFsConfig {
read_only: self.config.read_only,
spool_dir: self.config.spool_dir.clone(),
reconnect: ReconnectPolicy::from_secs(0),
unplug: UnplugSwitch::default(),
},
);
let shutdown = mtp_fs.shutdown();
let mut fuse_config = fuser::Config::default();
fuse_config.mount_options = mtp_fs.mount_options();
let session = match fuser::spawn_mount2(mtp_fs, &path, &fuse_config) {
Ok(session) => session,
Err(e) => {
error!("Can't mount {} at {}: {e}", ident.label, path.display());
let _ = std::fs::remove_dir(&path);
return;
}
};
let watcher_stop = Arc::new(AtomicBool::new(false));
{
let watcher_stop = Arc::clone(&watcher_stop);
let commands = self.commands.clone();
let key = ident.key.clone();
std::thread::spawn(move || loop {
if let Some(reason) = shutdown.wait_timeout(Duration::from_millis(200)) {
let _ = commands.send(Command::GiveUp { key, reason });
return;
}
if watcher_stop.load(Ordering::Relaxed) {
return;
}
});
}
info!("Mounted {} at {}", ident.label, path.display());
self.mounts.insert(
ident.key,
ActiveMount {
path,
label: ident.label,
session,
watcher_stop,
},
);
}
fn unmount(&mut self, key: &str, reason: &str) {
let Some(mount) = self.mounts.remove(key) else {
debug!("Nothing mounted for {key}; nothing to unmount ({reason})");
return;
};
let ActiveMount {
path,
label,
session,
watcher_stop,
} = mount;
watcher_stop.store(true, Ordering::Relaxed);
info!("Unmounting {label} from {}: {reason}", path.display());
if let Err(e) = force_unmount(&path) {
error!("Can't unmount {}: {e}", path.display());
}
let joining_path = path.clone();
std::thread::spawn(move || match session.umount_and_join() {
Ok(()) => debug!("The session for {} ended cleanly", joining_path.display()),
Err(e) => debug!("The session for {} ended with {e}", joining_path.display()),
});
if wait_until_unmounted(&path, self.config.unmount_timeout) {
if let Err(e) = std::fs::remove_dir(&path) {
warn!(
"Unmounted {label}, but {} is still there: {e}",
path.display()
);
}
} else {
error!(
"{} is STILL mounted {}s after unmounting it. \
Anything touching that path may hang; unmount it by hand with \
`fusermount3 -u -z {}`.",
path.display(),
self.config.unmount_timeout.as_secs(),
path.display()
);
}
}
fn unmount_all(&mut self) {
let keys: Vec<String> = self.mounts.keys().cloned().collect();
for key in keys {
self.unmount(&key, "the daemon is shutting down");
}
let _ = std::fs::remove_dir(&self.config.mount_root);
}
}
#[cfg(test)]
mod tests {
use super::*;
struct NoDevices;
impl DeviceSource for NoDevices {
fn opener(&self, _ident: &DeviceIdent) -> Arc<dyn DeviceOpener> {
unreachable!("these tests never get as far as opening a device")
}
}
fn ident(key: &str) -> DeviceIdent {
DeviceIdent {
key: key.to_string(),
label: format!("device {key}"),
serial: Some(key.to_string()),
}
}
fn supervisor(root: PathBuf) -> (Supervisor, Sender<Command>, Receiver<Command>) {
let (tx, rx) = std::sync::mpsc::channel();
let rt = tokio::runtime::Runtime::new().unwrap();
let handle = rt.handle().clone();
std::mem::forget(rt);
let supervisor = Supervisor::new(
SupervisorConfig::new(root, std::env::temp_dir(), false),
Arc::new(NoDevices),
handle,
tx.clone(),
);
(supervisor, tx, rx)
}
#[test]
fn a_departure_for_a_device_that_was_never_mounted_is_ignored() {
let root = tempfile::tempdir().unwrap();
let (mut supervisor, _tx, _rx) = supervisor(root.path().to_path_buf());
supervisor.unmount("ABC123", "test");
assert!(supervisor.mounted_keys().is_empty());
}
#[test]
fn stopping_with_nothing_mounted_returns() {
let root = tempfile::tempdir().unwrap();
let (supervisor, tx, rx) = supervisor(root.path().to_path_buf());
tx.send(Command::Stop("test".into())).unwrap();
drop(tx);
supervisor.run(rx);
}
#[test]
fn the_mount_root_is_created_when_the_loop_starts() {
let parent = tempfile::tempdir().unwrap();
let root = parent.path().join("runtime").join("mtp");
let (supervisor, tx, rx) = supervisor(root.clone());
tx.send(Command::Stop("test".into())).unwrap();
drop(tx);
supervisor.run(rx);
assert!(root.parent().unwrap().is_dir());
}
#[test]
fn a_device_that_cannot_be_opened_leaves_no_directory_behind() {
struct NeverOpens;
impl DeviceSource for NeverOpens {
fn opener(&self, _ident: &DeviceIdent) -> Arc<dyn DeviceOpener> {
struct Refuses;
impl DeviceOpener for Refuses {
fn open(
&self,
_rt: &tokio::runtime::Handle,
) -> Result<mtp_rs::mtp::MtpDevice, mtp_rs::Error> {
Err(mtp_rs::Error::ExclusiveAccess)
}
fn describe(&self) -> String {
"a device that won't open".into()
}
}
Arc::new(Refuses)
}
}
let root = tempfile::tempdir().unwrap();
let (tx, _rx) = std::sync::mpsc::channel();
let rt = tokio::runtime::Runtime::new().unwrap();
let handle = rt.handle().clone();
let mut supervisor = Supervisor::new(
SupervisorConfig::new(root.path().to_path_buf(), std::env::temp_dir(), false),
Arc::new(NeverOpens),
handle,
tx,
);
supervisor.mount(ident("ABC123"));
assert!(supervisor.mounted_keys().is_empty());
assert!(!root.path().join("ABC123").exists());
}
}