playdate_device/mount/
linux.rs1use 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 path: PathBuf,
21
22 part_node: PathBuf,
24
25 disk_node: PathBuf,
27
28 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 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 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 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 }
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
342fn enumerator() -> Result<Enumerator, Error> {
353 let mut enumerator = udev::Enumerator::new()?;
354 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() }