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