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 {
attached(&self) -> bool18 pub fn attached(&self) -> bool {
19 self.loop_device_path.is_some()
20 }
21
dev_path(&self) -> Option<&String>22 pub fn dev_path(&self) -> Option<&String> {
23 self.loop_device_path.as_ref()
24 }
25
attach(&mut self) -> Result<()>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
attach_by_exists(&mut self) -> Result<()>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")` 错误。
partition_path(&self, nth: u8) -> Result<PathBuf>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
detach(&mut self) -> Result<()>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
try_detach_when_drop(&self) -> bool139 pub fn try_detach_when_drop(&self) -> bool {
140 self.try_detach_when_drop
141 }
142
143 #[allow(dead_code)]
set_try_detach_when_drop(&mut self, try_detach_when_drop: bool)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 {
drop(&mut self)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 {
new() -> Self166 pub fn new() -> Self {
167 LoopDeviceBuilder {
168 img_path: None,
169 loop_device_path: None,
170 try_detach_when_drop: true,
171 }
172 }
173
img_path(mut self, img_path: PathBuf) -> Self174 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)]
try_detach_when_drop(mut self, try_detach_when_drop: bool) -> Self180 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
build(self) -> Result<LoopDevice>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
__loop_device_path_by_disk_image_path( disk_img_path: &str, losetup_a_output: &str, ) -> Result<String>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]
test_regex_find_loop_device()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]
test_parse_losetup_a_output()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]
test_parse_lsblk_output_not_match()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