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}