mod cli_cancel;
mod cli_output;
use clap::{Parser, Subcommand};
use cli_cancel::CANCEL_EXIT_CODE;
use cli_output::{CliOutput, OutputMode};
use liblitho::io_backend::{clone_io, flash_io};
use liblitho::progress::is_operation_cancelled;
use std::path::PathBuf;
use std::process::ExitCode;
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
#[arg(short = 'o', long = "output-mode", value_enum, default_value_t = OutputMode::Terminal, global = true)]
output_mode: OutputMode,
#[arg(long = "dry-run", global = true, default_value_t = false)]
dry_run: bool,
#[arg(long = "cancel-file", global = true)]
cancel_file: Option<PathBuf>,
#[arg(long = "yes", global = true, default_value_t = false)]
yes: bool,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Clone {
#[arg(short, long)]
file: String,
#[arg(short, long)]
device: String,
#[arg(short, long, default_value_t = 4096)]
block_size: usize,
#[arg(short, long, default_value_t = false)]
silent: bool,
},
Flash {
#[arg(short, long)]
file: String,
#[arg(short, long)]
device: String,
#[arg(short, long, default_value_t = 4096)]
block_size: usize,
#[arg(short, long, default_value_t = false)]
silent: bool,
#[arg(long = "verify", default_value_t = false)]
verify: bool,
},
Query {
#[arg(short, long)]
device: Option<String>,
},
}
fn run(cli: Cli) -> ExitCode {
let mut out = CliOutput::new(cli.output_mode);
match cli.command {
Commands::Clone {
file,
device,
block_size,
silent,
} => run_clone(
&mut out,
&device,
&file,
block_size,
silent,
cli.dry_run,
cli.yes,
cli.cancel_file.as_deref(),
),
Commands::Flash {
file,
device,
block_size,
silent,
verify,
} => run_flash(
&mut out,
&file,
&device,
block_size,
silent,
verify,
cli.dry_run,
cli.yes,
cli.cancel_file.as_deref(),
),
Commands::Query { device } => run_query(&out, device.as_deref()),
}
}
fn confirm_volume_dismount(out: &mut CliOutput, device: &str, yes: bool) -> Result<(), ExitCode> {
let mounts = match liblitho::devices::list_device_mounts(device) {
Ok(mounts) => mounts,
Err(e) => {
out.error(&format!("Could not query volumes on {device}: {e}"));
return Err(ExitCode::FAILURE);
}
};
if mounts.is_empty() {
return Ok(());
}
let summary: Vec<String> = mounts
.iter()
.map(|m| format!("{} on {}", m.source, m.mount_point))
.collect();
if !yes {
out.error(&format!(
"Disk {device} has mounted volumes ({}). Litho will unmount them automatically before writing. \
Re-run with --yes to confirm (all data on the disk will be destroyed).",
summary.join(", ")
));
return Err(ExitCode::FAILURE);
}
out.query_status(&format!(
"Unmounting volumes on {device}: {}",
summary.join(", ")
));
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn run_flash(
out: &mut CliOutput,
file: &str,
device: &str,
block_size: usize,
silent: bool,
verify: bool,
dry_run: bool,
yes: bool,
cancel_file: Option<&std::path::Path>,
) -> ExitCode {
if !dry_run {
if let Err(e) = liblitho::devices::validate_device_for_io(device) {
out.error(&e.to_string());
return ExitCode::FAILURE;
}
if let Err(code) = confirm_volume_dismount(out, device, yes) {
return code;
}
if let Err(e) = liblitho::devices::ensure_device_ready_for_io(device, yes) {
out.error(&e.to_string());
return ExitCode::FAILURE;
}
}
if dry_run {
out.dry_run_ok("flash", file, device, block_size);
return ExitCode::SUCCESS;
}
out.operation_start("Flashing", file, device, block_size);
let cancel = cli_cancel::prepare_operation_cancel();
cli_cancel::spawn_cancel_watchers(cancel.clone(), cancel_file.map(PathBuf::from));
let cancel_ref = Some(cancel.as_ref());
let result = if silent {
flash_io::<fn(liblitho::progress::OperationProgress)>(
file, device, block_size, true, verify, None, cancel_ref,
)
} else {
flash_io(
file,
device,
block_size,
false,
verify,
Some(|event| {
out.on_progress(&event);
}),
cancel_ref,
)
};
out.finish_progress_line();
match result {
Ok(()) => {
out.done_ok("flash");
ExitCode::SUCCESS
}
Err(e) if is_operation_cancelled(&e) => {
out.cancelled("Flash cancelled - device may be partially written.");
ExitCode::from(CANCEL_EXIT_CODE)
}
Err(e) => {
out.error(&e.to_string());
ExitCode::FAILURE
}
}
}
#[allow(clippy::too_many_arguments)]
fn run_clone(
out: &mut CliOutput,
device: &str,
file: &str,
block_size: usize,
silent: bool,
dry_run: bool,
yes: bool,
cancel_file: Option<&std::path::Path>,
) -> ExitCode {
if !dry_run {
if let Err(e) = liblitho::devices::validate_device_for_io(device) {
out.error(&e.to_string());
return ExitCode::FAILURE;
}
if let Err(code) = confirm_volume_dismount(out, device, yes) {
return code;
}
if let Err(e) = liblitho::devices::ensure_device_ready_for_io(device, yes) {
out.error(&e.to_string());
return ExitCode::FAILURE;
}
}
if dry_run {
out.dry_run_ok("clone", device, file, block_size);
return ExitCode::SUCCESS;
}
out.operation_start("Cloning", device, file, block_size);
let cancel = cli_cancel::prepare_operation_cancel();
cli_cancel::spawn_cancel_watchers(cancel.clone(), cancel_file.map(PathBuf::from));
let cancel_ref = Some(cancel.as_ref());
let result = if silent {
clone_io::<fn(liblitho::progress::OperationProgress)>(
device, file, block_size, true, None, cancel_ref,
)
} else {
clone_io(
device,
file,
block_size,
false,
Some(|event| {
out.on_progress(&event);
}),
cancel_ref,
)
};
out.finish_progress_line();
match result {
Ok(()) => {
out.done_ok("clone");
ExitCode::SUCCESS
}
Err(e) if is_operation_cancelled(&e) => {
out.cancelled("Clone cancelled — incomplete output file removed.");
ExitCode::from(CANCEL_EXIT_CODE)
}
Err(e) => {
out.error(&e.to_string());
ExitCode::FAILURE
}
}
}
fn run_query(out: &CliOutput, device: Option<&str>) -> ExitCode {
match device {
Some(path) => {
out.query_status(&format!("Querying device: {path}"));
match liblitho::devices::get_storage_devices() {
Ok(devices) => {
let mut found = false;
for dev in devices {
if device_path_matches(&dev.device_name, path) {
out.query_device(&dev);
found = true;
break;
}
}
if !found {
out.error(&format!("Device not found: {path}"));
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
}
Err(e) => {
out.error(&e.to_string());
ExitCode::FAILURE
}
}
}
None => match liblitho::devices::get_storage_devices() {
Ok(devices) => {
if devices.is_empty() {
out.query_status("No storage devices found");
} else {
for dev in devices {
out.query_device(&dev);
}
}
ExitCode::SUCCESS
}
Err(e) => {
out.error(&e.to_string());
ExitCode::FAILURE
}
},
}
}
fn device_path_matches(device_name: &str, query_path: &str) -> bool {
liblitho::devices::device_paths_equivalent(device_name, query_path)
}
fn main() -> ExitCode {
run(Cli::parse())
}