cadmus_core/device/usb/kobo/
operations.rs

1//! Common USB operations trait for Kobo devices.
2
3use crate::device::metadata::DeviceMetadata;
4use crate::device::usb::error::UsbError;
5use std::fs;
6use std::process::Command;
7use tracing::{debug, error, info, warn};
8
9use nix::unistd::sync;
10
11#[cfg(target_os = "linux")]
12use nix::mount::{mount, umount2, MntFlags, MsFlags};
13#[cfg(target_os = "linux")]
14use procfs;
15#[cfg(target_os = "linux")]
16use std::path::Path;
17
18/// Trait providing common USB operations for Kobo devices.
19///
20/// Implementors must provide access to the device metadata via the
21/// [`metadata`](KoboUsbOperations::metadata) method. All other methods have
22/// default implementations that use the metadata.
23///
24/// # Example
25///
26/// ```ignore
27/// use cadmus_core::device::usb::kobo::operations::KoboUsbOperations;
28/// use cadmus_core::device::DeviceMetadata;
29///
30/// struct MyUsbManager {
31///     metadata: DeviceMetadata,
32/// }
33///
34/// impl KoboUsbOperations for MyUsbManager {
35///     fn metadata(&self) -> &DeviceMetadata {
36///         &self.metadata
37///     }
38/// }
39///
40/// # fn example(manager: &MyUsbManager) -> Result<(), cadmus_core::device::usb::UsbError> {
41/// manager.prepare_for_usb_share()?;
42/// # Ok(())
43/// # }
44/// ```
45pub trait KoboUsbOperations {
46    /// Provides access to the device metadata.
47    ///
48    /// Implementors must return a reference to their [`DeviceMetadata`], which
49    /// is used by the default implementations of other trait methods.
50    fn metadata(&self) -> &DeviceMetadata;
51
52    /// Syncs filesystem buffers and drops caches.
53    ///
54    /// This function ensures all pending writes are flushed to disk before
55    /// unmounting partitions for USB mass storage mode.
56    ///
57    /// # Errors
58    ///
59    /// Returns [`UsbError::Io`] if the sync command or cache drop fails.
60    fn sync_and_drop_caches(&self) -> Result<(), UsbError> {
61        debug!("Syncing filesystem buffers");
62
63        sync();
64
65        fs::write("/proc/sys/vm/drop_caches", "3").map_err(|e| {
66            error!(error = %e, "Failed to drop caches");
67            UsbError::Io(e)
68        })?;
69
70        Ok(())
71    }
72
73    /// Checks if a mount point is currently mounted.
74    ///
75    /// Uses procfs to read mount information and checks if the
76    /// mount_point appears in the list.
77    ///
78    /// # Errors
79    ///
80    /// Returns [`UsbError::Io`] if mount information cannot be read.
81    /// Callers should treat a read failure as an error rather than assuming
82    /// the filesystem is unmounted.
83    #[cfg(target_os = "linux")]
84    fn is_mounted(&self, mount_point: &str) -> Result<bool, UsbError> {
85        procfs::mounts()
86            .map(|mounts| mounts.iter().any(|m| m.fs_file == mount_point))
87            .map_err(|e| {
88                warn!(error = %e, mount_point = %mount_point, "Failed to read mounts");
89                UsbError::Io(std::io::Error::other(format!(
90                    "Failed to read mount info: {}",
91                    e
92                )))
93            })
94    }
95
96    #[cfg(not(target_os = "linux"))]
97    fn is_mounted(&self, _mount_point: &str) -> Result<bool, UsbError> {
98        Ok(false)
99    }
100
101    /// Unmounts a partition lazily.
102    ///
103    /// Detaches the filesystem immediately but cleans up references in the background.
104    ///
105    /// # Errors
106    ///
107    /// Returns [`UsbError::Partition`] if the unmount operation fails.
108    #[cfg(target_os = "linux")]
109    fn unmount_partition(&self, mount_point: &str) -> Result<(), UsbError> {
110        debug!(mount_point = %mount_point, "Unmounting partition");
111
112        umount2(mount_point, MntFlags::MNT_DETACH).map_err(|e| {
113            error!(mount_point = %mount_point, error = %e, "Failed to unmount partition");
114            UsbError::Partition(format!("Failed to unmount {}: {}", mount_point, e))
115        })?;
116
117        info!(mount_point = %mount_point, "Unmounted partition");
118        Ok(())
119    }
120
121    #[cfg(not(target_os = "linux"))]
122    fn unmount_partition(&self, _mount_point: &str) -> Result<(), UsbError> {
123        Err(UsbError::Partition(
124            "Unmount not supported on this platform".to_string(),
125        ))
126    }
127
128    /// Prepares the system for USB mass storage mode.
129    ///
130    /// Performs the necessary cleanup before enabling USB sharing:
131    /// - Syncs filesystem buffers
132    /// - Drops page caches
133    /// - Unmounts /mnt/onboard and /mnt/sd if mounted
134    ///
135    /// # Errors
136    ///
137    /// Returns [`UsbError::Partition`] if unmounting fails critically.
138    fn prepare_for_usb_share(&self) -> Result<(), UsbError> {
139        self.sync_and_drop_caches()?;
140
141        for name in ["onboard", "sd"] {
142            let mount_point = format!("/mnt/{}", name);
143            if self.is_mounted(&mount_point)? {
144                self.unmount_partition(&mount_point)?;
145            }
146        }
147
148        Ok(())
149    }
150
151    /// Remounts partitions after USB sharing is disabled.
152    ///
153    /// Mounts the onboard partition and optionally the SD card if present.
154    /// Uses vfat filesystem with noatime, nodiratime, shortname=mixed, and utf8 options.
155    ///
156    /// # Errors
157    ///
158    /// Returns [`UsbError::Partition`] if mounting the onboard partition fails.
159    #[cfg(target_os = "linux")]
160    fn remount_partitions(&self) -> Result<(), UsbError> {
161        let onboard_partition = &self.metadata().partition;
162
163        info!("Remounting onboard partition");
164
165        mount(
166            Some(onboard_partition.as_str()),
167            "/mnt/onboard",
168            Some("vfat"),
169            MsFlags::MS_NOATIME | MsFlags::MS_NODIRATIME,
170            Some("shortname=mixed,utf8"),
171        )
172        .map_err(|e| {
173            error!(error = %e, "Failed to mount onboard partition");
174            UsbError::Partition(format!("Cannot mount onboard: {}", e))
175        })?;
176
177        info!("Onboard partition remounted successfully");
178
179        let sd_partition = "/dev/mmcblk1p1";
180        if Path::new(sd_partition).exists() {
181            debug!("Attempting to remount SD card");
182            let _ = mount(
183                Some(sd_partition),
184                "/mnt/sd",
185                Some("vfat"),
186                MsFlags::MS_NOATIME | MsFlags::MS_NODIRATIME,
187                Some("shortname=mixed,utf8"),
188            );
189        }
190
191        Ok(())
192    }
193
194    #[cfg(not(target_os = "linux"))]
195    fn remount_partitions(&self) -> Result<(), UsbError> {
196        Err(UsbError::Partition(
197            "Mount not supported on this platform".to_string(),
198        ))
199    }
200
201    /// Runs filesystem check and repair.
202    ///
203    /// Runs `dosfsck -a -w` on the partition up to twice. The first run
204    /// attempts automatic repair. If it exits with a non-zero status
205    /// (exit code 1 means recoverable errors were found; see
206    /// [`fsck.fat(8)`](https://man7.org/linux/man-pages/man8/fsck.fat.8.html)),
207    /// a second run is performed to verify the repairs. Only after both
208    /// runs fail is the filesystem considered unrecoverable.
209    ///
210    /// ## `dosfsck` exit codes
211    ///
212    /// | Code | Meaning |
213    /// |------|---------|
214    /// | `0`  | No errors found |
215    /// | `1`  | Recoverable errors found (or internal inconsistency) |
216    /// | `2`  | Usage error – filesystem was not accessed |
217    ///
218    /// The two-pass approach mirrors the original shell script behaviour:
219    /// the first pass repairs, and the second pass confirms the result.
220    ///
221    /// # Errors
222    ///
223    /// Returns [`UsbError::Partition`] if `/mnt/onboard` is still mounted,
224    /// to prevent running dosfsck on a live filesystem.
225    /// Returns [`UsbError::Filesystem`] if filesystem corruption is detected
226    /// and cannot be repaired. Returns [`UsbError::Io`] if the command fails
227    /// to execute.
228    fn check_filesystem(&self) -> Result<(), UsbError> {
229        if self.is_mounted("/mnt/onboard")? {
230            error!("Refusing to run filesystem check: /mnt/onboard is still mounted");
231            return Err(UsbError::Partition(
232                "/mnt/onboard is still mounted; filesystem check aborted".to_string(),
233            ));
234        }
235
236        let partition = &self.metadata().partition;
237
238        info!(partition = %partition, "Running filesystem check");
239
240        let result = Command::new("dosfsck")
241            .args(["-a", "-w", partition])
242            .output();
243
244        match result {
245            Ok(output) => {
246                let stdout = String::from_utf8_lossy(&output.stdout);
247                let stderr = String::from_utf8_lossy(&output.stderr);
248
249                if output.status.success() {
250                    info!("Filesystem check passed");
251                    return Ok(());
252                }
253
254                warn!(stdout = %stdout, stderr = %stderr, "First filesystem check failed, retrying");
255
256                let retry = Command::new("dosfsck")
257                    .args(["-a", "-w", partition])
258                    .output();
259
260                match retry {
261                    Ok(retry_output) if retry_output.status.success() => {
262                        info!("Filesystem check passed on retry");
263                        Ok(())
264                    }
265                    Ok(_) => {
266                        error!("Filesystem corruption detected and cannot be repaired");
267                        Err(UsbError::Filesystem(
268                            "Filesystem corruption detected, reboot required".to_string(),
269                        ))
270                    }
271                    Err(e) => Err(UsbError::Io(e)),
272                }
273            }
274            Err(e) => Err(UsbError::Io(e)),
275        }
276    }
277}