playdate_device/mount/
linux.rs

1use std::borrow::Cow;
2use std::collections::HashMap;
3use std::ffi::OsStr;
4use std::path::Path;
5use std::path::PathBuf;
6use std::future::Future;
7use std::future::IntoFuture;
8
9use futures::FutureExt;
10use udev::Enumerator;
11
12use crate::device::serial::SerialNumber;
13use crate::error::Error;
14use crate::device::Device;
15
16
17#[derive(Debug, Clone)]
18pub struct Volume {
19	/// FS mount point.
20	path: PathBuf,
21
22	/// Partition node path, e.g.: `/dev/sda1`.
23	part_node: PathBuf,
24
25	/// Disk node path, e.g.: `/dev/sda`.
26	disk_node: PathBuf,
27
28	/// Device sysfs path.
29	dev_sysfs: PathBuf,
30}
31
32impl Volume {
33	pub fn new(path: PathBuf, part: PathBuf, disk: PathBuf, dev_sysfs: PathBuf) -> Self {
34		Self { path,
35		       part_node: part,
36		       disk_node: disk,
37		       dev_sysfs }
38	}
39}
40
41impl std::fmt::Display for Volume {
42	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.path.display().fmt(f) }
43}
44
45impl Volume {
46	/// This volume's path.
47	pub fn path(&self) -> Cow<'_, Path> { self.path.as_path().into() }
48}
49
50
51pub mod unmount {
52	use futures::TryFutureExt;
53
54	use super::*;
55	use crate::mount::Unmount;
56	use crate::mount::UnmountAsync;
57
58
59	impl Unmount for Volume {
60		#[cfg_attr(feature = "tracing", tracing::instrument())]
61		fn unmount_blocking(&self) -> Result<(), Error> {
62			use std::process::Command;
63
64			let res =
65				unmount_eject(&self).or_else(|err| {
66					                    eject(self).status()
67					                               .map_err(Error::from)
68					                               .and_then(|res| res.exit_ok().map_err(Error::from))
69					                               .map_err(|err2| Error::chain(err2, [err]))
70				                    })
71				                    .or_else(|err| -> Result<(), Error> {
72					                    unmount(self).status()
73					                                 .map_err(Error::from)
74					                                 .and_then(|res| res.exit_ok().map_err(Error::from))
75					                                 .map_err(|err2| Error::chain(err2, [err]))
76				                    })
77				                    .or_else(move |err| -> Result<(), Error> {
78					                    udisksctl_unmount(self).status()
79					                                           .map_err(Error::from)
80					                                           .and_then(|res| res.exit_ok().map_err(Error::from))
81					                                           .map_err(|err2| Error::chain(err2, [err]))
82				                    })
83				                    .or_else(move |err| -> Result<(), Error> {
84					                    udisks_unmount(self).status()
85					                                        .map_err(Error::from)
86					                                        .and_then(|res| res.exit_ok().map_err(Error::from))
87					                                        .map_err(|err2| Error::chain(err2, [err]))
88				                    })
89				                    .inspect(|_| trace!("unmounted {self}"));
90
91			// TODO: use `udisks_power_off` also as fallback for `udisksctl_power_off`:
92			Command::from(udisksctl_power_off(self)).status()
93			                                        .map_err(Error::from)
94			                                        .and_then(|res| res.exit_ok().map_err(Error::from))
95			                                        .map_err(move |err2| {
96				                                        if let Some(err) = res.err() {
97					                                        Error::chain(err2, [err])
98				                                        } else {
99					                                        err2
100				                                        }
101			                                        })
102		}
103	}
104
105	impl UnmountAsync for Volume {
106		#[cfg_attr(feature = "tracing", tracing::instrument())]
107		async fn unmount(&self) -> Result<(), Error> {
108			use futures_lite::future::ready;
109			#[cfg(all(feature = "tokio", not(feature = "async-std")))]
110			use tokio::process::Command;
111			#[cfg(feature = "async-std")]
112			use async_std::process::Command;
113
114			async { unmount_eject(&self) }.or_else(|err| {
115				                              Command::from(eject(self)).status()
116				                                                        .map_err(|err2| Error::chain(err2, [err]))
117				                                                        .and_then(|res| {
118					                                                        ready(res.exit_ok().map_err(Error::from))
119				                                                        })
120			                              })
121			                              .or_else(|err| {
122				                              Command::from(unmount(self)).status()
123				                                                          .map_err(|err2| Error::chain(err2, [err]))
124				                                                          .and_then(|res| {
125					                                                          ready(res.exit_ok().map_err(Error::from))
126				                                                          })
127			                              })
128			                              .or_else(|err| {
129				                              Command::from(udisksctl_unmount(self)).status()
130				                                                                    .map_err(|err2| {
131					                                                                    Error::chain(err2, [err])
132				                                                                    })
133				                                                                    .and_then(|res| {
134					                                                                    ready(
135					                                                                          res.exit_ok()
136					                                                                             .map_err(Error::from),
137					)
138				                                                                    })
139			                              })
140			                              .or_else(|err| {
141				                              Command::from(udisks_unmount(self)).status()
142				                                                                 .map_err(|err2| {
143					                                                                 Error::chain(err2, [err])
144				                                                                 })
145				                                                                 .and_then(|res| {
146					                                                                 ready(
147					                                                                       res.exit_ok()
148					                                                                          .map_err(Error::from),
149					)
150				                                                                 })
151			                              })
152			                              .inspect_ok(|_| trace!("unmounted {self}"))
153			                              .then(|res| {
154				                              // TODO: use `udisks_power_off` also as fallback for `udisksctl_power_off`:
155				                              Command::from(udisksctl_power_off(self)).status()
156				                                                                      .map_err(Error::from)
157				                                                                      .and_then(|res| {
158					                                                                      ready(
159					                                                                        res.exit_ok()
160					                                                                           .map_err(Error::from),
161					)
162				                                                                      })
163				                                                                      .map_err(|err2| {
164					                                                                      if let Some(err) = res.err()
165					                                                                      {
166						                                                                      Error::chain(err2, [err])
167					                                                                      } else {
168						                                                                      err2
169					                                                                      }
170				                                                                      })
171			                              })
172			                              .await
173		}
174	}
175
176
177	#[cfg_attr(feature = "tracing", tracing::instrument())]
178	pub fn unmount_eject(vol: &Volume) -> Result<(), Error> {
179		use eject::device::Device;
180
181		let drive = Device::open(&vol.disk_node).map_err(std::io::Error::from)?;
182		drive.eject().map_err(std::io::Error::from)?;
183		trace!("Ejected {}", &vol.disk_node.display());
184		Ok(())
185	}
186
187
188	fn eject(vol: &Volume) -> std::process::Command {
189		let mut cmd = std::process::Command::new("eject");
190		cmd.arg(vol.path().as_ref());
191		cmd
192	}
193
194	fn unmount(vol: &Volume) -> std::process::Command {
195		let mut cmd = std::process::Command::new("umount");
196		cmd.arg(vol.path().as_ref());
197		cmd
198	}
199
200	fn udisksctl_unmount(vol: &Volume) -> std::process::Command {
201		let mut cmd = std::process::Command::new("udisksctl");
202		cmd.args(["unmount", "--no-user-interaction", "-b"]);
203		cmd.arg(&vol.part_node);
204		cmd
205	}
206
207	fn udisksctl_power_off(vol: &Volume) -> std::process::Command {
208		let mut cmd = std::process::Command::new("udisksctl");
209		cmd.args(["power-off", "--no-user-interaction", "-b"]);
210		cmd.arg(&vol.disk_node);
211		cmd
212	}
213
214	fn udisks_unmount(vol: &Volume) -> std::process::Command {
215		let mut cmd = std::process::Command::new("udisks");
216		cmd.arg("--unmount");
217		cmd.arg(&vol.part_node);
218		cmd
219	}
220
221	fn udisks_power_off(vol: &Volume) -> std::process::Command {
222		let mut cmd = std::process::Command::new("udisks");
223		cmd.arg("--detach");
224		cmd.arg(&vol.disk_node);
225		cmd
226	}
227
228	// NOTE: mb. try to use `udisks`, that's existing in Ubuntu.
229	// udisksctl unmount -b /dev/sdc1 && udisksctl power-off -b /dev/sdc
230	// udisks --unmount /dev/sdb1 && udisks --detach /dev/sdb
231}
232
233
234#[cfg_attr(feature = "tracing", tracing::instrument(fields(dev = dev.as_ref().serial_number())))]
235pub async fn volume_for<Info>(dev: Info) -> Result<Volume, Error>
236	where Info: AsRef<nusb::DeviceInfo> {
237	let sysfs = dev.as_ref().sysfs_path();
238	let mut enumerator = enumerator()?;
239	enumerator.add_syspath(sysfs)?;
240
241	if let Some(sn) = dev.as_ref().serial_number() {
242		enumerator.match_property("ID_SERIAL_SHORT", sn)?;
243	}
244
245	let mounts = lfs_core::read_mountinfo()?;
246	enumerator.scan_devices()?
247	          .filter_map(|udev| {
248		          udev.devtype()
249		              .filter(|ty| *ty == OsStr::new("partition"))
250		              .is_some()
251		              .then(move || udev.devnode().map(Path::to_path_buf).map(|node| (udev, node)))
252	          })
253	          .flatten()
254	          .find_map(|(udev, node)| {
255		          mounts.iter()
256		                .find(|inf| Path::new(inf.fs.as_str()) == node.as_path())
257		                .map(|inf| (udev, node, inf))
258	          })
259	          .and_then(|(udev, node, minf)| {
260		          let disk = udev.parent()
261		                         .filter(is_disk)
262		                         .or_else(|| udev.parent().map(|d| d.parent().filter(is_disk)).flatten())
263		                         .and_then(|dev| dev.devnode().map(ToOwned::to_owned));
264		          let sysfs = PathBuf::from(sysfs);
265		          disk.map(move |disk| Volume::new(minf.mount_point.clone(), node, disk, sysfs))
266	          })
267	          .ok_or_else(|| Error::not_found())
268}
269
270
271#[cfg_attr(feature = "tracing", tracing::instrument(skip(devs)))]
272pub async fn volumes_for_map<I>(devs: I) -> Result<HashMap<Device, Option<Volume>>, Error>
273	where I: IntoIterator<Item = Device> {
274	let mounts = lfs_core::read_mountinfo()?;
275
276	if mounts.is_empty() {
277		return Ok(devs.into_iter().map(|dev| (dev, None)).collect());
278	}
279
280	let mut enumerator = enumerator()?;
281
282	let udevs: Vec<_> = enumerator.scan_devices()?
283	                              .filter(is_partition)
284	                              .filter_map(|dev| {
285		                              if let Some(sn) = dev.property_value("ID_SERIAL_SHORT") {
286			                              let sn = sn.to_string_lossy().to_string();
287			                              Some((dev, sn))
288		                              } else {
289			                              if let Some(sn) = dev.property_value("ID_SERIAL") {
290				                              let sn: Result<SerialNumber, _> =
291					                              sn.to_string_lossy().as_ref().try_into();
292				                              sn.ok().map(|sn| (dev, sn.to_string()))
293			                              } else {
294				                              None
295			                              }
296		                              }
297	                              })
298	                              .collect();
299
300	if udevs.is_empty() {
301		return Ok(devs.into_iter().map(|dev| (dev, None)).collect());
302	}
303
304	let mut devs = devs.into_iter().filter_map(|dev| {
305		                               if let Some(sn) = dev.info().serial_number().map(ToOwned::to_owned) {
306			                               Some((dev, sn))
307		                               } else {
308			                               None
309		                               }
310	                               });
311
312	let result =
313		devs.map(|(dev, ref sna)| {
314			    let node =
315				    udevs.iter()
316				         .find_map(|(inf, snb)| {
317					         (sna == snb).then(|| inf.devnode())
318					                     .flatten()
319					                     .map(ToOwned::to_owned)
320					                     .map(|dn| (inf, dn))
321				         })
322				         .and_then(|(udev, node)| {
323					         mounts.iter()
324					               .find(|inf| Path::new(inf.fs.as_str()) == node)
325					               .and_then(|inf| {
326						               let disk = udev.parent()
327						                              .filter(is_disk)
328						                              .or_else(|| udev.parent().map(|d| d.parent().filter(is_disk)).flatten())
329						                              .and_then(|dev| dev.devnode().map(ToOwned::to_owned));
330
331						               let sysfs = dev.info().sysfs_path().to_owned();
332						               disk.map(move |disk| Volume::new(inf.mount_point.clone(), node, disk, sysfs))
333					               })
334				         });
335			    (dev, node)
336		    })
337		    .collect();
338	Ok(result)
339}
340
341
342// TODO: this is needed too:
343// pub fn volumes_for<'i, I: 'i>(
344// 	devs: I)
345// 	-> Result<impl Iterator<Item = (impl Future<Output = Result<PathBuf, Error>>, &'i Device)>, Error>
346// 	where I: IntoIterator<Item = &'i Device> {
347// 	//
348// 	Ok(vec![(futures::future::lazy(|_| todo!()).into_future(), &todo!())].into_iter())
349// }
350
351
352fn enumerator() -> Result<Enumerator, Error> {
353	let mut enumerator = udev::Enumerator::new()?;
354	// filter only PD devices:
355	enumerator.match_property("ID_VENDOR", "Panic")?;
356	enumerator.match_property("ID_MODEL", "Playdate")?;
357	Ok(enumerator)
358}
359
360
361fn is_partition(dev: &udev::Device) -> bool {
362	dev.devtype()
363	   .filter(|ty| *ty == OsStr::new("partition"))
364	   .is_some()
365}
366
367fn is_disk(dev: &udev::Device) -> bool { dev.devtype().filter(|ty| *ty == OsStr::new("disk")).is_some() }