use crate::{
RunContext, Runnable,
util::{check_device_for_overwrite, open_path},
};
use anyhow::{Context, Result, bail};
use btrfs_uapi::{
filesystem::filesystem_info,
replace::{ReplaceSource, ReplaceState, replace_start, replace_status},
sysfs::SysfsBtrfs,
};
use clap::Parser;
use std::{
ffi::CString, fs, os::unix::io::AsFd, path::PathBuf, thread, time::Duration,
};
#[derive(Parser, Debug)]
#[allow(clippy::doc_markdown, clippy::struct_excessive_bools)]
pub struct ReplaceStartCommand {
pub source: String,
pub target: PathBuf,
pub mount_point: PathBuf,
#[clap(short = 'r', long)]
pub redundancy_only: bool,
#[clap(short = 'f', long)]
pub force: bool,
#[clap(short = 'B', long)]
pub no_background: bool,
#[clap(long)]
pub enqueue: bool,
#[clap(short = 'K', long)]
pub nodiscard: bool,
}
impl Runnable for ReplaceStartCommand {
#[allow(clippy::too_many_lines)]
fn run(&self, _ctx: &RunContext) -> Result<()> {
check_device_for_overwrite(&self.target, self.force)?;
let file = open_path(&self.mount_point)?;
let fd = file.as_fd();
if self.enqueue {
let info = filesystem_info(fd).with_context(|| {
format!(
"failed to get filesystem info for '{}'",
self.mount_point.display()
)
})?;
let sysfs = SysfsBtrfs::new(&info.uuid);
let op =
sysfs.wait_for_exclusive_operation().with_context(|| {
format!(
"failed to check exclusive operation on '{}'",
self.mount_point.display()
)
})?;
if op != "none" {
eprintln!("waited for exclusive operation '{op}' to finish");
}
}
let current = replace_status(fd).with_context(|| {
format!(
"failed to get replace status on '{}'",
self.mount_point.display()
)
})?;
if current.state == ReplaceState::Started {
bail!(
"a device replace operation is already in progress on '{}'",
self.mount_point.display()
);
}
let source = if let Ok(devid) = self.source.parse::<u64>() {
ReplaceSource::DevId(devid)
} else {
ReplaceSource::Path(
&CString::new(self.source.as_bytes()).with_context(|| {
format!("invalid source device path '{}'", self.source)
})?,
)
};
let tgtdev = CString::new(self.target.as_os_str().as_encoded_bytes())
.with_context(|| {
format!("invalid target device path '{}'", self.target.display())
})?;
if !self.nodiscard {
let tgtfile = fs::OpenOptions::new()
.write(true)
.open(&self.target)
.with_context(|| {
format!(
"failed to open target device '{}' for discard",
self.target.display()
)
})?;
match btrfs_uapi::blkdev::discard_whole_device(tgtfile.as_fd()) {
Ok(0) => {}
Ok(_) => eprintln!(
"discarded target device '{}'",
self.target.display()
),
Err(e) => {
eprintln!(
"warning: discard failed on '{}': {e}; continuing anyway",
self.target.display()
);
}
}
}
match replace_start(fd, &source, &tgtdev, self.redundancy_only)
.with_context(|| {
format!(
"failed to start replace on '{}'",
self.mount_point.display()
)
})? {
Ok(()) => {}
Err(e) => bail!("{e}"),
}
println!(
"replace started: {} -> {}",
self.source,
self.target.display(),
);
if self.no_background {
loop {
thread::sleep(Duration::from_secs(1));
let status = replace_status(fd).with_context(|| {
format!(
"failed to get replace status on '{}'",
self.mount_point.display()
)
})?;
#[allow(clippy::cast_precision_loss)]
let pct = status.progress_1000 as f64 / 10.0;
eprint!(
"\r{pct:.1}% done, {} write errs, {} uncorr. read errs",
status.num_write_errors,
status.num_uncorrectable_read_errors,
);
if status.state != ReplaceState::Started {
eprintln!();
match status.state {
ReplaceState::Finished => {
println!("replace finished successfully");
}
ReplaceState::Canceled => {
bail!("replace was cancelled");
}
_ => {
bail!(
"replace ended in unexpected state: {:?}",
status.state
);
}
}
break;
}
}
}
Ok(())
}
}