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