mtp_mount/daemon/usb.rs
1//! Production wiring: real USB hotplug into supervisor commands.
2//!
3//! Everything USB-specific about the daemon is here, so the supervisor stays a
4//! plain state machine over a channel (see [`crate::daemon::supervisor`]).
5
6use std::sync::mpsc::Sender;
7use std::sync::Arc;
8
9use futures::StreamExt;
10use log::{info, warn};
11use mtp_rs::mtp::{watch_devices, HotplugEvent, MtpDeviceInfo};
12
13use crate::daemon::dryrun::{DeviceFacts, DryRunCommand};
14use crate::daemon::supervisor::{Command, DeviceChange, DeviceIdent, DeviceSource};
15use crate::device::{DeviceOpener, UnplugSwitch};
16
17/// Everything the watch reported about a device, in a form the rest of the
18/// daemon can hold on to.
19///
20/// `MtpDeviceInfo` is `#[non_exhaustive]`, so nothing outside `mtp-rs` can build
21/// one: this is the only place a real device turns into data our own tests can
22/// produce (see [`crate::daemon::dryrun`]).
23pub fn facts_of(info: &MtpDeviceInfo) -> DeviceFacts {
24 DeviceFacts {
25 serial: info.serial_number.clone(),
26 vendor_id: info.vendor_id,
27 product_id: info.product_id,
28 location_id: info.location_id,
29 manufacturer: info.manufacturer.clone(),
30 product: info.product.clone(),
31 speed: info.speed.map(|s| format!("{s:?}")),
32 match_reason: Some(info.match_reason.as_str().to_string()),
33 label: info.display(),
34 }
35}
36
37/// How the daemon names and identifies a device the USB watch reported.
38///
39/// The key has to come out the same for an arrival and the matching departure,
40/// because the departure is what tells the supervisor which mount to take down.
41/// Both events carry the same [`MtpDeviceInfo`], so deriving the key purely
42/// from its fields is what makes that hold.
43///
44/// It goes through [`DeviceFacts`] so that `--dry-run` reports the key a mount
45/// would really use, rather than a second derivation that could agree in a test
46/// and disagree on a cable.
47pub fn ident_of(info: &MtpDeviceInfo) -> DeviceIdent {
48 facts_of(info).ident()
49}
50
51/// Opens real USB devices, matched by serial number.
52pub struct UsbSource;
53
54impl DeviceSource for UsbSource {
55 fn opener(&self, ident: &DeviceIdent) -> Arc<dyn DeviceOpener> {
56 // A device that reports no serial can only be reopened as "first
57 // available". That's the same compromise the CLI makes, and the daemon
58 // never reopens anyway: reconnect is off, so a device that goes away is
59 // unmounted and re-mounted from a fresh hotplug event.
60 Arc::new(crate::device::UsbOpener::new(
61 ident.serial.clone(),
62 UnplugSwitch::default(),
63 ))
64 }
65}
66
67/// Start watching USB and feed what it sees to the supervisor.
68///
69/// Devices already plugged in arrive as [`HotplugEvent::Arrived`] on the first
70/// poll, so this is the only enumeration the daemon does. Listing devices
71/// separately at startup would mount each of them twice.
72///
73/// # Errors
74///
75/// Returns an error if the OS refuses to set up USB hotplug notifications,
76/// which leaves the daemon with nothing to do.
77pub fn spawn_hotplug_watch(
78 rt: &tokio::runtime::Handle,
79 commands: Sender<Command>,
80) -> Result<(), mtp_rs::Error> {
81 let mut watch = watch_devices()?;
82 rt.spawn(async move {
83 while let Some(event) = watch.next().await {
84 let change = match event {
85 HotplugEvent::Arrived(info) => {
86 info!("Plugged in: {}", info.display());
87 DeviceChange::Arrived(ident_of(&info))
88 }
89 HotplugEvent::Left(info) => {
90 info!("Unplugged: {}", info.display());
91 DeviceChange::Left(ident_of(&info))
92 }
93 };
94 // A closed channel means the supervisor stopped; so should this.
95 if commands.send(Command::Device(change)).is_err() {
96 return;
97 }
98 }
99 warn!("The USB hotplug watch ended; no further devices will be picked up.");
100 });
101 Ok(())
102}
103
104/// Start watching USB and feed what it sees to a dry run.
105///
106/// The same stream [`spawn_hotplug_watch`] uses, reported instead of acted on:
107/// nothing is opened, nothing is mounted. See [`crate::daemon::dryrun`].
108///
109/// # Errors
110///
111/// Returns an error if the OS refuses to set up USB hotplug notifications,
112/// which leaves the dry run with nothing to report.
113pub fn spawn_dry_run_watch(
114 rt: &tokio::runtime::Handle,
115 events: Sender<DryRunCommand>,
116) -> Result<(), mtp_rs::Error> {
117 let mut watch = watch_devices()?;
118 rt.spawn(async move {
119 while let Some(event) = watch.next().await {
120 let command = match event {
121 HotplugEvent::Arrived(info) => DryRunCommand::Arrived(facts_of(&info)),
122 HotplugEvent::Left(info) => DryRunCommand::Left(facts_of(&info)),
123 };
124 // A closed channel means the dry run stopped; so should this.
125 if events.send(command).is_err() {
126 return;
127 }
128 }
129 let _ = events.send(DryRunCommand::Stop(
130 "the USB hotplug watch ended".to_string(),
131 ));
132 });
133 Ok(())
134}