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