Skip to main content

btrfs_cli/replace/
status.rs

1use crate::{Format, Runnable};
2use anyhow::{Context, Result};
3use btrfs_uapi::replace::{ReplaceState, replace_status};
4use clap::Parser;
5use std::{
6    fs::File, io::Write, os::unix::io::AsFd, path::PathBuf, thread,
7    time::Duration,
8};
9
10/// Print status of a running device replace operation.
11///
12/// Without -1 the status is printed continuously until the replace operation
13/// finishes or is cancelled. With -1 the status is printed once and the
14/// command exits.
15#[derive(Parser, Debug)]
16pub struct ReplaceStatusCommand {
17    /// Print once instead of continuously until the replace finishes
18    #[clap(short = '1', long)]
19    pub once: bool,
20
21    /// Path to a mounted btrfs filesystem
22    pub mount_point: PathBuf,
23}
24
25fn format_time(t: &std::time::SystemTime) -> String {
26    let secs = t
27        .duration_since(std::time::UNIX_EPOCH)
28        .unwrap_or_default()
29        .as_secs();
30
31    // Use libc::localtime_r for locale-aware formatting, matching the
32    // pattern used elsewhere in the codebase (subvolume show).
33    let secs_i64 = secs as nix::libc::time_t;
34    let mut tm: nix::libc::tm = unsafe { std::mem::zeroed() };
35    unsafe { nix::libc::localtime_r(&secs_i64, &mut tm) };
36
37    // Format as "%e.%b %T" to match btrfs-progs output.
38    let mut buf = [0u8; 64];
39    let fmt = b"%e.%b %T\0";
40    let len = unsafe {
41        nix::libc::strftime(
42            buf.as_mut_ptr() as *mut nix::libc::c_char,
43            buf.len(),
44            fmt.as_ptr() as *const nix::libc::c_char,
45            &tm,
46        )
47    };
48    String::from_utf8_lossy(&buf[..len]).into_owned()
49}
50
51impl Runnable for ReplaceStatusCommand {
52    fn run(&self, _format: Format, _dry_run: bool) -> Result<()> {
53        let file = File::open(&self.mount_point).with_context(|| {
54            format!("failed to open '{}'", self.mount_point.display())
55        })?;
56        let fd = file.as_fd();
57
58        loop {
59            let status = replace_status(fd).with_context(|| {
60                format!(
61                    "failed to get replace status on '{}'",
62                    self.mount_point.display()
63                )
64            })?;
65
66            let line = match status.state {
67                ReplaceState::NeverStarted => "Never started".to_string(),
68                ReplaceState::Started => {
69                    let pct = status.progress_1000 as f64 / 10.0;
70                    format!(
71                        "{pct:.1}% done, {} write errs, {} uncorr. read errs",
72                        status.num_write_errors,
73                        status.num_uncorrectable_read_errors,
74                    )
75                }
76                ReplaceState::Finished => {
77                    let started = status
78                        .time_started
79                        .map(|t| format_time(&t))
80                        .unwrap_or_default();
81                    let stopped = status
82                        .time_stopped
83                        .map(|t| format_time(&t))
84                        .unwrap_or_default();
85                    format!(
86                        "Started on {started}, finished on {stopped}, \
87                         {} write errs, {} uncorr. read errs",
88                        status.num_write_errors,
89                        status.num_uncorrectable_read_errors,
90                    )
91                }
92                ReplaceState::Canceled => {
93                    let started = status
94                        .time_started
95                        .map(|t| format_time(&t))
96                        .unwrap_or_default();
97                    let stopped = status
98                        .time_stopped
99                        .map(|t| format_time(&t))
100                        .unwrap_or_default();
101                    let pct = status.progress_1000 as f64 / 10.0;
102                    format!(
103                        "Started on {started}, canceled on {stopped} at {pct:.1}%, \
104                         {} write errs, {} uncorr. read errs",
105                        status.num_write_errors,
106                        status.num_uncorrectable_read_errors,
107                    )
108                }
109                ReplaceState::Suspended => {
110                    let started = status
111                        .time_started
112                        .map(|t| format_time(&t))
113                        .unwrap_or_default();
114                    let stopped = status
115                        .time_stopped
116                        .map(|t| format_time(&t))
117                        .unwrap_or_default();
118                    let pct = status.progress_1000 as f64 / 10.0;
119                    format!(
120                        "Started on {started}, suspended on {stopped} at {pct:.1}%, \
121                         {} write errs, {} uncorr. read errs",
122                        status.num_write_errors,
123                        status.num_uncorrectable_read_errors,
124                    )
125                }
126            };
127
128            print!("\r{line}");
129            std::io::stdout().flush()?;
130
131            // Terminal states or one-shot mode: print newline and exit.
132            if self.once || status.state != ReplaceState::Started {
133                println!();
134                break;
135            }
136
137            thread::sleep(Duration::from_secs(1));
138        }
139
140        Ok(())
141    }
142}