From c8dffb9609111c9cd07e6e464eff54ac2d662257 Mon Sep 17 00:00:00 2001 From: P-Storm Date: Thu, 17 Jul 2025 22:35:35 +0200 Subject: [PATCH 1/2] cli_commands: Moved everything probe related to their own module and restructured everything to support it. --- src/bin/bmputil-cli/cli_commands/mod.rs | 74 +++ .../cli_commands/probe.rs} | 598 +++++------------- src/bin/bmputil-cli/main.rs | 265 ++++++++ 3 files changed, 484 insertions(+), 453 deletions(-) create mode 100644 src/bin/bmputil-cli/cli_commands/mod.rs rename src/bin/{bmputil-cli.rs => bmputil-cli/cli_commands/probe.rs} (56%) create mode 100644 src/bin/bmputil-cli/main.rs diff --git a/src/bin/bmputil-cli/cli_commands/mod.rs b/src/bin/bmputil-cli/cli_commands/mod.rs new file mode 100644 index 0000000..e994c1a --- /dev/null +++ b/src/bin/bmputil-cli/cli_commands/mod.rs @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// SPDX-FileCopyrightText: 2022-2025 1BitSquared +// SPDX-FileContributor: Written by P-Storm +// SPDX-FileContributor: Modified by P-Storm + +use bmputil::bmp::FirmwareType; +use bmputil::{AllowDangerous, BmpParams, FlashParams}; +use clap::Subcommand; +use directories::ProjectDirs; +use log::error; + +use crate::cli_commands::probe::ProbeArguments; +use crate::{CliArguments, CompletionArguments}; + +pub mod probe; + +#[derive(Subcommand)] +pub enum ToplevelCommmands +{ + /// Actions to be performed against a probe + Probe(ProbeArguments), + /// Actions to be performed against a target connected to a probe + Target, + /// Actions that run the tool as a debug/tracing server + Server, + /// Actions that run debugging commands against a target connected to a probe + Debug, + /// Generate completions data for the shell + Complete(CompletionArguments), +} + +impl FlashParams for CliArguments +{ + fn allow_dangerous_options(&self) -> AllowDangerous + { + match &self.subcommand { + ToplevelCommmands::Probe(probe_args) => probe_args.allow_dangerous_options(), + _ => AllowDangerous::Never, + } + } + + fn override_firmware_type(&self) -> Option + { + match &self.subcommand { + ToplevelCommmands::Probe(probe_args) => probe_args.override_firmware_type(), + _ => None, + } + } +} + +impl BmpParams for CliArguments +{ + fn index(&self) -> Option + { + self.index + } + + fn serial_number(&self) -> Option<&str> + { + self.serial_number.as_deref() + } +} + +fn paths() -> ProjectDirs +{ + // Try to get the application paths available + match ProjectDirs::from("org", "black-magic", "bmputil") { + Some(paths) => paths, + None => { + error!("Failed to get program working paths"); + std::process::exit(2); + }, + } +} diff --git a/src/bin/bmputil-cli.rs b/src/bin/bmputil-cli/cli_commands/probe.rs similarity index 56% rename from src/bin/bmputil-cli.rs rename to src/bin/bmputil-cli/cli_commands/probe.rs index f2338f7..f329cbd 100644 --- a/src/bin/bmputil-cli.rs +++ b/src/bin/bmputil-cli/cli_commands/probe.rs @@ -1,75 +1,21 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 // SPDX-FileCopyrightText: 2022-2025 1BitSquared -// SPDX-FileContributor: Written by Mikaela Szekely -// SPDX-FileContributor: Modified by Rachel Mant +// SPDX-FileContributor: Written by P-Storm // SPDX-FileContributor: Modified by P-Storm -use std::ffi::OsStr; -use std::io::stdout; -use std::str::FromStr; - +use bmputil::AllowDangerous; use bmputil::bmp::{BmpDevice, BmpMatcher, FirmwareType}; use bmputil::metadata::download_metadata; -#[cfg(windows)] -use bmputil::windows; -use bmputil::{AllowDangerous, BmpParams, FlashParams}; -use clap::builder::TypedValueParser; -use clap::builder::styling::Styles; -use clap::{Arg, ArgAction, Args, Command, CommandFactory, Parser, Subcommand, crate_description, crate_version}; -use clap_complete::{Shell, generate}; -use color_eyre::config::HookBuilder; -use color_eyre::eyre::{Context, EyreHandler, InstallError, OptionExt, Result}; +use clap::{ArgAction, Args, Subcommand}; +use color_eyre::eyre::{Context, OptionExt}; use directories::ProjectDirs; use log::{debug, error, info, warn}; -use owo_colors::OwoColorize; - -#[derive(Parser)] -#[command( - version, - about = format!("{} v{}", crate_description!(), crate_version!()), - styles(style()), - disable_colored_help(false), - arg_required_else_help(true) -)] -struct CliArguments -{ - #[arg(global = true, short = 's', long = "serial", alias = "serial-number")] - /// Use the device with the given serial number - serial_number: Option, - #[arg(global = true, long = "index", value_parser = usize::from_str)] - /// Use the nth found device (may be unstable!) - index: Option, - #[arg(global = true, short = 'p', long = "port")] - /// Use the device on the given USB port - port: Option, - - #[cfg(windows)] - #[arg(global = true, long = "windows-wdi-install-mode", value_parser = u32::from_str, hide = true)] - /// Internal argument used when re-executing this command to acquire admin for installing drivers - windows_wdi_install_mode: Option, - - #[command(subcommand)] - pub subcommand: ToplevelCommmands, -} -#[derive(Subcommand)] -enum ToplevelCommmands -{ - /// Actions to be performed against a probe - Probe(ProbeArguments), - /// Actions to be performed against a target connected to a probe - #[command(subcommand)] - Target(TargetCommmands), - /// Actions that run the tool as a debug/tracing server - Server, - /// Actions that run debugging commands against a target connected to a probe - Debug, - /// Generate completions data for the shell - Complete(CompletionArguments), -} +use crate::cli_commands::paths; +use crate::{CliArguments, ConfirmedBoolParser}; #[derive(Args)] -struct ProbeArguments +pub struct ProbeArguments { #[arg(global = true, long = "allow-dangerous-options", hide = true, default_value_t = AllowDangerous::Never)] #[arg(value_enum)] @@ -80,12 +26,49 @@ struct ProbeArguments subcommand: ProbeCommmands, } -#[derive(Subcommand)] -#[command(arg_required_else_help(true))] -enum TargetCommmands +impl ProbeArguments { - /// Print information about the target power control state - Power, + pub fn allow_dangerous_options(&self) -> AllowDangerous + { + self.allow_dangerous_options + } + + pub fn override_firmware_type(&self) -> Option + { + match &self.subcommand { + ProbeCommmands::Update(flash_args) => flash_args.override_firmware_type, + ProbeCommmands::Switch(switch_args) => switch_args.override_firmware_type, + _ => None, + } + } + + pub fn subcommand(&self, cli_args: &CliArguments) -> color_eyre::Result<()> + { + let paths = paths(); + match &self.subcommand { + ProbeCommmands::Info(info_args) => info_command(&cli_args, info_args), + ProbeCommmands::Update(update_args) => { + if let Some(subcommand) = &update_args.subcommand { + match subcommand { + UpdateCommands::List => display_releases(&paths), + } + } else { + update_probe(&cli_args, update_args, &paths) + } + }, + ProbeCommmands::Switch(_) => bmputil::switcher::switch_firmware(cli_args, &paths), + ProbeCommmands::Reboot(reboot_args) => reboot_command(&cli_args, reboot_args), + #[cfg(windows)] + ProbeCommmands::InstallDrivers(driver_args) => { + windows::ensure_access( + cli_args.windows_wdi_install_mode, + true, // explicitly_requested. + driver_args.force, + ); + Ok(()) + }, + } + } } #[derive(Subcommand)] @@ -113,6 +96,35 @@ struct InfoArguments list_targets: bool, } +fn info_command(cli_args: &CliArguments, info_args: &InfoArguments) -> color_eyre::Result<()> +{ + // Try and identify all the probes on the system that are allowed by the invocation + let matcher = BmpMatcher::from_params(cli_args); + let mut results = matcher.find_matching_probes(); + + // If we were invoked to list the targets supported by a specific probe, dispatch to the function for that + if info_args.list_targets { + return list_targets(results.pop_single("list targets").map_err(|kind| kind.error())?); + } + + // Otherwise, turn the result set into a list and go through them displaying them + let devices = results.pop_all()?; + let multiple = devices.len() > 1; + + for (index, dev) in devices.iter().enumerate() { + debug!("Probe identity: {}", dev.firmware_identity()?); + println!("Found: {dev}"); + + // If we have multiple connected probes, then additionally display their index + // and print a trailing newline. + if multiple { + println!(" Index: {index}\n"); + } + } + + Ok(()) +} + #[derive(Args)] struct UpdateArguments { @@ -175,65 +187,82 @@ struct DriversArguments force: bool, } -#[derive(Args)] -struct CompletionArguments -{ - shell: Shell, -} - -impl BmpParams for CliArguments +fn display_releases(paths: &ProjectDirs) -> color_eyre::Result<()> { - fn index(&self) -> Option - { - self.index - } - - fn serial_number(&self) -> Option<&str> - { - self.serial_number.as_deref() - } -} - -impl FlashParams for CliArguments -{ - fn allow_dangerous_options(&self) -> AllowDangerous - { - match &self.subcommand { - ToplevelCommmands::Probe(probe_args) => probe_args.allow_dangerous_options, - _ => AllowDangerous::Never, + // Figure out where the metadata cache is + let cache = paths.cache_dir(); + // Acquire the metadata for display + let metadata = download_metadata(cache)?; + // Loop through all the entries and display them + for (version, release) in metadata.releases { + info!("Details of release {version}:"); + info!("-> Release includes BMDA builds? {}", release.includes_bmda); + info!( + "-> Release done for probes: {}", + release + .firmware + .keys() + .map(|p| p.to_string()) + .collect::>() + .join(", ") + ); + for (probe, firmware) in release.firmware { + info!( + "-> probe {} has {} firmware variants", + probe.to_string(), + firmware.variants.len() + ); + for (variant, download) in firmware.variants { + info!(" -> Firmware variant {}", variant); + info!( + " -> {} will be downloaded as {}", + download.friendly_name, + download.file_name.display() + ); + info!(" -> Variant will be downloaded from {}", download.uri); + } } - } - - fn override_firmware_type(&self) -> Option - { - match &self.subcommand { - ToplevelCommmands::Probe(probe_args) => match &probe_args.subcommand { - ProbeCommmands::Update(flash_args) => flash_args.override_firmware_type, - ProbeCommmands::Switch(switch_args) => switch_args.override_firmware_type, - _ => None, - }, - _ => None, + if let Some(bmda) = release.bmda { + info!("-> Release contains BMDA for {} OSes", bmda.len()); + for (os, bmda_arch) in bmda { + info!( + " -> {} release is for {} architectures", + os.to_string(), + bmda_arch.binaries.len() + ); + for (arch, binary) in bmda_arch.binaries { + info!(" -> BMDA binary for {}", arch.to_string()); + info!(" -> Name of executable in archive: {}", binary.file_name.display()); + info!(" -> Archive will be downloaded from {}", binary.uri); + } + } } } + Ok(()) } -#[derive(Clone)] -struct ConfirmedBoolParser {} - -impl TypedValueParser for ConfirmedBoolParser +fn list_targets(probe: BmpDevice) -> color_eyre::Result<()> { - type Value = bool; - - fn parse_ref(&self, cmd: &Command, _arg: Option<&Arg>, value: &OsStr) -> Result - { - let value = value - .to_str() - .ok_or_else(|| clap::Error::new(clap::error::ErrorKind::InvalidUtf8).with_cmd(cmd))?; - Ok(value == "really") + // Extract the remote protocol interface for the probe + let remote = probe.bmd_serial_interface()?.remote()?; + // Ask it what architectures it supports, and display that + let archs = remote.supported_architectures()?; + if let Some(archs) = archs { + info!("Probe supports the following target architectures: {archs}"); + } else { + info!("Could not determine what target architectures your probe supports - please upgrade your firmware."); } + // Ask it what target families it supports, and display that + let families = remote.supported_families()?; + if let Some(families) = families { + info!("Probe supports the following target families: {families}"); + } else { + info!("Could not determine what target families your probe supports - please upgrade your firmware."); + } + Ok(()) } -fn reboot_command(cli_args: &CliArguments, reboot_args: &RebootArguments) -> Result<()> +fn reboot_command(cli_args: &CliArguments, reboot_args: &RebootArguments) -> color_eyre::Result<()> { let matcher = BmpMatcher::from_params(cli_args); let mut results = matcher.find_matching_probes(); @@ -271,7 +300,7 @@ fn reboot_command(cli_args: &CliArguments, reboot_args: &RebootArguments) -> Res dev.detach_and_destroy().wrap_err("detaching device") } -fn update_probe(cli_args: &CliArguments, flash_args: &UpdateArguments, paths: &ProjectDirs) -> Result<()> +fn update_probe(cli_args: &CliArguments, flash_args: &UpdateArguments, paths: &ProjectDirs) -> color_eyre::Result<()> { use bmputil::switcher::{download_firmware, pick_firmware}; @@ -353,340 +382,3 @@ fn update_probe(cli_args: &CliArguments, flash_args: &UpdateArguments, paths: &P bmputil::flasher::flash_probe(cli_args, probe, file_name) } - -fn display_releases(paths: &ProjectDirs) -> Result<()> -{ - // Figure out where the metadata cache is - let cache = paths.cache_dir(); - // Acquire the metadata for display - let metadata = download_metadata(cache)?; - // Loop through all the entries and display them - for (version, release) in metadata.releases { - info!("Details of release {version}:"); - info!("-> Release includes BMDA builds? {}", release.includes_bmda); - info!( - "-> Release done for probes: {}", - release - .firmware - .keys() - .map(|p| p.to_string()) - .collect::>() - .join(", ") - ); - for (probe, firmware) in release.firmware { - info!( - "-> probe {} has {} firmware variants", - probe.to_string(), - firmware.variants.len() - ); - for (variant, download) in firmware.variants { - info!(" -> Firmware variant {}", variant); - info!( - " -> {} will be downloaded as {}", - download.friendly_name, - download.file_name.display() - ); - info!(" -> Variant will be downloaded from {}", download.uri); - } - } - if let Some(bmda) = release.bmda { - info!("-> Release contains BMDA for {} OSes", bmda.len()); - for (os, bmda_arch) in bmda { - info!( - " -> {} release is for {} architectures", - os.to_string(), - bmda_arch.binaries.len() - ); - for (arch, binary) in bmda_arch.binaries { - info!(" -> BMDA binary for {}", arch.to_string()); - info!(" -> Name of executable in archive: {}", binary.file_name.display()); - info!(" -> Archive will be downloaded from {}", binary.uri); - } - } - } - } - Ok(()) -} - -fn list_targets(probe: BmpDevice) -> Result<()> -{ - // Extract the remote protocol interface for the probe - let remote = probe.bmd_serial_interface()?.remote()?; - // Ask it what architectures it supports, and display that - let archs = remote.supported_architectures()?; - if let Some(archs) = archs { - info!("Probe supports the following target architectures: {archs}"); - } else { - info!("Could not determine what target architectures your probe supports - please upgrade your firmware."); - } - // Ask it what target families it supports, and display that - let families = remote.supported_families()?; - if let Some(families) = families { - info!("Probe supports the following target families: {families}"); - } else { - info!("Could not determine what target families your probe supports - please upgrade your firmware."); - } - Ok(()) -} - -fn power_command(cli_args: &CliArguments) -> Result<()> -{ - // Try and identify all the probes on the system that are allowed by the invocation - let matcher = BmpMatcher::from_params(cli_args); - let mut results = matcher.find_matching_probes(); - - // Otherwise, turn the result set into a list and go through them displaying them - let device = results.pop_single("power").map_err(|kind| kind.error())?; - let remote = device.bmd_serial_interface()?.remote()?; - - let power = remote.get_target_power_state()?; - - info!("Device target power state: {}", power); - - Ok(()) -} - -fn info_command(cli_args: &CliArguments, info_args: &InfoArguments) -> Result<()> -{ - // Try and identify all the probes on the system that are allowed by the invocation - let matcher = BmpMatcher::from_params(cli_args); - let mut results = matcher.find_matching_probes(); - - // If we were invoked to list the targets supported by a specific probe, dispatch to the function for that - if info_args.list_targets { - return list_targets(results.pop_single("list targets").map_err(|kind| kind.error())?); - } - - // Otherwise, turn the result set into a list and go through them displaying them - let devices = results.pop_all()?; - let multiple = devices.len() > 1; - - for (index, dev) in devices.iter().enumerate() { - debug!("Probe identity: {}", dev.firmware_identity()?); - println!("Found: {dev}"); - - // If we have multiple connected probes, then additionally display their index - // and print a trailing newline. - if multiple { - println!(" Index: {index}\n"); - } - } - - Ok(()) -} - -type EyreHookFunc = Box Box + Send + Sync + 'static>; -type PanicHookFunc = Box) + Send + Sync + 'static>; - -struct BmputilHook -{ - inner_hook: EyreHookFunc, -} - -struct BmputilPanic -{ - inner_hook: PanicHookFunc, -} - -struct BmputilHandler -{ - inner_handler: Box, -} - -impl BmputilHook -{ - fn build_handler(&self, error: &(dyn std::error::Error + 'static)) -> BmputilHandler - { - BmputilHandler { - inner_handler: (*self.inner_hook)(error), - } - } - - pub fn install(self) -> Result<(), InstallError> - { - color_eyre::eyre::set_hook(self.into_eyre_hook()) - } - - pub fn into_eyre_hook(self) -> EyreHookFunc - { - Box::new(move |err| Box::new(self.build_handler(err))) - } -} - -impl BmputilPanic -{ - pub fn install(self) - { - std::panic::set_hook(self.into_panic_hook()); - } - - pub fn into_panic_hook(self) -> PanicHookFunc - { - Box::new(move |panic_info| { - self.print_header(); - (*self.inner_hook)(panic_info); - self.print_footer(); - }) - } - - fn print_header(&self) - { - eprintln!("------------[ ✂ cut here ✂ ]------------"); - eprintln!("Unhandled crash in bmputil-cli v{}", crate_version!()); - eprintln!(); - } - - fn print_footer(&self) - { - eprintln!(); - eprintln!("{}", "Please include all lines down to this one from the cut here".yellow()); - eprintln!("{}", "marker, and report this issue to our issue tracker at".yellow()); - eprintln!("https://github.com/blackmagic-debug/bmputil/issues"); - } -} - -impl EyreHandler for BmputilHandler -{ - fn debug(&self, error: &(dyn std::error::Error + 'static), fmt: &mut core::fmt::Formatter<'_>) - -> core::fmt::Result - { - writeln!(fmt, "------------[ ✂ cut here ✂ ]------------")?; - write!(fmt, "Unhandled crash in bmputil-cli v{}", crate_version!())?; - self.inner_handler.debug(error, fmt)?; - writeln!(fmt)?; - writeln!(fmt)?; - writeln!( - fmt, - "{}", - "Please include all lines down to this one from the cut here".yellow() - )?; - writeln!(fmt, "{}", " marker, and report this issue to our issue tracker at".yellow())?; - write!(fmt, "https://github.com/blackmagic-debug/bmputil/issues") - } - - fn track_caller(&mut self, location: &'static std::panic::Location<'static>) - { - self.inner_handler.track_caller(location); - } -} - -fn install_error_handler() -> Result<()> -{ - // Grab us a new default handler - let default_handler = HookBuilder::default(); - // Turn that into a pair of hooks - one for panic, and the other for errors - let (panic_hook, eyre_hook) = default_handler.try_into_hooks()?; - - // Make an instance of our custom handler, paassing it the panic one to do normal panic - // handling with, so we only have to deal with our additions, and install it - BmputilPanic { - inner_hook: panic_hook.into_panic_hook(), - } - .install(); - - // Make an instance of our custom handler, passing it the default one to do the main - // error handling with, so we only have to deal with our additions, and install it - BmputilHook { - inner_hook: eyre_hook.into_eyre_hook(), - } - .install()?; - Ok(()) -} - -/// Clap v3 style (approximate) -/// See https://stackoverflow.com/a/75343828 -fn style() -> clap::builder::Styles -{ - Styles::styled() - .usage( - anstyle::Style::new() - .fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Yellow))) - .bold(), - ) - .header( - anstyle::Style::new() - .bold() - .fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Yellow))), - ) - .literal(anstyle::Style::new().fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Green)))) -} - -fn main() -> Result<()> -{ - install_error_handler()?; - env_logger::Builder::new() - .filter_level(log::LevelFilter::Info) - .parse_default_env() - .init(); - - let cli_args = CliArguments::parse(); - - // If the user hasn't requested us to go installing drivers explicitly, make sure that we - // actually have sufficient permissions here to do what is needed - #[cfg(windows)] - match cli_args.subcommand { - ToplevelCommmands::Probe(ProbeArguments { - subcommand: ProbeCommmands::InstallDrivers(_), - .. - }) => (), - // Potentially install drivers, but still do whatever else the user wanted. - _ => { - windows::ensure_access( - cli_args.windows_wdi_install_mode, - false, // explicitly_requested - false, // force - ); - }, - } - - // Try to get the application paths available - let paths = match ProjectDirs::from("org", "black-magic", "bmputil") { - Some(paths) => paths, - None => { - error!("Failed to get program working paths"); - std::process::exit(2); - }, - }; - - match &cli_args.subcommand { - ToplevelCommmands::Probe(probe_args) => match &probe_args.subcommand { - ProbeCommmands::Info(info_args) => info_command(&cli_args, info_args), - ProbeCommmands::Update(update_args) => { - if let Some(subcommand) = &update_args.subcommand { - match subcommand { - UpdateCommands::List => display_releases(&paths), - } - } else { - update_probe(&cli_args, update_args, &paths) - } - }, - ProbeCommmands::Switch(_) => bmputil::switcher::switch_firmware(&cli_args, &paths), - ProbeCommmands::Reboot(reboot_args) => reboot_command(&cli_args, reboot_args), - #[cfg(windows)] - ProbeCommmands::InstallDrivers(driver_args) => { - windows::ensure_access( - cli_args.windows_wdi_install_mode, - true, // explicitly_requested. - driver_args.force, - ); - Ok(()) - }, - }, - ToplevelCommmands::Target(command) => match command { - TargetCommmands::Power => power_command(&cli_args), - }, - ToplevelCommmands::Server => { - warn!("Command space reserved for future tool version"); - Ok(()) - }, - ToplevelCommmands::Debug => { - warn!("Command space reserved for future tool version"); - Ok(()) - }, - ToplevelCommmands::Complete(comp_args) => { - let mut cmd = CliArguments::command(); - generate(comp_args.shell, &mut cmd, "bmputil-cli", &mut stdout()); - Ok(()) - }, - } -} diff --git a/src/bin/bmputil-cli/main.rs b/src/bin/bmputil-cli/main.rs new file mode 100644 index 0000000..87c6b00 --- /dev/null +++ b/src/bin/bmputil-cli/main.rs @@ -0,0 +1,265 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// SPDX-FileCopyrightText: 2022-2025 1BitSquared +// SPDX-FileContributor: Written by Mikaela Szekely +// SPDX-FileContributor: Modified by P-Storm + +mod cli_commands; + +use std::ffi::OsStr; +use std::io::stdout; +use std::str::FromStr; + +use bmputil::bmp::{BmpDevice, BmpMatcher, FirmwareType}; +use bmputil::metadata::download_metadata; +#[cfg(windows)] +use bmputil::windows; +use bmputil::{AllowDangerous, BmpParams, FlashParams}; +use clap::builder::TypedValueParser; +use clap::builder::styling::Styles; +use clap::{Arg, ArgAction, Args, Command, CommandFactory, Parser, Subcommand, crate_description, crate_version}; +use clap_complete::{Shell, generate}; +use color_eyre::config::HookBuilder; +use color_eyre::eyre::{Context, EyreHandler, InstallError, OptionExt, Result}; +use directories::ProjectDirs; +use log::{debug, error, info, warn}; +use owo_colors::OwoColorize; + +use crate::cli_commands::ToplevelCommmands; +use crate::cli_commands::probe::ProbeArguments; + +#[derive(Parser)] +#[command( + version, + about = format!("{} v{}", crate_description!(), crate_version!()), + styles(style()), + disable_colored_help(false), + arg_required_else_help(true) +)] +struct CliArguments +{ + #[arg(global = true, short = 's', long = "serial", alias = "serial-number")] + /// Use the device with the given serial number + serial_number: Option, + #[arg(global = true, long = "index", value_parser = usize::from_str)] + /// Use the nth found device (may be unstable!) + index: Option, + #[arg(global = true, short = 'p', long = "port")] + /// Use the device on the given USB port + port: Option, + + #[cfg(windows)] + #[arg(global = true, long = "windows-wdi-install-mode", value_parser = u32::from_str, hide = true)] + /// Internal argument used when re-executing this command to acquire admin for installing drivers + windows_wdi_install_mode: Option, + + #[command(subcommand)] + pub subcommand: ToplevelCommmands, +} + +#[derive(Args)] +struct CompletionArguments +{ + shell: Shell, +} + +#[derive(Clone)] +struct ConfirmedBoolParser {} + +impl TypedValueParser for ConfirmedBoolParser +{ + type Value = bool; + + fn parse_ref(&self, cmd: &Command, _arg: Option<&Arg>, value: &OsStr) -> Result + { + let value = value + .to_str() + .ok_or_else(|| clap::Error::new(clap::error::ErrorKind::InvalidUtf8).with_cmd(cmd))?; + Ok(value == "really") + } +} + +type EyreHookFunc = Box Box + Send + Sync + 'static>; +type PanicHookFunc = Box) + Send + Sync + 'static>; + +struct BmputilHook +{ + inner_hook: EyreHookFunc, +} + +struct BmputilPanic +{ + inner_hook: PanicHookFunc, +} + +struct BmputilHandler +{ + inner_handler: Box, +} + +impl BmputilHook +{ + fn build_handler(&self, error: &(dyn std::error::Error + 'static)) -> BmputilHandler + { + BmputilHandler { + inner_handler: (*self.inner_hook)(error), + } + } + + pub fn install(self) -> Result<(), InstallError> + { + color_eyre::eyre::set_hook(self.into_eyre_hook()) + } + + pub fn into_eyre_hook(self) -> EyreHookFunc + { + Box::new(move |err| Box::new(self.build_handler(err))) + } +} + +impl BmputilPanic +{ + pub fn install(self) + { + std::panic::set_hook(self.into_panic_hook()); + } + + pub fn into_panic_hook(self) -> PanicHookFunc + { + Box::new(move |panic_info| { + self.print_header(); + (*self.inner_hook)(panic_info); + self.print_footer(); + }) + } + + fn print_header(&self) + { + eprintln!("------------[ ✂ cut here ✂ ]------------"); + eprintln!("Unhandled crash in bmputil-cli v{}", crate_version!()); + eprintln!(); + } + + fn print_footer(&self) + { + eprintln!(); + eprintln!("{}", "Please include all lines down to this one from the cut here".yellow()); + eprintln!("{}", "marker, and report this issue to our issue tracker at".yellow()); + eprintln!("https://github.com/blackmagic-debug/bmputil/issues"); + } +} + +impl EyreHandler for BmputilHandler +{ + fn debug(&self, error: &(dyn std::error::Error + 'static), fmt: &mut core::fmt::Formatter<'_>) + -> core::fmt::Result + { + writeln!(fmt, "------------[ ✂ cut here ✂ ]------------")?; + write!(fmt, "Unhandled crash in bmputil-cli v{}", crate_version!())?; + self.inner_handler.debug(error, fmt)?; + writeln!(fmt)?; + writeln!(fmt)?; + writeln!( + fmt, + "{}", + "Please include all lines down to this one from the cut here".yellow() + )?; + writeln!(fmt, "{}", " marker, and report this issue to our issue tracker at".yellow())?; + write!(fmt, "https://github.com/blackmagic-debug/bmputil/issues") + } + + fn track_caller(&mut self, location: &'static std::panic::Location<'static>) + { + self.inner_handler.track_caller(location); + } +} + +fn install_error_handler() -> Result<()> +{ + // Grab us a new default handler + let default_handler = HookBuilder::default(); + // Turn that into a pair of hooks - one for panic, and the other for errors + let (panic_hook, eyre_hook) = default_handler.try_into_hooks()?; + + // Make an instance of our custom handler, paassing it the panic one to do normal panic + // handling with, so we only have to deal with our additions, and install it + BmputilPanic { + inner_hook: panic_hook.into_panic_hook(), + } + .install(); + + // Make an instance of our custom handler, passing it the default one to do the main + // error handling with, so we only have to deal with our additions, and install it + BmputilHook { + inner_hook: eyre_hook.into_eyre_hook(), + } + .install()?; + Ok(()) +} + +/// Clap v3 style (approximate) +/// See https://stackoverflow.com/a/75343828 +fn style() -> clap::builder::Styles +{ + Styles::styled() + .usage( + anstyle::Style::new() + .fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Yellow))) + .bold(), + ) + .header( + anstyle::Style::new() + .bold() + .fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Yellow))), + ) + .literal(anstyle::Style::new().fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Green)))) +} + +fn main() -> Result<()> +{ + install_error_handler()?; + env_logger::Builder::new() + .filter_level(log::LevelFilter::Info) + .parse_default_env() + .init(); + + let cli_args = CliArguments::parse(); + + // If the user hasn't requested us to go installing drivers explicitly, make sure that we + // actually have sufficient permissions here to do what is needed + #[cfg(windows)] + match cli_args.subcommand { + ToplevelCommmands::Probe(ProbeArguments { + subcommand: ProbeCommmands::InstallDrivers(_), + .. + }) => (), + // Potentially install drivers, but still do whatever else the user wanted. + _ => { + windows::ensure_access( + cli_args.windows_wdi_install_mode, + false, // explicitly_requested + false, // force + ); + }, + } + + match &cli_args.subcommand { + ToplevelCommmands::Probe(probe_args) => probe_args.subcommand(&cli_args), + ToplevelCommmands::Target => { + warn!("Command space reserved for future tool version"); + Ok(()) + }, + ToplevelCommmands::Server => { + warn!("Command space reserved for future tool version"); + Ok(()) + }, + ToplevelCommmands::Debug => { + warn!("Command space reserved for future tool version"); + Ok(()) + }, + ToplevelCommmands::Complete(comp_args) => { + let mut cmd = CliArguments::command(); + generate(comp_args.shell, &mut cmd, "bmputil-cli", &mut stdout()); + Ok(()) + }, + } +} From 6ca94a65e8fe1ef52278c6c853d935ba768b536a Mon Sep 17 00:00:00 2001 From: P-Storm Date: Sun, 27 Jul 2025 16:34:12 +0200 Subject: [PATCH 2/2] target: moved target command to own file --- src/bin/bmputil-cli/cli_commands/mod.rs | 5 ++- src/bin/bmputil-cli/cli_commands/target.rs | 41 ++++++++++++++++++++++ src/bin/bmputil-cli/main.rs | 5 +-- 3 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 src/bin/bmputil-cli/cli_commands/target.rs diff --git a/src/bin/bmputil-cli/cli_commands/mod.rs b/src/bin/bmputil-cli/cli_commands/mod.rs index e994c1a..e9d1cb1 100644 --- a/src/bin/bmputil-cli/cli_commands/mod.rs +++ b/src/bin/bmputil-cli/cli_commands/mod.rs @@ -10,9 +10,11 @@ use directories::ProjectDirs; use log::error; use crate::cli_commands::probe::ProbeArguments; +use crate::cli_commands::target::TargetCommmands; use crate::{CliArguments, CompletionArguments}; pub mod probe; +mod target; #[derive(Subcommand)] pub enum ToplevelCommmands @@ -20,7 +22,8 @@ pub enum ToplevelCommmands /// Actions to be performed against a probe Probe(ProbeArguments), /// Actions to be performed against a target connected to a probe - Target, + #[command(subcommand)] + Target(TargetCommmands), /// Actions that run the tool as a debug/tracing server Server, /// Actions that run debugging commands against a target connected to a probe diff --git a/src/bin/bmputil-cli/cli_commands/target.rs b/src/bin/bmputil-cli/cli_commands/target.rs new file mode 100644 index 0000000..966b538 --- /dev/null +++ b/src/bin/bmputil-cli/cli_commands/target.rs @@ -0,0 +1,41 @@ +use bmputil::bmp::BmpMatcher; +use clap::Subcommand; +use color_eyre::eyre::Result; +use log::info; + +use crate::CliArguments; + +#[derive(Subcommand)] +#[command(arg_required_else_help(true))] +pub enum TargetCommmands +{ + /// Print information about the target power control state + Power, +} + +impl TargetCommmands +{ + pub fn subcommand(&self, cli_args: &CliArguments) -> Result<()> + { + match self { + TargetCommmands::Power => power_command(&cli_args), + } + } +} + +fn power_command(cli_args: &CliArguments) -> Result<()> +{ + // Try and identify all the probes on the system that are allowed by the invocation + let matcher = BmpMatcher::from_params(cli_args); + let mut results = matcher.find_matching_probes(); + + // Otherwise, turn the result set into a list and go through them displaying them + let device = results.pop_single("power").map_err(|kind| kind.error())?; + let remote = device.bmd_serial_interface()?.remote()?; + + let power = remote.get_target_power_state()?; + + info!("Device target power state: {}", power); + + Ok(()) +} diff --git a/src/bin/bmputil-cli/main.rs b/src/bin/bmputil-cli/main.rs index 87c6b00..3d0712a 100644 --- a/src/bin/bmputil-cli/main.rs +++ b/src/bin/bmputil-cli/main.rs @@ -244,10 +244,7 @@ fn main() -> Result<()> match &cli_args.subcommand { ToplevelCommmands::Probe(probe_args) => probe_args.subcommand(&cli_args), - ToplevelCommmands::Target => { - warn!("Command space reserved for future tool version"); - Ok(()) - }, + ToplevelCommmands::Target(target_commands) => target_commands.subcommand(&cli_args), ToplevelCommmands::Server => { warn!("Command space reserved for future tool version"); Ok(())