Skip to main content

btrfs_cli/scrub/
start.rs

1use crate::{
2    Format, RunContext, Runnable,
3    util::{SizeFormat, fmt_size, 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 cols::Cols;
14use console::Term;
15use std::{
16    os::unix::io::{AsFd, AsRawFd},
17    path::PathBuf,
18    thread,
19    time::Duration,
20};
21
22/// Start a new scrub on the filesystem or a device.
23///
24/// Scrubs all devices sequentially. This command blocks until the scrub
25/// completes; use Ctrl-C to cancel.
26#[derive(Parser, Debug)]
27#[allow(clippy::struct_excessive_bools)]
28pub struct ScrubStartCommand {
29    /// Do not background (default behavior, accepted for compatibility)
30    #[clap(short = 'B')]
31    pub no_background: bool,
32
33    /// Stats per device
34    #[clap(long, short)]
35    pub device: bool,
36
37    /// Read-only mode: check for errors but do not attempt repairs
38    #[clap(long, short)]
39    pub readonly: bool,
40
41    /// Print full raw data instead of summary
42    #[clap(short = 'R', long)]
43    pub raw: bool,
44
45    /// Force starting new scrub even if a scrub is already running
46    #[clap(long, short)]
47    pub force: bool,
48
49    /// Set the throughput limit for each device (0 for unlimited), restored
50    /// afterwards
51    #[clap(long, value_name = "SIZE", value_parser = parse_size_with_suffix)]
52    pub limit: Option<u64>,
53
54    /// Set ioprio class (see ionice(1) manpage)
55    #[clap(short = 'c', value_name = "CLASS")]
56    pub ioprio_class: Option<i32>,
57
58    /// Set ioprio classdata (see ionice(1) manpage)
59    #[clap(short = 'n', value_name = "CDATA")]
60    pub ioprio_classdata: Option<i32>,
61
62    /// Path to a mounted btrfs filesystem or a device
63    pub path: PathBuf,
64}
65
66impl Runnable for ScrubStartCommand {
67    fn run(&self, ctx: &RunContext) -> Result<()> {
68        let file = open_path(&self.path)?;
69        let fd = file.as_fd();
70
71        let fs = filesystem_info(fd).with_context(|| {
72            format!(
73                "failed to get filesystem info for '{}'",
74                self.path.display()
75            )
76        })?;
77        let devices = device_info_all(fd, &fs).with_context(|| {
78            format!("failed to get device info for '{}'", self.path.display())
79        })?;
80
81        if !self.force {
82            for dev in &devices {
83                if scrub_progress(fd, dev.devid)
84                    .with_context(|| {
85                        format!(
86                            "failed to check scrub status for device {}",
87                            dev.devid
88                        )
89                    })?
90                    .is_some()
91                {
92                    bail!(
93                        "Scrub is already running.\n\
94                         To cancel use 'btrfs scrub cancel {path}'.\n\
95                         To see the status use 'btrfs scrub status {path}'",
96                        path = self.path.display()
97                    );
98                }
99            }
100        }
101
102        let sysfs = SysfsBtrfs::new(&fs.uuid);
103        let old_limits = self.apply_limits(&sysfs, &devices)?;
104
105        if self.ioprio_class.is_some() || self.ioprio_classdata.is_some() {
106            super::set_ioprio(
107                self.ioprio_class.unwrap_or(3), // default: idle
108                self.ioprio_classdata.unwrap_or(0),
109            );
110        }
111
112        println!("UUID: {}", fs.uuid.as_hyphenated());
113
114        let mode = SizeFormat::HumanIec;
115
116        match ctx.format {
117            Format::Modern => {
118                self.run_modern(fd, &devices, &mode, ctx.quiet, &self.path);
119            }
120            Format::Text => {
121                self.run_text(fd, &devices, &mode);
122            }
123            Format::Json => unreachable!(),
124        }
125
126        self.restore_limits(&sysfs, &old_limits);
127
128        Ok(())
129    }
130}
131
132impl ScrubStartCommand {
133    fn run_text(
134        &self,
135        fd: std::os::unix::io::BorrowedFd,
136        devices: &[btrfs_uapi::device::DeviceInfo],
137        mode: &SizeFormat,
138    ) {
139        let mut fs_totals = btrfs_uapi::scrub::ScrubProgress::default();
140
141        for dev in devices {
142            println!("scrubbing device {} ({})", dev.devid, dev.path);
143
144            match scrub_start(fd, dev.devid, self.readonly) {
145                Ok(progress) => {
146                    super::accumulate(&mut fs_totals, &progress);
147                    if self.device {
148                        super::print_device_progress(
149                            &progress, dev.devid, &dev.path, self.raw, mode,
150                        );
151                    }
152                }
153                Err(e) => {
154                    eprintln!("error scrubbing device {}: {e}", dev.devid);
155                }
156            }
157        }
158
159        if !self.device {
160            if self.raw {
161                super::print_raw_progress(&fs_totals, 0, "filesystem totals");
162            } else {
163                super::print_error_summary(&fs_totals);
164            }
165        } else if devices.len() > 1 {
166            println!("\nFilesystem totals:");
167            if self.raw {
168                super::print_raw_progress(&fs_totals, 0, "filesystem totals");
169            } else {
170                super::print_error_summary(&fs_totals);
171            }
172        }
173    }
174
175    #[allow(clippy::too_many_lines)]
176    fn run_modern(
177        &self,
178        fd: std::os::unix::io::BorrowedFd,
179        devices: &[btrfs_uapi::device::DeviceInfo],
180        mode: &SizeFormat,
181        quiet: bool,
182        mount_path: &std::path::Path,
183    ) {
184        let term = Term::stderr();
185        let is_term = term.is_term();
186        let poll_interval = if is_term {
187            Duration::from_millis(200)
188        } else {
189            Duration::from_secs(1)
190        };
191
192        let mut fs_totals = btrfs_uapi::scrub::ScrubProgress::default();
193        let mut dev_results: Vec<(
194            u64,
195            String,
196            btrfs_uapi::scrub::ScrubProgress,
197        )> = Vec::new();
198
199        for dev in devices {
200            let readonly = self.readonly;
201            let devid = dev.devid;
202            let dev_used = dev.bytes_used;
203
204            // Spawn the blocking scrub on a background thread.
205            // SAFETY: the fd outlives the thread (we join before returning),
206            // and the ioctl is safe to call from any thread.
207            let raw_fd = fd.as_raw_fd();
208            let handle = thread::spawn(move || {
209                use std::os::unix::io::BorrowedFd;
210                let fd = unsafe { BorrowedFd::borrow_raw(raw_fd) };
211                scrub_start(fd, devid, readonly)
212            });
213
214            // Poll progress while the scrub thread is running.
215            if !quiet {
216                while !handle.is_finished() {
217                    if let Ok(Some(progress)) = scrub_progress(fd, devid) {
218                        let msg =
219                            format_progress(devid, &progress, dev_used, mode);
220                        if is_term {
221                            let _ = term.clear_line();
222                            let _ = term.write_str(&msg);
223                        } else {
224                            let _ = term.write_line(&msg);
225                        }
226                    }
227                    thread::sleep(poll_interval);
228                }
229
230                if is_term {
231                    let _ = term.clear_line();
232                }
233            }
234
235            match handle.join().unwrap() {
236                Ok(progress) => {
237                    super::accumulate(&mut fs_totals, &progress);
238                    dev_results.push((devid, dev.path.clone(), progress));
239                }
240                Err(e) => {
241                    eprintln!("error scrubbing device {devid}: {e}");
242                }
243            }
244        }
245
246        if self.raw {
247            // Raw mode: filesystem as root, devices with counter children.
248            let mp = mount_path.display().to_string();
249            let mut root = ScrubRawRow {
250                name: "filesystem".to_string(),
251                value: mp,
252                children: dev_results
253                    .iter()
254                    .map(|(devid, path, p)| {
255                        scrub_raw_row(&format!("devid {devid}"), path, p)
256                    })
257                    .collect(),
258            };
259
260            // For multi-device, add an aggregated totals row at the end.
261            if dev_results.len() > 1 {
262                root.children.push(scrub_raw_row("totals", "", &fs_totals));
263            }
264
265            let mut out = std::io::stdout().lock();
266            let _ = ScrubRawRow::print_table(&[root], &mut out);
267        } else {
268            // Summary mode: compact tree with key stats.
269            let mp = mount_path.display().to_string();
270            let mut root =
271                scrub_result_row("filesystem", &mp, &fs_totals, mode);
272            root.children = dev_results
273                .iter()
274                .map(|(devid, path, p)| {
275                    scrub_result_row(&format!("devid {devid}"), path, p, mode)
276                })
277                .collect();
278
279            let mut out = std::io::stdout().lock();
280            let _ = ScrubResultRow::print_table(&[root], &mut out);
281        }
282
283        if fs_totals.malloc_errors > 0 {
284            eprintln!(
285                "WARNING: {} memory allocation error(s) during scrub — results may be incomplete",
286                fs_totals.malloc_errors
287            );
288        }
289    }
290
291    fn apply_limits(
292        &self,
293        sysfs: &SysfsBtrfs,
294        devices: &[btrfs_uapi::device::DeviceInfo],
295    ) -> Result<Vec<(u64, u64)>> {
296        let mut old_limits = Vec::new();
297        if let Some(limit) = self.limit {
298            for dev in devices {
299                let old = sysfs.scrub_speed_max_get(dev.devid).with_context(
300                    || {
301                        format!(
302                            "failed to read scrub limit for devid {}",
303                            dev.devid
304                        )
305                    },
306                )?;
307                old_limits.push((dev.devid, old));
308                sysfs.scrub_speed_max_set(dev.devid, limit).with_context(
309                    || {
310                        format!(
311                            "failed to set scrub limit for devid {}",
312                            dev.devid
313                        )
314                    },
315                )?;
316            }
317        }
318        Ok(old_limits)
319    }
320
321    #[allow(clippy::unused_self)] // method kept on the command struct for consistency
322    fn restore_limits(&self, sysfs: &SysfsBtrfs, old_limits: &[(u64, u64)]) {
323        for &(devid, old_limit) in old_limits {
324            if let Err(e) = sysfs.scrub_speed_max_set(devid, old_limit) {
325                eprintln!(
326                    "WARNING: failed to restore scrub limit for devid {devid}: {e}"
327                );
328            }
329        }
330    }
331}
332
333#[derive(Cols)]
334struct ScrubResultRow {
335    #[column(tree)]
336    name: String,
337    #[column(header = "PATH", wrap)]
338    path: String,
339    #[column(header = "DATA", right)]
340    data: String,
341    #[column(header = "META", right)]
342    meta: String,
343    #[column(header = "CORRECTED", right)]
344    corrected: String,
345    #[column(header = "UNCORRECTABLE", right)]
346    uncorrectable: String,
347    #[column(header = "UNVERIFIED", right)]
348    unverified: String,
349    #[column(children)]
350    children: Vec<Self>,
351}
352
353fn scrub_result_row(
354    name: &str,
355    path: &str,
356    p: &btrfs_uapi::scrub::ScrubProgress,
357    mode: &SizeFormat,
358) -> ScrubResultRow {
359    ScrubResultRow {
360        name: name.to_string(),
361        path: path.to_string(),
362        data: fmt_size(p.data_bytes_scrubbed, mode),
363        meta: fmt_size(p.tree_bytes_scrubbed, mode),
364        corrected: p.corrected_errors.to_string(),
365        uncorrectable: p.uncorrectable_errors.to_string(),
366        unverified: p.unverified_errors.to_string(),
367        children: Vec::new(),
368    }
369}
370
371#[derive(Cols)]
372struct ScrubRawRow {
373    #[column(tree)]
374    name: String,
375    #[column(header = "VALUE", right, wrap)]
376    value: String,
377    #[column(children)]
378    children: Vec<Self>,
379}
380
381fn scrub_raw_row(
382    name: &str,
383    value: &str,
384    p: &btrfs_uapi::scrub::ScrubProgress,
385) -> ScrubRawRow {
386    let counters = vec![
387        ("data_extents_scrubbed", p.data_extents_scrubbed),
388        ("tree_extents_scrubbed", p.tree_extents_scrubbed),
389        ("data_bytes_scrubbed", p.data_bytes_scrubbed),
390        ("tree_bytes_scrubbed", p.tree_bytes_scrubbed),
391        ("read_errors", p.read_errors),
392        ("csum_errors", p.csum_errors),
393        ("verify_errors", p.verify_errors),
394        ("no_csum", p.no_csum),
395        ("csum_discards", p.csum_discards),
396        ("super_errors", p.super_errors),
397        ("malloc_errors", p.malloc_errors),
398        ("uncorrectable_errors", p.uncorrectable_errors),
399        ("corrected_errors", p.corrected_errors),
400        ("unverified_errors", p.unverified_errors),
401        ("last_physical", p.last_physical),
402    ];
403    ScrubRawRow {
404        name: name.to_string(),
405        value: value.to_string(),
406        children: counters
407            .into_iter()
408            .map(|(k, v)| ScrubRawRow {
409                name: k.to_string(),
410                value: v.to_string(),
411                children: Vec::new(),
412            })
413            .collect(),
414    }
415}
416
417fn format_progress(
418    devid: u64,
419    progress: &btrfs_uapi::scrub::ScrubProgress,
420    dev_used: u64,
421    mode: &SizeFormat,
422) -> String {
423    let scrubbed = progress.bytes_scrubbed();
424    let errors = super::status::format_error_count(progress);
425    format!(
426        "scrubbing devid {devid}: {}/~{} ({errors})",
427        fmt_size(scrubbed, mode),
428        fmt_size(dev_used, mode),
429    )
430}