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#[derive(Parser, Debug)]
27#[allow(clippy::struct_excessive_bools)]
28pub struct ScrubStartCommand {
29 #[clap(short = 'B')]
31 pub no_background: bool,
32
33 #[clap(long, short)]
35 pub device: bool,
36
37 #[clap(long, short)]
39 pub readonly: bool,
40
41 #[clap(short = 'R', long)]
43 pub raw: bool,
44
45 #[clap(long, short)]
47 pub force: bool,
48
49 #[clap(long, value_name = "SIZE", value_parser = parse_size_with_suffix)]
52 pub limit: Option<u64>,
53
54 #[clap(short = 'c', value_name = "CLASS")]
56 pub ioprio_class: Option<i32>,
57
58 #[clap(short = 'n', value_name = "CDATA")]
60 pub ioprio_classdata: Option<i32>,
61
62 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), 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 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 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 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 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 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)] 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}