xref: /DADK/dadk/src/actions/rootfs/loopdev.rs (revision 2c1a697be787a4320d89f007f63fbc0fc2be2b67)
1 use core::str;
2 use std::{path::PathBuf, process::Command, thread::sleep, time::Duration};
3 
4 use anyhow::{anyhow, Result};
5 use regex::Regex;
6 
7 use crate::utils::abs_path;
8 
9 const LOOP_DEVICE_LOSETUP_A_REGEX: &str = r"^/dev/loop(\d+)";
10 
11 pub struct LoopDevice {
12     img_path: Option<PathBuf>,
13     loop_device_path: Option<String>,
14     /// 尝试在drop时自动detach
15     try_detach_when_drop: bool,
16 }
17 impl LoopDevice {
18     pub fn attached(&self) -> bool {
19         self.loop_device_path.is_some()
20     }
21 
22     pub fn dev_path(&self) -> Option<&String> {
23         self.loop_device_path.as_ref()
24     }
25 
26     pub fn attach(&mut self) -> Result<()> {
27         if self.attached() {
28             return Ok(());
29         }
30         if self.img_path.is_none() {
31             return Err(anyhow!("Image path not set"));
32         }
33 
34         let output = Command::new("losetup")
35             .arg("-f")
36             .arg("--show")
37             .arg("-P")
38             .arg(self.img_path.as_ref().unwrap())
39             .output()?;
40 
41         if output.status.success() {
42             let loop_device = String::from_utf8(output.stdout)?.trim().to_string();
43             self.loop_device_path = Some(loop_device);
44             sleep(Duration::from_millis(100));
45             log::trace!(
46                 "Loop device attached: {}",
47                 self.loop_device_path.as_ref().unwrap()
48             );
49             Ok(())
50         } else {
51             Err(anyhow::anyhow!(
52                 "Failed to mount disk image: losetup command exited with status {}",
53                 output.status
54             ))
55         }
56     }
57 
58     /// 尝试连接已经存在的loop device
59     pub fn attach_by_exists(&mut self) -> Result<()> {
60         if self.attached() {
61             return Ok(());
62         }
63         if self.img_path.is_none() {
64             return Err(anyhow!("Image path not set"));
65         }
66         log::trace!(
67             "Try to attach loop device by exists: image path: {}",
68             self.img_path.as_ref().unwrap().display()
69         );
70         // losetup -a 查看是否有已经attach了的,如果有,就附着上去
71         let cmd = Command::new("losetup")
72             .arg("-a")
73             .output()
74             .map_err(|e| anyhow!("Failed to run losetup -a: {}", e))?;
75         let output = String::from_utf8(cmd.stdout)?;
76         let s = __loop_device_path_by_disk_image_path(
77             self.img_path.as_ref().unwrap().to_str().unwrap(),
78             &output,
79         )
80         .map_err(|e| anyhow!("Failed to find loop device: {}", e))?;
81         self.loop_device_path = Some(s);
82         Ok(())
83     }
84 
85     /// 获取指定分区的路径
86     ///
87     /// # 参数
88     ///
89     /// * `nth` - 分区的编号
90     ///
91     /// # 返回值
92     ///
93     /// 返回一个 `Result<String>`,包含分区路径的字符串。如果循环设备未附加,则返回错误。
94     ///
95     /// # 错误
96     ///
97     /// 如果循环设备未附加,则返回 `anyhow!("Loop device not attached")` 错误。
98     pub fn partition_path(&self, nth: u8) -> Result<PathBuf> {
99         if !self.attached() {
100             return Err(anyhow!("Loop device not attached"));
101         }
102         let s = format!("{}p{}", self.loop_device_path.as_ref().unwrap(), nth);
103         let s = PathBuf::from(s);
104         // 判断路径是否存在
105         if !s.exists() {
106             return Err(anyhow!("Partition not exist"));
107         }
108         Ok(s)
109     }
110 
111     pub fn detach(&mut self) -> Result<()> {
112         if self.loop_device_path.is_none() {
113             return Ok(());
114         }
115         let loop_device = self.loop_device_path.take().unwrap();
116         let p = PathBuf::from(&loop_device);
117         log::trace!(
118             "Detach loop device: {}, exists: {}",
119             p.display(),
120             p.exists()
121         );
122         let output = Command::new("losetup")
123             .arg("-d")
124             .arg(loop_device)
125             .output()?;
126 
127         if output.status.success() {
128             self.loop_device_path = None;
129             Ok(())
130         } else {
131             Err(anyhow::anyhow!(
132                 "Failed to detach loop device: {}, {}",
133                 output.status,
134                 str::from_utf8(output.stderr.as_slice()).unwrap_or("<Unknown>")
135             ))
136         }
137     }
138 
139     pub fn try_detach_when_drop(&self) -> bool {
140         self.try_detach_when_drop
141     }
142 
143     #[allow(dead_code)]
144     pub fn set_try_detach_when_drop(&mut self, try_detach_when_drop: bool) {
145         self.try_detach_when_drop = try_detach_when_drop;
146     }
147 }
148 
149 impl Drop for LoopDevice {
150     fn drop(&mut self) {
151         if self.try_detach_when_drop() {
152             if let Err(e) = self.detach() {
153                 log::warn!("Failed to detach loop device: {}", e);
154             }
155         }
156     }
157 }
158 
159 pub struct LoopDeviceBuilder {
160     img_path: Option<PathBuf>,
161     loop_device_path: Option<String>,
162     try_detach_when_drop: bool,
163 }
164 
165 impl LoopDeviceBuilder {
166     pub fn new() -> Self {
167         LoopDeviceBuilder {
168             img_path: None,
169             loop_device_path: None,
170             try_detach_when_drop: true,
171         }
172     }
173 
174     pub fn img_path(mut self, img_path: PathBuf) -> Self {
175         self.img_path = Some(abs_path(&img_path));
176         self
177     }
178 
179     #[allow(dead_code)]
180     pub fn try_detach_when_drop(mut self, try_detach_when_drop: bool) -> Self {
181         self.try_detach_when_drop = try_detach_when_drop;
182         self
183     }
184 
185     pub fn build(self) -> Result<LoopDevice> {
186         let loop_dev = LoopDevice {
187             img_path: self.img_path,
188             loop_device_path: self.loop_device_path,
189             try_detach_when_drop: self.try_detach_when_drop,
190         };
191 
192         Ok(loop_dev)
193     }
194 }
195 
196 fn __loop_device_path_by_disk_image_path(
197     disk_img_path: &str,
198     losetup_a_output: &str,
199 ) -> Result<String> {
200     let re = Regex::new(LOOP_DEVICE_LOSETUP_A_REGEX)?;
201     for line in losetup_a_output.lines() {
202         if !line.contains(disk_img_path) {
203             continue;
204         }
205         let caps = re.captures(line);
206         if caps.is_none() {
207             continue;
208         }
209         let caps = caps.unwrap();
210         let loop_device = caps.get(1).unwrap().as_str();
211         let loop_device = format!("/dev/loop{}", loop_device);
212         return Ok(loop_device);
213     }
214     Err(anyhow!("Loop device not found"))
215 }
216 
217 #[cfg(test)]
218 mod tests {
219     use super::*;
220 
221     #[test]
222     fn test_regex_find_loop_device() {
223         const DEVICE_NAME_SHOULD_MATCH: [&str; 3] =
224             ["/dev/loop11", "/dev/loop11p1", "/dev/loop11p1 "];
225         let device_name = "/dev/loop11";
226         let re = Regex::new(LOOP_DEVICE_LOSETUP_A_REGEX).unwrap();
227         for name in DEVICE_NAME_SHOULD_MATCH {
228             assert!(re.find(name).is_some(), "{} should match", name);
229             assert_eq!(
230                 re.find(name).unwrap().as_str(),
231                 device_name,
232                 "{} should match {}",
233                 name,
234                 device_name
235             );
236         }
237     }
238 
239     #[test]
240     fn test_parse_losetup_a_output() {
241         let losetup_a_output = r#"/dev/loop1: []: (/data/bin/disk-image-x86_64.img)
242 /dev/loop29: []: (/var/lib/abc.img)
243 /dev/loop13: []: (/var/lib/snapd/snaps/gtk-common-themes_1535.snap
244 /dev/loop19: []: (/var/lib/snapd/snaps/gnome-42-2204_172.snap)"#;
245         let disk_img_path = "/data/bin/disk-image-x86_64.img";
246         let loop_device_path =
247             __loop_device_path_by_disk_image_path(disk_img_path, losetup_a_output).unwrap();
248         assert_eq!(loop_device_path, "/dev/loop1");
249     }
250 
251     #[test]
252     fn test_parse_lsblk_output_not_match() {
253         let losetup_a_output = r#"/dev/loop1: []: (/data/bin/disk-image-x86_64.img)
254 /dev/loop29: []: (/var/lib/abc.img)
255 /dev/loop13: []: (/var/lib/snapd/snaps/gtk-common-themes_1535.snap
256 /dev/loop19: []: (/var/lib/snapd/snaps/gnome-42-2204_172.snap)"#;
257         let disk_img_path = "/data/bin/disk-image-riscv64.img";
258         let loop_device_path =
259             __loop_device_path_by_disk_image_path(disk_img_path, losetup_a_output);
260         assert!(
261             loop_device_path.is_err(),
262             "should not match any loop device"
263         );
264     }
265 }
266