Skip to main content

btrfs_cli/scrub/
start.rs

1use crate::{
2    Format, Runnable,
3    util::{SizeFormat, open_path, parse_size_with_suffix},
4};
5use anyhow::{Context, Result, bail};
6use btrfs_uapi::{
7    device::device_info_all,
8    filesystem::filesystem_info,
9    scrub::{scrub_progress, scrub_start},
10    sysfs::SysfsBtrfs,
11};
12use clap::Parser;
13use std::{os::unix::io::AsFd, path::PathBuf};
14
15/// Start a new scrub on the filesystem or a device.
16///
17/// Scrubs all devices sequentially. This command blocks until the scrub
18/// completes; use Ctrl-C to cancel.
19#[derive(Parser, Debug)]
20#[allow(clippy::struct_excessive_bools)]
21pub struct ScrubStartCommand {
22    /// Do not background (default behavior, accepted for compatibility)
23    #[clap(short = 'B')]
24    pub no_background: bool,
25
26    /// Stats per device
27    #[clap(long, short)]
28    pub device: bool,
29
30    /// Read-only mode: check for errors but do not attempt repairs
31    #[clap(long, short)]
32    pub readonly: bool,
33
34    /// Print full raw data instead of summary
35    #[clap(short = 'R')]
36    pub raw: bool,
37
38    /// Force starting new scrub even if a scrub is already running
39    #[clap(long, short)]
40    pub force: bool,
41
42    /// Set the throughput limit for each device (0 for unlimited), restored
43    /// afterwards
44    #[clap(long, value_name = "SIZE", value_parser = parse_size_with_suffix)]
45    pub limit: Option<u64>,
46
47    /// Set ioprio class (see ionice(1) manpage)
48    #[clap(short = 'c', value_name = "CLASS")]
49    pub ioprio_class: Option<i32>,
50
51    /// Set ioprio classdata (see ionice(1) manpage)
52    #[clap(short = 'n', value_name = "CDATA")]
53    pub ioprio_classdata: Option<i32>,
54
55    /// Path to a mounted btrfs filesystem or a device
56    pub path: PathBuf,
57}
58
59impl Runnable for ScrubStartCommand {
60    fn run(&self, _format: Format, _dry_run: bool) -> Result<()> {
61        let file = open_path(&self.path)?;
62        let fd = file.as_fd();
63
64        let fs = filesystem_info(fd).with_context(|| {
65            format!(
66                "failed to get filesystem info for '{}'",
67                self.path.display()
68            )
69        })?;
70        let devices = device_info_all(fd, &fs).with_context(|| {
71            format!("failed to get device info for '{}'", self.path.display())
72        })?;
73
74        if !self.force {
75            for dev in &devices {
76                if scrub_progress(fd, dev.devid)
77                    .with_context(|| {
78                        format!(
79                            "failed to check scrub status for device {}",
80                            dev.devid
81                        )
82                    })?
83                    .is_some()
84                {
85                    bail!(
86                        "Scrub is already running.\n\
87                         To cancel use 'btrfs scrub cancel {path}'.\n\
88                         To see the status use 'btrfs scrub status {path}'",
89                        path = self.path.display()
90                    );
91                }
92            }
93        }
94
95        let sysfs = SysfsBtrfs::new(&fs.uuid);
96        let old_limits = self.apply_limits(&sysfs, &devices)?;
97
98        if self.ioprio_class.is_some() || self.ioprio_classdata.is_some() {
99            super::set_ioprio(
100                self.ioprio_class.unwrap_or(3), // default: idle
101                self.ioprio_classdata.unwrap_or(0),
102            );
103        }
104
105        println!("UUID: {}", fs.uuid.as_hyphenated());
106
107        let mode = SizeFormat::HumanIec;
108        let mut fs_totals = btrfs_uapi::scrub::ScrubProgress::default();
109
110        for dev in &devices {
111            println!("scrubbing device {} ({})", dev.devid, dev.path);
112
113            match scrub_start(fd, dev.devid, self.readonly) {
114                Ok(progress) => {
115                    super::accumulate(&mut fs_totals, &progress);
116                    if self.device {
117                        super::print_device_progress(
118                            &progress, dev.devid, &dev.path, self.raw, &mode,
119                        );
120                    }
121                }
122                Err(e) => {
123                    eprintln!("error scrubbing device {}: {e}", dev.devid);
124                }
125            }
126        }
127
128        if !self.device {
129            if self.raw {
130                super::print_raw_progress(&fs_totals, 0, "filesystem totals");
131            } else {
132                super::print_error_summary(&fs_totals);
133            }
134        } else if devices.len() > 1 {
135            println!("\nFilesystem totals:");
136            if self.raw {
137                super::print_raw_progress(&fs_totals, 0, "filesystem totals");
138            } else {
139                super::print_error_summary(&fs_totals);
140            }
141        }
142
143        self.restore_limits(&sysfs, &old_limits);
144
145        Ok(())
146    }
147}
148
149impl ScrubStartCommand {
150    fn apply_limits(
151        &self,
152        sysfs: &SysfsBtrfs,
153        devices: &[btrfs_uapi::device::DeviceInfo],
154    ) -> Result<Vec<(u64, u64)>> {
155        let mut old_limits = Vec::new();
156        if let Some(limit) = self.limit {
157            for dev in devices {
158                let old = sysfs.scrub_speed_max_get(dev.devid).with_context(
159                    || {
160                        format!(
161                            "failed to read scrub limit for devid {}",
162                            dev.devid
163                        )
164                    },
165                )?;
166                old_limits.push((dev.devid, old));
167                sysfs.scrub_speed_max_set(dev.devid, limit).with_context(
168                    || {
169                        format!(
170                            "failed to set scrub limit for devid {}",
171                            dev.devid
172                        )
173                    },
174                )?;
175            }
176        }
177        Ok(old_limits)
178    }
179
180    #[allow(clippy::unused_self)] // method kept on the command struct for consistency
181    fn restore_limits(&self, sysfs: &SysfsBtrfs, old_limits: &[(u64, u64)]) {
182        for &(devid, old_limit) in old_limits {
183            if let Err(e) = sysfs.scrub_speed_max_set(devid, old_limit) {
184                eprintln!(
185                    "WARNING: failed to restore scrub limit for devid {devid}: {e}"
186                );
187            }
188        }
189    }
190}