Skip to main content

btrfs_cli/device/
scan.rs

1use crate::{Format, Runnable};
2use anyhow::{Context, Result};
3use btrfs_uapi::device::{device_forget, device_scan};
4use clap::Parser;
5use std::{
6    ffi::CString,
7    fs,
8    path::{Path, PathBuf},
9};
10
11/// Scan or unregister devices for multi-device btrfs filesystems
12///
13/// Without --forget, registers each given device with the kernel so that
14/// multi-device filesystems can be assembled and mounted.
15///
16/// With --forget, unregisters the given devices (or all stale devices if none
17/// are specified) so the kernel no longer tracks them.
18#[derive(Parser, Debug)]
19pub struct DeviceScanCommand {
20    /// Unregister devices instead of registering them.
21    ///
22    /// If no devices are given, all devices that are not part of a currently
23    /// mounted filesystem are unregistered.
24    #[clap(long, short = 'u', alias = "forget")]
25    pub forget: bool,
26
27    /// Block devices to scan or unregister
28    pub devices: Vec<PathBuf>,
29}
30
31impl Runnable for DeviceScanCommand {
32    fn run(&self, _format: Format, _dry_run: bool) -> Result<()> {
33        if self.forget {
34            if self.devices.is_empty() {
35                device_forget(None).context(
36                    "failed to unregister stale devices from kernel",
37                )?;
38                println!("unregistered all stale devices");
39            } else {
40                let mut had_error = false;
41                for device in &self.devices {
42                    match forget_one(device) {
43                        Ok(()) => {
44                            println!("unregistered '{}'", device.display())
45                        }
46                        Err(e) => {
47                            eprintln!(
48                                "error unregistering '{}': {e}",
49                                device.display()
50                            );
51                            had_error = true;
52                        }
53                    }
54                }
55                if had_error {
56                    anyhow::bail!(
57                        "one or more devices could not be unregistered"
58                    );
59                }
60            }
61        } else if self.devices.is_empty() {
62            scan_all()?;
63        } else {
64            let mut had_error = false;
65            for device in &self.devices {
66                match scan_one(device) {
67                    Ok(()) => println!("registered '{}'", device.display()),
68                    Err(e) => {
69                        eprintln!(
70                            "error registering '{}': {e}",
71                            device.display()
72                        );
73                        had_error = true;
74                    }
75                }
76            }
77            if had_error {
78                anyhow::bail!("one or more devices could not be registered");
79            }
80        }
81
82        Ok(())
83    }
84}
85
86/// Scan all block devices on the system for btrfs filesystems.
87///
88/// Enumerates devices from /proc/partitions and attempts to register each one.
89/// Non-btrfs devices silently fail (the kernel rejects them), so only devices
90/// that the kernel actually recognizes as btrfs are reported.
91fn scan_all() -> Result<()> {
92    let devices = block_devices_from_proc_partitions()
93        .context("failed to enumerate block devices from /proc/partitions")?;
94
95    if devices.is_empty() {
96        println!("no block devices found");
97        return Ok(());
98    }
99
100    let mut registered = 0u32;
101    for device in &devices {
102        let path_str = match device.to_str() {
103            Some(s) => s,
104            None => continue,
105        };
106        let cpath = match CString::new(path_str) {
107            Ok(c) => c,
108            Err(_) => continue,
109        };
110        if device_scan(&cpath).is_ok() {
111            println!("registered '{}'", device.display());
112            registered += 1;
113        }
114    }
115
116    if registered == 0 {
117        println!("no btrfs devices found");
118    }
119
120    Ok(())
121}
122
123/// Parse /proc/partitions and return /dev/ paths for all block devices.
124///
125/// /proc/partitions has a two-line header followed by lines of the form:
126///   major minor #blocks name
127fn block_devices_from_proc_partitions() -> Result<Vec<PathBuf>> {
128    let contents = fs::read_to_string("/proc/partitions")
129        .context("failed to read /proc/partitions")?;
130
131    let mut devices = Vec::new();
132    for line in contents.lines().skip(2) {
133        let name = match line.split_whitespace().nth(3) {
134            Some(n) => n,
135            None => continue,
136        };
137        devices.push(PathBuf::from(format!("/dev/{name}")));
138    }
139    Ok(devices)
140}
141
142fn scan_one(device: &Path) -> Result<()> {
143    let path_str = device.to_str().ok_or_else(|| {
144        anyhow::anyhow!("path is not valid UTF-8: '{}'", device.display())
145    })?;
146    let cpath = CString::new(path_str).with_context(|| {
147        format!("path contains a null byte: '{}'", device.display())
148    })?;
149    device_scan(&cpath).with_context(|| {
150        format!("failed to register '{}'", device.display())
151    })?;
152    Ok(())
153}
154
155fn forget_one(device: &Path) -> Result<()> {
156    let path_str = device.to_str().ok_or_else(|| {
157        anyhow::anyhow!("path is not valid UTF-8: '{}'", device.display())
158    })?;
159    let cpath = CString::new(path_str).with_context(|| {
160        format!("path contains a null byte: '{}'", device.display())
161    })?;
162    device_forget(Some(&cpath)).with_context(|| {
163        format!("failed to unregister '{}'", device.display())
164    })?;
165    Ok(())
166}