feat: improve error management in adb_cli

- adds an explicit error message when error needs to be filled with an
  issue
- remove anyhow dependency in adb_cli
- update deps
This commit is contained in:
Corentin LIAUD
2025-12-12 19:28:50 +01:00
parent c9d5df55d4
commit 1564e99aa7
16 changed files with 208 additions and 122 deletions

View File

@@ -11,11 +11,10 @@ rust-version.workspace = true
version.workspace = true
[dependencies]
adb_client = { version = "^2.1.17" }
anyhow = { version = "1.0.100" }
clap = { version = "4.5.51", features = ["derive"] }
adb_client = { version = "^2.1.18" }
clap = { version = "4.5.53", features = ["derive"] }
env_logger = { version = "0.11.8" }
log = { version = "0.4.28" }
log = { version = "0.4.29" }
[target.'cfg(unix)'.dependencies]
termios = { version = "0.3.3" }

View File

@@ -1,4 +1,4 @@
# adb_cli
# `adb_cli`
[![MIT licensed](https://img.shields.io/crates/l/adb_cli.svg)](./LICENSE-MIT)
![Crates.io Total Downloads](https://img.shields.io/crates/d/adb_cli)

View File

@@ -4,7 +4,7 @@ use std::os::unix::prelude::{AsRawFd, RawFd};
use termios::{TCSANOW, Termios, VMIN, VTIME, tcsetattr};
use crate::Result;
use crate::models::{ADBCliError, ADBCliResult};
pub struct ADBTermios {
fd: RawFd,
@@ -13,7 +13,7 @@ pub struct ADBTermios {
}
impl ADBTermios {
pub fn new(fd: &impl AsRawFd) -> Result<Self> {
pub fn new(fd: &impl AsRawFd) -> Result<Self, ADBCliError> {
let mut new_termios = Termios::from_fd(fd.as_raw_fd())?;
let old_termios = new_termios; // Saves previous state
new_termios.c_lflag = 0;
@@ -27,7 +27,7 @@ impl ADBTermios {
})
}
pub fn set_adb_termios(&mut self) -> Result<()> {
pub fn set_adb_termios(&mut self) -> ADBCliResult<()> {
Ok(tcsetattr(self.fd, TCSANOW, &self.new_termios)?)
}
}

View File

@@ -1,8 +1,8 @@
use adb_client::ADBEmulatorDevice;
use crate::models::{EmuCommand, EmulatorCommand};
use crate::models::{ADBCliResult, EmuCommand, EmulatorCommand};
pub fn handle_emulator_commands(emulator_command: EmulatorCommand) -> anyhow::Result<()> {
pub fn handle_emulator_commands(emulator_command: EmulatorCommand) -> ADBCliResult<()> {
let mut emulator = ADBEmulatorDevice::new(emulator_command.serial, None)?;
match emulator_command.command {

View File

@@ -1,14 +1,13 @@
use std::{fs::File, io::Write};
use adb_client::ADBServerDevice;
use anyhow::{Result, anyhow};
use crate::models::LocalDeviceCommand;
use crate::models::{ADBCliResult, LocalDeviceCommand};
pub fn handle_local_commands(
mut device: ADBServerDevice,
local_device_commands: LocalDeviceCommand,
) -> Result<()> {
) -> ADBCliResult<()> {
match local_device_commands {
LocalDeviceCommand::HostFeatures => {
let features = device
@@ -16,7 +15,7 @@ pub fn handle_local_commands(
.iter()
.map(ToString::to_string)
.reduce(|a, b| format!("{a},{b}"))
.ok_or(anyhow!("cannot list features"))?;
.unwrap_or_default();
log::info!("Available host features: {features}");
Ok(())

View File

@@ -14,7 +14,6 @@ use adb_client::{
#[cfg(any(target_os = "linux", target_os = "macos"))]
use adb_termios::ADBTermios;
use anyhow::Result;
use clap::Parser;
use handlers::{handle_emulator_commands, handle_host_commands, handle_local_commands};
use models::{DeviceCommands, LocalCommand, MainCommand, Opts};
@@ -24,87 +23,10 @@ use std::io::Write;
use std::path::Path;
use utils::setup_logger;
fn main() -> Result<()> {
// This depends on `clap`
let opts = Opts::parse();
use crate::models::{ADBCliError, ADBCliResult};
// SAFETY:
// We are assuming the entire process is single-threaded
// at this point.
// This seems true for the current version of `clap`,
// but there's no guarantee for future updates
unsafe { setup_logger(opts.debug) };
// Directly handling methods / commands that aren't linked to [`ADBDeviceExt`] trait.
// Other methods just have to create a concrete [`ADBDeviceExt`] instance, and return it.
// This instance will then be used to execute desired command.
let (mut device, commands) = match opts.command {
MainCommand::Host(server_command) => return Ok(handle_host_commands(server_command)?),
MainCommand::Emu(emulator_command) => return handle_emulator_commands(emulator_command),
MainCommand::Local(server_command) => {
// Must start server to communicate with device, but only if this is a local one.
let server_address_ip = server_command.address.ip();
if server_address_ip.is_loopback() || server_address_ip.is_unspecified() {
ADBServer::start(&HashMap::default(), &None);
}
let device = match server_command.serial {
Some(serial) => ADBServerDevice::new(serial, Some(server_command.address)),
None => ADBServerDevice::autodetect(Some(server_command.address)),
};
match server_command.command {
LocalCommand::DeviceCommands(device_commands) => (device.boxed(), device_commands),
LocalCommand::LocalDeviceCommand(local_device_command) => {
return handle_local_commands(device, local_device_command);
}
}
}
MainCommand::Usb(usb_command) => {
let device = match (usb_command.vendor_id, usb_command.product_id) {
(Some(vid), Some(pid)) => match usb_command.path_to_private_key {
Some(pk) => ADBUSBDevice::new_with_custom_private_key(vid, pid, pk)?,
None => ADBUSBDevice::new(vid, pid)?,
},
(None, None) => match usb_command.path_to_private_key {
Some(pk) => ADBUSBDevice::autodetect_with_custom_private_key(pk)?,
None => ADBUSBDevice::autodetect()?,
},
_ => {
anyhow::bail!(
"please either supply values for both the --vendor-id and --product-id flags or none."
);
}
};
(device.boxed(), usb_command.commands)
}
MainCommand::Tcp(tcp_command) => {
let device = match tcp_command.path_to_private_key {
Some(pk) => ADBTcpDevice::new_with_custom_private_key(tcp_command.address, pk)?,
None => ADBTcpDevice::new(tcp_command.address)?,
};
(device.boxed(), tcp_command.commands)
}
MainCommand::Mdns => {
let mut service = MDNSDiscoveryService::new()?;
let (tx, rx) = std::sync::mpsc::channel();
service.start(tx)?;
log::info!("Starting mdns discovery...");
while let Ok(device) = rx.recv() {
log::info!(
"Found device {} with addresses {:?}",
device.fullname,
device.addresses
);
}
return Ok(service.shutdown()?);
}
};
match commands {
fn run_command(mut device: Box<dyn ADBDeviceExt>, command: DeviceCommands) -> ADBCliResult<()> {
match command {
DeviceCommands::Shell { commands } => {
if commands.is_empty() {
// Need to duplicate some code here as ADBTermios [Drop] implementation resets terminal state.
@@ -166,3 +88,84 @@ fn main() -> Result<()> {
Ok(())
}
fn main() -> ADBCliResult<()> {
// This depends on `clap`
let opts = Opts::parse();
setup_logger(opts.debug);
// Directly handling methods / commands that aren't linked to [`ADBDeviceExt`] trait.
// Other methods just have to create a concrete [`ADBDeviceExt`] instance, and return it.
// This instance will then be used to execute desired command.
let (device, commands) = match opts.command {
MainCommand::Host(server_command) => return Ok(handle_host_commands(server_command)?),
MainCommand::Emu(emulator_command) => return handle_emulator_commands(emulator_command),
MainCommand::Local(server_command) => {
// Must start server to communicate with device, but only if this is a local one.
let server_address_ip = server_command.address.ip();
if server_address_ip.is_loopback() || server_address_ip.is_unspecified() {
ADBServer::start(&HashMap::default(), &None);
}
let device = match server_command.serial {
Some(serial) => ADBServerDevice::new(serial, Some(server_command.address)),
None => ADBServerDevice::autodetect(Some(server_command.address)),
};
match server_command.command {
LocalCommand::DeviceCommands(device_commands) => (device.boxed(), device_commands),
LocalCommand::LocalDeviceCommand(local_device_command) => {
return handle_local_commands(device, local_device_command);
}
}
}
MainCommand::Usb(usb_command) => {
let device = match (usb_command.vendor_id, usb_command.product_id) {
(Some(vid), Some(pid)) => match usb_command.path_to_private_key {
Some(pk) => ADBUSBDevice::new_with_custom_private_key(vid, pid, pk)?,
None => ADBUSBDevice::new(vid, pid)?,
},
(None, None) => match usb_command.path_to_private_key {
Some(pk) => ADBUSBDevice::autodetect_with_custom_private_key(pk)?,
None => ADBUSBDevice::autodetect()?,
},
_ => {
return Err(ADBCliError::Standard(
"cannot specify flags --vendor-id without --product-id or vice versa"
.into(),
));
}
};
(device.boxed(), usb_command.commands)
}
MainCommand::Tcp(tcp_command) => {
let device = match tcp_command.path_to_private_key {
Some(pk) => ADBTcpDevice::new_with_custom_private_key(tcp_command.address, pk)?,
None => ADBTcpDevice::new(tcp_command.address)?,
};
(device.boxed(), tcp_command.commands)
}
MainCommand::Mdns => {
let mut service = MDNSDiscoveryService::new()?;
let (tx, rx) = std::sync::mpsc::channel();
service.start(tx)?;
log::info!("Starting mdns discovery...");
while let Ok(device) = rx.recv() {
log::info!(
"Found device {} with addresses {:?}",
device.fullname,
device.addresses
);
}
return Ok(service.shutdown()?);
}
};
run_command(device, commands)?;
Ok(())
}

View File

@@ -0,0 +1,84 @@
use std::fmt::Debug;
use adb_client::RustADBError;
pub type ADBCliResult<T> = Result<T, ADBCliError>;
pub enum ADBCliError {
Standard(Box<dyn std::error::Error>),
MayNeedAnIssue(Box<dyn std::error::Error>),
}
impl Debug for ADBCliError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ADBCliError::Standard(error) => write!(f, "{error}"),
ADBCliError::MayNeedAnIssue(error) => write!(
f,
r"
This error is abnormal and may need to fill an issue.
Please submit it to this project's repository here: https://github.com/cocool97/adb_client/issues.
Error source:
{error}
",
),
}
}
}
impl From<std::io::Error> for ADBCliError {
fn from(value: std::io::Error) -> Self {
// We do not consider adb_cli related `std::io::error` as critical
Self::Standard(Box::new(value))
}
}
impl From<adb_client::RustADBError> for ADBCliError {
fn from(value: adb_client::RustADBError) -> Self {
let value = Box::new(value);
match value.as_ref() {
// List of [`RustADBError`] that may need an issue as abnormal
RustADBError::RegexParsingError
| RustADBError::WrongResponseReceived(_, _)
| RustADBError::FramebufferImageError(_)
| RustADBError::FramebufferConversionError
| RustADBError::UnimplementedFramebufferImageVersion(_)
| RustADBError::IOError(_)
| RustADBError::ADBRequestFailed(_)
| RustADBError::UnknownDeviceState(_)
| RustADBError::Utf8StrError(_)
| RustADBError::Utf8StringError(_)
| RustADBError::RegexError(_)
| RustADBError::ParseIntError(_)
| RustADBError::ConversionError
| RustADBError::IntegerConversionError(_)
| RustADBError::HomeError
| RustADBError::NoHomeDirectory
| RustADBError::UsbError(_)
| RustADBError::InvalidIntegrity(_, _)
| RustADBError::Base64DecodeError(_)
| RustADBError::Base64EncodeError(_)
| RustADBError::RSAError(_)
| RustADBError::TryFromSliceError(_)
| RustADBError::RsaPkcs8Error(_)
| RustADBError::CertificateGenerationError(_)
| RustADBError::TLSError(_)
| RustADBError::PemCertError(_)
| RustADBError::PoisonError
| RustADBError::UpgradeError(_)
| RustADBError::MDNSError(_)
| RustADBError::SendError(_)
| RustADBError::UnknownTransport(_) => Self::MayNeedAnIssue(value),
// List of [`RustADBError`] that may occur in standard contexts and therefore do not require for issues
RustADBError::ADBDeviceNotPaired
| RustADBError::UnknownResponseType(_)
| RustADBError::DeviceNotFound(_)
| RustADBError::USBNoDescriptorFound
| RustADBError::ADBShellNotSupported
| RustADBError::USBDeviceNotFound(_, _)
| RustADBError::WrongFileExtension(_)
| RustADBError::AddrParseError(_) => Self::Standard(value),
}
}
}

View File

@@ -1,4 +1,4 @@
use std::net::SocketAddrV4;
use std::{net::SocketAddrV4, str::FromStr};
use adb_client::{RustADBError, WaitForDeviceTransport};
use clap::Parser;
@@ -6,7 +6,7 @@ use clap::Parser;
fn parse_wait_for_device_device_transport(
value: &str,
) -> Result<WaitForDeviceTransport, RustADBError> {
WaitForDeviceTransport::try_from(value)
WaitForDeviceTransport::from_str(value)
}
#[derive(Parser, Debug)]

View File

@@ -1,3 +1,4 @@
mod adb_cli_error;
mod device;
mod emu;
mod host;
@@ -7,6 +8,7 @@ mod reboot_type;
mod tcp;
mod usb;
pub use adb_cli_error::{ADBCliError, ADBCliResult};
pub use device::DeviceCommands;
pub use emu::{EmuCommand, EmulatorCommand};
pub use host::{HostCommand, MdnsCommand};

View File

@@ -1,14 +1,9 @@
/// # Safety
///
/// This conditionally mutates the process' environment.
/// See [`std::env::set_var`] for more info.
pub unsafe fn setup_logger(debug: bool) {
// RUST_LOG variable has more priority then "--debug" flag
if std::env::var("RUST_LOG").is_err() {
let level = if debug { "trace" } else { "info" };
use env_logger::{Builder, Env};
unsafe { std::env::set_var("RUST_LOG", level) };
}
env_logger::init();
/// Sets up appropriate logger level:
/// - if `RUST_LOG` environment variable is set, use its value
/// - else, use `debug` CLI option
pub fn setup_logger(debug: bool) {
Builder::from_env(Env::default().default_filter_or(if debug { "debug" } else { "info" }))
.init();
}

View File

@@ -15,21 +15,21 @@ base64 = { version = "0.22.1" }
bincode = { version = "2.0.1", features = ["serde"] }
byteorder = { version = "1.5.0" }
chrono = { version = "0.4.42", default-features = false, features = ["std"] }
image = { version = "0.25.8", default-features = false }
log = { version = "0.4.28" }
mdns-sd = { version = "0.17.0", default-features = false, features = [
image = { version = "0.25.9", default-features = false }
log = { version = "0.4.29" }
mdns-sd = { version = "0.17.1", default-features = false, features = [
"logging",
] }
num-bigint = { version = "0.8.5", package = "num-bigint-dig" }
num-bigint = { version = "0.8.6", package = "num-bigint-dig" }
num-traits = { version = "0.2.19" }
quick-protobuf = { version = "0.8.1" }
rand = { version = "0.9.2" }
rcgen = { version = "0.14.5" }
regex = { version = "1.12.2", features = ["perf", "std", "unicode"] }
rsa = { version = "0.9.8" }
rsa = { version = "0.9.9" }
rusb = { version = "0.9.4", features = ["vendored"] }
rustls = { version = "0.23.35" }
rustls-pki-types = { version = "1.13.0" }
rustls-pki-types = { version = "1.13.1" }
serde = { version = "1.0.228", features = ["derive"] }
serde_repr = { version = "0.1.20" }
sha1 = { version = "0.10.6", features = ["oid"] }

View File

@@ -1,4 +1,4 @@
use std::fmt::Display;
use std::{fmt::Display, str::FromStr};
use crate::RustADBError;
@@ -24,10 +24,10 @@ impl Display for WaitForDeviceTransport {
}
}
impl TryFrom<&str> for WaitForDeviceTransport {
type Error = RustADBError;
impl FromStr for WaitForDeviceTransport {
type Err = RustADBError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"usb" => Ok(Self::Usb),
"local" => Ok(Self::Local),

View File

@@ -47,6 +47,7 @@ impl USBTransport {
/// Instantiate a new [`USBTransport`] from a [`rusb::Device`].
///
/// Devices can be enumerated using [`rusb::devices()`] and then filtered out to get desired device.
#[must_use]
pub fn new_from_device(rusb_device: rusb::Device<GlobalContext>) -> Self {
Self {
device: rusb_device,

View File

@@ -22,6 +22,6 @@ name = "stub_gen"
[dependencies]
adb_client = { path = "../adb_client" }
anyhow = { version = "1.0.100" }
pyo3 = { version = "0.27.1", features = ["abi3-py310", "anyhow"] }
pyo3-stub-gen = { version = "0.17.0" }
pyo3-stub-gen-derive = { version = "0.17.0" }
pyo3 = { version = "0.27.2", features = ["abi3-py310", "anyhow"] }
pyo3-stub-gen = { version = "0.17.2" }
pyo3-stub-gen-derive = { version = "0.17.2" }

View File

@@ -12,6 +12,7 @@ pub struct PyADBServerDevice(pub ADBServerDevice);
#[gen_stub_pymethods]
#[pymethods]
impl PyADBServerDevice {
#[must_use]
#[getter]
/// Device identifier
pub fn identifier(&self) -> Option<String> {

View File

@@ -12,12 +12,14 @@ pub struct PyDeviceShort(DeviceShort);
#[gen_stub_pymethods]
#[pymethods]
impl PyDeviceShort {
#[must_use]
#[getter]
/// Device identifier
pub fn identifier(&self) -> String {
self.0.identifier.clone()
}
#[must_use]
#[getter]
/// Device state
pub fn state(&self) -> String {