breaking: make ADBDeviceExt dyn-compatible (#70)

* feat: make ADBDeviceExt dyn-compatible

* feat: clean CLI code
This commit is contained in:
cli
2024-12-06 17:47:38 +01:00
committed by GitHub
parent 66d124475d
commit 5dfd30cc5b
29 changed files with 454 additions and 585 deletions

View File

@@ -1,12 +0,0 @@
use clap::Parser;
#[derive(Parser, Debug)]
pub enum EmuCommand {
/// Send a SMS with given phone number and given content
Sms {
phone_number: String,
content: String,
},
/// Rotate device screen from 90°
Rotate,
}

View File

@@ -1,11 +0,0 @@
mod emu;
mod host;
mod local;
mod tcp;
mod usb;
pub use emu::EmuCommand;
pub use host::{HostCommand, MdnsCommand};
pub use local::LocalCommand;
pub use tcp::TcpCommand;
pub use usb::{DeviceCommands, UsbCommand};

View File

@@ -1,61 +0,0 @@
use std::num::ParseIntError;
use std::path::PathBuf;
use clap::Parser;
use crate::models::RebootTypeCommand;
fn parse_hex_id(id: &str) -> Result<u16, ParseIntError> {
u16::from_str_radix(id, 16)
}
#[derive(Parser, Debug)]
pub struct UsbCommand {
/// Hexadecimal vendor id of this USB device
#[clap(short = 'v', long = "vendor-id", value_parser=parse_hex_id, value_name="VID")]
pub vendor_id: Option<u16>,
/// Hexadecimal product id of this USB device
#[clap(short = 'p', long = "product-id", value_parser=parse_hex_id, value_name="PID")]
pub product_id: Option<u16>,
/// Path to a custom private key to use for authentication
#[clap(short = 'k', long = "private-key")]
pub path_to_private_key: Option<PathBuf>,
#[clap(subcommand)]
pub commands: DeviceCommands,
}
#[derive(Parser, Debug)]
pub enum DeviceCommands {
/// Spawn an interactive shell or run a list of commands on the device
Shell { commands: Vec<String> },
/// Pull a file from device
Pull { source: String, destination: String },
/// Push a file on device
Push { filename: String, path: String },
/// Stat a file on device
Stat { path: String },
/// Run an activity on device specified by the intent
Run {
/// The package whose activity is to be invoked
#[clap(short = 'p', long = "package")]
package: String,
/// The activity to be invoked itself, Usually it is MainActivity
#[clap(short = 'a', long = "activity")]
activity: String,
},
/// Reboot the device
Reboot {
#[clap(subcommand)]
reboot_type: RebootTypeCommand,
},
/// Install an APK on device
Install {
/// Path to APK file. Extension must be ".apk"
path: PathBuf,
},
/// Dump framebuffer of device
Framebuffer {
/// Framebuffer image destination path
path: String,
},
}

View File

@@ -0,0 +1,20 @@
use adb_client::ADBEmulatorDevice;
use crate::models::{EmuCommand, EmulatorCommand};
pub fn handle_emulator_commands(emulator_command: EmulatorCommand) -> anyhow::Result<()> {
let mut emulator = ADBEmulatorDevice::new(emulator_command.serial, None)?;
match emulator_command.command {
EmuCommand::Sms {
phone_number,
content,
} => {
emulator.send_sms(&phone_number, &content)?;
log::info!("SMS sent to {phone_number}");
}
EmuCommand::Rotate => emulator.rotate()?,
}
Ok(())
}

View File

@@ -0,0 +1,78 @@
use adb_client::{ADBServer, DeviceShort, MDNSBackend, Result};
use crate::models::{HostCommand, MdnsCommand, ServerCommand};
pub fn handle_host_commands(server_command: ServerCommand<HostCommand>) -> Result<()> {
let mut adb_server = ADBServer::new(server_command.address);
match server_command.command {
HostCommand::Version => {
let version = adb_server.version()?;
log::info!("Android Debug Bridge version {}", version);
log::info!("Package version {}-rust", std::env!("CARGO_PKG_VERSION"));
}
HostCommand::Kill => {
adb_server.kill()?;
}
HostCommand::Devices { long } => {
if long {
log::info!("List of devices attached (extended)");
for device in adb_server.devices_long()? {
log::info!("{}", device);
}
} else {
log::info!("List of devices attached");
for device in adb_server.devices()? {
log::info!("{}", device);
}
}
}
HostCommand::TrackDevices => {
let callback = |device: DeviceShort| {
log::info!("{}", device);
Ok(())
};
log::info!("Live list of devices attached");
adb_server.track_devices(callback)?;
}
HostCommand::Pair { address, code } => {
adb_server.pair(address, code)?;
log::info!("Paired device {address}");
}
HostCommand::Connect { address } => {
adb_server.connect_device(address)?;
log::info!("Connected to {address}");
}
HostCommand::Disconnect { address } => {
adb_server.disconnect_device(address)?;
log::info!("Disconnected {address}");
}
HostCommand::Mdns { subcommand } => match subcommand {
MdnsCommand::Check => {
let check = adb_server.mdns_check()?;
let server_status = adb_server.server_status()?;
match server_status.mdns_backend {
MDNSBackend::Unknown => log::info!("unknown mdns backend..."),
MDNSBackend::Bonjour => match check {
true => log::info!("mdns daemon version [Bonjour]"),
false => log::info!("ERROR: mdns daemon unavailable"),
},
MDNSBackend::OpenScreen => {
log::info!("mdns daemon version [Openscreen discovery 0.0.0]")
}
}
}
MdnsCommand::Services => {
log::info!("List of discovered mdns services");
for service in adb_server.mdns_services()? {
log::info!("{}", service);
}
}
},
HostCommand::ServerStatus => {
log::info!("{}", adb_server.server_status()?);
}
}
Ok(())
}

View File

@@ -0,0 +1,35 @@
use std::{fs::File, io::Write};
use adb_client::ADBServerDevice;
use anyhow::{anyhow, Result};
use crate::models::LocalDeviceCommand;
pub fn handle_local_commands(
mut device: ADBServerDevice,
local_device_commands: LocalDeviceCommand,
) -> Result<()> {
match local_device_commands {
LocalDeviceCommand::HostFeatures => {
let features = device
.host_features()?
.iter()
.map(|v| v.to_string())
.reduce(|a, b| format!("{a},{b}"))
.ok_or(anyhow!("cannot list features"))?;
log::info!("Available host features: {features}");
Ok(())
}
LocalDeviceCommand::List { path } => Ok(device.list(path)?),
LocalDeviceCommand::Logcat { path } => {
let writer: Box<dyn Write> = if let Some(path) = path {
let f = File::create(path)?;
Box::new(f)
} else {
Box::new(std::io::stdout())
};
Ok(device.get_logs(writer)?)
}
}
}

View File

@@ -0,0 +1,7 @@
mod emulator_commands;
mod host_commands;
mod local_commands;
pub use emulator_commands::handle_emulator_commands;
pub use host_commands::handle_host_commands;
pub use local_commands::handle_local_commands;

View File

@@ -3,336 +3,68 @@
#[cfg(any(target_os = "linux", target_os = "macos"))]
mod adb_termios;
mod commands;
mod handlers;
mod models;
mod utils;
use adb_client::{
ADBDeviceExt, ADBEmulatorDevice, ADBServer, ADBTcpDevice, ADBUSBDevice, DeviceShort,
MDNSBackend, MDNSDiscoveryService,
};
use anyhow::{anyhow, Result};
use adb_client::{ADBDeviceExt, ADBServer, ADBTcpDevice, ADBUSBDevice, MDNSDiscoveryService};
use adb_termios::ADBTermios;
use anyhow::Result;
use clap::Parser;
use commands::{DeviceCommands, EmuCommand, HostCommand, LocalCommand, MdnsCommand};
use models::{Command, Opts};
use handlers::{handle_emulator_commands, handle_host_commands, handle_local_commands};
use models::{DeviceCommands, LocalCommand, MainCommand, Opts};
use std::fs::File;
use std::io::Write;
use std::path::Path;
use utils::setup_logger;
fn main() -> Result<()> {
let opts = Opts::parse();
// RUST_LOG variable has more priority then "--debug" flag
if std::env::var("RUST_LOG").is_err() {
let level = match opts.debug {
true => "trace",
false => "info",
};
setup_logger(opts.debug);
std::env::set_var("RUST_LOG", level);
}
// 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) => {
let mut adb_server = ADBServer::new(server_command.address);
// Setting default log level as "info" if not set
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "info");
}
env_logger::init();
match opts.command {
Command::Local(local) => {
let mut adb_server = ADBServer::new(opts.address);
let mut device = match opts.serial {
let device = match server_command.serial {
Some(serial) => adb_server.get_device_by_name(&serial)?,
None => adb_server.get_device()?,
};
match local {
LocalCommand::Pull { path, filename } => {
let mut output = File::create(Path::new(&filename))?;
device.pull(&path, &mut output)?;
log::info!("Downloaded {path} as {filename}");
}
LocalCommand::Push { filename, path } => {
let mut input = File::open(Path::new(&filename))?;
device.push(&mut input, &path)?;
log::info!("Uploaded {filename} to {path}");
}
LocalCommand::List { path } => {
device.list(path)?;
}
LocalCommand::Stat { path } => {
let stat_response = device.stat(path)?;
println!("{}", stat_response);
}
LocalCommand::Shell { commands } => {
if commands.is_empty() {
// Need to duplicate some code here as ADBTermios [Drop] implementation resets terminal state.
// Using a scope here would call drop() too early..
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
let mut adb_termios = adb_termios::ADBTermios::new(std::io::stdin())?;
adb_termios.set_adb_termios()?;
device.shell(std::io::stdin(), std::io::stdout())?;
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
device.shell(std::io::stdin(), std::io::stdout())?;
}
} else {
device.shell_command(commands, std::io::stdout())?;
}
}
LocalCommand::Run { package, activity } => {
let output = device.run_activity(&package, &activity)?;
std::io::stdout().write_all(&output)?;
}
LocalCommand::HostFeatures => {
let features = device
.host_features()?
.iter()
.map(|v| v.to_string())
.reduce(|a, b| format!("{a},{b}"))
.ok_or(anyhow!("cannot list features"))?;
log::info!("Available host features: {features}");
}
LocalCommand::Reboot { reboot_type } => {
log::info!("Reboots device in mode {:?}", reboot_type);
device.reboot(reboot_type.into())?
}
LocalCommand::Framebuffer { path } => {
device.framebuffer(&path)?;
log::info!("Framebuffer dropped: {path}");
}
LocalCommand::Logcat { path } => {
let writer: Box<dyn Write> = if let Some(path) = path {
let f = File::create(path)?;
Box::new(f)
} else {
Box::new(std::io::stdout())
};
device.get_logs(writer)?;
}
LocalCommand::Install { path } => {
log::info!("Starting installation of APK {}...", path.display());
device.install(path)?;
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)
}
}
}
Command::Host(host) => {
let mut adb_server = ADBServer::new(opts.address);
match host {
HostCommand::Version => {
let version = adb_server.version()?;
log::info!("Android Debug Bridge version {}", version);
log::info!("Package version {}-rust", std::env!("CARGO_PKG_VERSION"));
}
HostCommand::Kill => {
adb_server.kill()?;
}
HostCommand::Devices { long } => {
if long {
log::info!("List of devices attached (extended)");
for device in adb_server.devices_long()? {
log::info!("{}", device);
}
} else {
log::info!("List of devices attached");
for device in adb_server.devices()? {
log::info!("{}", device);
}
}
}
HostCommand::TrackDevices => {
let callback = |device: DeviceShort| {
log::info!("{}", device);
Ok(())
};
log::info!("Live list of devices attached");
adb_server.track_devices(callback)?;
}
HostCommand::Pair { address, code } => {
adb_server.pair(address, code)?;
log::info!("Paired device {address}");
}
HostCommand::Connect { address } => {
adb_server.connect_device(address)?;
log::info!("Connected to {address}");
}
HostCommand::Disconnect { address } => {
adb_server.disconnect_device(address)?;
log::info!("Disconnected {address}");
}
HostCommand::Mdns { subcommand } => match subcommand {
MdnsCommand::Check => {
let check = adb_server.mdns_check()?;
let server_status = adb_server.server_status()?;
match server_status.mdns_backend {
MDNSBackend::Unknown => log::info!("unknown mdns backend..."),
MDNSBackend::Bonjour => match check {
true => log::info!("mdns daemon version [Bonjour]"),
false => log::info!("ERROR: mdns daemon unavailable"),
},
MDNSBackend::OpenScreen => {
log::info!("mdns daemon version [Openscreen discovery 0.0.0]")
}
}
}
MdnsCommand::Services => {
log::info!("List of discovered mdns services");
for service in adb_server.mdns_services()? {
log::info!("{}", service);
}
}
},
HostCommand::ServerStatus => {
log::info!("{}", adb_server.server_status()?);
}
}
}
Command::Emu(emu) => {
let mut emulator = match opts.serial {
Some(serial) => ADBEmulatorDevice::new(serial, None)?,
None => return Err(anyhow!("Serial must be set to use emulators !")),
};
match emu {
EmuCommand::Sms {
phone_number,
content,
} => {
emulator.send_sms(&phone_number, &content)?;
log::info!("SMS sent to {phone_number}");
}
EmuCommand::Rotate => emulator.rotate()?,
}
}
Command::Usb(usb) => {
let mut device = match (usb.vendor_id, usb.product_id) {
(Some(vid), Some(pid)) => match usb.path_to_private_key {
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.path_to_private_key {
(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.");
}
};
match usb.commands {
DeviceCommands::Shell { commands } => {
if commands.is_empty() {
// Need to duplicate some code here as ADBTermios [Drop] implementation resets terminal state.
// Using a scope here would call drop() too early..
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
let mut adb_termios = adb_termios::ADBTermios::new(std::io::stdin())?;
adb_termios.set_adb_termios()?;
device.shell(std::io::stdin(), std::io::stdout())?;
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
device.shell(std::io::stdin(), std::io::stdout())?;
}
} else {
device.shell_command(commands, std::io::stdout())?;
}
}
DeviceCommands::Pull {
source,
destination,
} => {
let mut output = File::create(Path::new(&destination))?;
device.pull(&source, &mut output)?;
log::info!("Downloaded {source} as {destination}");
}
DeviceCommands::Stat { path } => {
let stat_response = device.stat(&path)?;
println!("{}", stat_response);
}
DeviceCommands::Reboot { reboot_type } => {
log::info!("Reboots device in mode {:?}", reboot_type);
device.reboot(reboot_type.into())?
}
DeviceCommands::Push { filename, path } => {
let mut input = File::open(Path::new(&filename))?;
device.push(&mut input, &path)?;
log::info!("Uploaded {filename} to {path}");
}
DeviceCommands::Run { package, activity } => {
let output = device.run_activity(&package, &activity)?;
std::io::stdout().write_all(&output)?;
}
DeviceCommands::Install { path } => {
log::info!("Starting installation of APK {}...", path.display());
device.install(path)?;
}
DeviceCommands::Framebuffer { path } => device.framebuffer(path)?,
}
(device.boxed(), usb_command.commands)
}
Command::Tcp(tcp) => {
let mut device = ADBTcpDevice::new(tcp.address)?;
match tcp.commands {
DeviceCommands::Shell { commands } => {
if commands.is_empty() {
// Need to duplicate some code here as ADBTermios [Drop] implementation resets terminal state.
// Using a scope here would call drop() too early..
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
let mut adb_termios = adb_termios::ADBTermios::new(std::io::stdin())?;
adb_termios.set_adb_termios()?;
device.shell(std::io::stdin(), std::io::stdout())?;
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
device.shell(std::io::stdin(), std::io::stdout())?;
}
} else {
device.shell_command(commands, std::io::stdout())?;
}
}
DeviceCommands::Pull {
source,
destination,
} => {
let mut output = File::create(Path::new(&destination))?;
device.pull(&source, &mut output)?;
log::info!("Downloaded {source} as {destination}");
}
DeviceCommands::Stat { path } => {
let stat_response = device.stat(&path)?;
println!("{}", stat_response);
}
DeviceCommands::Reboot { reboot_type } => {
log::info!("Reboots device in mode {:?}", reboot_type);
device.reboot(reboot_type.into())?
}
DeviceCommands::Push { filename, path } => {
let mut input = File::open(Path::new(&filename))?;
device.push(&mut input, &path)?;
log::info!("Uploaded {filename} to {path}");
}
DeviceCommands::Run { package, activity } => {
let output = device.run_activity(&package, &activity)?;
std::io::stdout().write_all(&output)?;
}
DeviceCommands::Install { path } => {
log::info!("Starting installation of APK {}...", path.display());
device.install(path)?;
}
DeviceCommands::Framebuffer { path } => device.framebuffer(path)?,
}
MainCommand::Tcp(tcp_command) => {
let device = ADBTcpDevice::new(tcp_command.address)?;
(device.boxed(), tcp_command.commands)
}
Command::MdnsDiscovery => {
MainCommand::Mdns => {
let mut service = MDNSDiscoveryService::new()?;
let (tx, rx) = std::sync::mpsc::channel();
@@ -347,8 +79,61 @@ fn main() -> Result<()> {
)
}
service.shutdown()?;
return Ok(service.shutdown()?);
}
};
match commands {
DeviceCommands::Shell { commands } => {
if commands.is_empty() {
// Need to duplicate some code here as ADBTermios [Drop] implementation resets terminal state.
// Using a scope here would call drop() too early..
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
let mut adb_termios = ADBTermios::new(std::io::stdin())?;
adb_termios.set_adb_termios()?;
device.shell(&mut std::io::stdin(), Box::new(std::io::stdout()))?;
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
device.shell(std::io::stdin(), std::io::stdout())?;
}
} else {
let commands: Vec<&str> = commands.iter().map(|v| v.as_str()).collect();
device.shell_command(&commands, &mut std::io::stdout())?;
}
}
DeviceCommands::Pull {
source,
destination,
} => {
let mut output = File::create(Path::new(&destination))?;
device.pull(&source, &mut output)?;
log::info!("Downloaded {source} as {destination}");
}
DeviceCommands::Stat { path } => {
let stat_response = device.stat(&path)?;
println!("{}", stat_response);
}
DeviceCommands::Reboot { reboot_type } => {
log::info!("Reboots device in mode {:?}", reboot_type);
device.reboot(reboot_type.into())?
}
DeviceCommands::Push { filename, path } => {
let mut input = File::open(Path::new(&filename))?;
device.push(&mut input, &path)?;
log::info!("Uploaded {filename} to {path}");
}
DeviceCommands::Run { package, activity } => {
let output = device.run_activity(&package, &activity)?;
std::io::stdout().write_all(&output)?;
}
DeviceCommands::Install { path } => {
log::info!("Starting installation of APK {}...", path.display());
device.install(&path)?;
}
DeviceCommands::Framebuffer { path } => device.framebuffer(&path)?,
}
Ok(())

View File

@@ -1,22 +1,19 @@
use clap::Parser;
use std::path::PathBuf;
use crate::models::RebootTypeCommand;
use clap::Parser;
use super::RebootTypeCommand;
#[derive(Parser, Debug)]
pub enum LocalCommand {
/// List available server features.
HostFeatures,
/// Push a file on device
Push { filename: String, path: String },
/// Pull a file from device
Pull { path: String, filename: String },
/// List a directory on device
List { path: String },
/// Stat a file specified on device
Stat { path: String },
pub enum DeviceCommands {
/// Spawn an interactive shell or run a list of commands on the device
Shell { commands: Vec<String> },
/// Pull a file from device
Pull { source: String, destination: String },
/// Push a file on device
Push { filename: String, path: String },
/// Stat a file on device
Stat { path: String },
/// Run an activity on device specified by the intent
Run {
/// The package whose activity is to be invoked
@@ -31,16 +28,14 @@ pub enum LocalCommand {
#[clap(subcommand)]
reboot_type: RebootTypeCommand,
},
/// Dump framebuffer of device
Framebuffer { path: String },
/// Get logs of device
Logcat {
/// Path to output file (created if not exists)
path: Option<String>,
},
/// Install an APK on device
Install {
/// Path to APK file. Extension must be ".apk"
path: PathBuf,
},
/// Dump framebuffer of device
Framebuffer {
/// Framebuffer image destination path
path: String,
},
}

20
adb_cli/src/models/emu.rs Normal file
View File

@@ -0,0 +1,20 @@
use clap::{Parser, Subcommand};
#[derive(Debug, Parser)]
pub struct EmulatorCommand {
#[clap(short = 's', long = "serial")]
pub serial: String,
#[clap(subcommand)]
pub command: EmuCommand,
}
#[derive(Debug, Subcommand)]
pub enum EmuCommand {
/// Send a SMS with given phone number and given content
Sms {
phone_number: String,
content: String,
},
/// Rotate device screen from 90°
Rotate,
}

View File

@@ -0,0 +1,24 @@
use clap::Parser;
use super::DeviceCommands;
#[derive(Parser, Debug)]
pub enum LocalCommand {
#[clap(flatten)]
DeviceCommands(DeviceCommands),
#[clap(flatten)]
LocalDeviceCommand(LocalDeviceCommand),
}
#[derive(Parser, Debug)]
pub enum LocalDeviceCommand {
/// List available server features.
HostFeatures,
/// List a directory on device
List { path: String },
/// Get logs of device
Logcat {
/// Path to output file (created if not exists)
path: Option<String>,
},
}

View File

@@ -1,5 +1,17 @@
mod device;
mod emu;
mod host;
mod local;
mod opts;
mod reboot_type;
mod tcp;
mod usb;
pub use opts::{Command, Opts};
pub use device::DeviceCommands;
pub use emu::{EmuCommand, EmulatorCommand};
pub use host::{HostCommand, MdnsCommand};
pub use local::{LocalCommand, LocalDeviceCommand};
pub use opts::{MainCommand, Opts, ServerCommand};
pub use reboot_type::RebootTypeCommand;
pub use tcp::TcpCommand;
pub use usb::UsbCommand;

View File

@@ -1,36 +1,41 @@
use std::net::SocketAddrV4;
use clap::Parser;
use clap::{Parser, Subcommand};
use crate::commands::{EmuCommand, HostCommand, LocalCommand, TcpCommand, UsbCommand};
use super::{EmulatorCommand, HostCommand, LocalCommand, TcpCommand, UsbCommand};
#[derive(Parser, Debug)]
#[derive(Debug, Parser)]
#[clap(about, version, author)]
pub struct Opts {
#[clap(long = "debug")]
pub debug: bool,
#[clap(subcommand)]
pub command: MainCommand,
}
#[derive(Debug, Parser)]
pub enum MainCommand {
/// Server related commands
Host(ServerCommand<HostCommand>),
/// Device related commands using server
Local(ServerCommand<LocalCommand>),
/// Emulator related commands
Emu(EmulatorCommand),
/// USB device related commands
Usb(UsbCommand),
/// TCP device related commands
Tcp(TcpCommand),
/// MDNS discovery related commands
Mdns,
}
#[derive(Debug, Parser)]
pub struct ServerCommand<T: Subcommand> {
#[clap(short = 'a', long = "address", default_value = "127.0.0.1:5037")]
pub address: SocketAddrV4,
/// Serial id of a specific device. Every request will be sent to this device.
#[clap(short = 's', long = "serial")]
pub serial: Option<String>,
#[clap(subcommand)]
pub command: Command,
}
#[derive(Parser, Debug)]
pub enum Command {
#[clap(flatten)]
Local(LocalCommand),
#[clap(flatten)]
Host(HostCommand),
/// Emulator specific commands
#[clap(subcommand)]
Emu(EmuCommand),
/// Device commands via USB, no server needed
Usb(UsbCommand),
/// Device commands via TCP, no server needed
Tcp(TcpCommand),
/// Discover devices over MDNS without using adb-server
MdnsDiscovery,
pub command: T,
}

25
adb_cli/src/models/usb.rs Normal file
View File

@@ -0,0 +1,25 @@
use std::num::ParseIntError;
use std::path::PathBuf;
use clap::Parser;
use super::DeviceCommands;
fn parse_hex_id(id: &str) -> Result<u16, ParseIntError> {
u16::from_str_radix(id, 16)
}
#[derive(Parser, Debug)]
pub struct UsbCommand {
/// Hexadecimal vendor id of this USB device
#[clap(short = 'v', long = "vendor-id", value_parser=parse_hex_id, value_name="VID")]
pub vendor_id: Option<u16>,
/// Hexadecimal product id of this USB device
#[clap(short = 'p', long = "product-id", value_parser=parse_hex_id, value_name="PID")]
pub product_id: Option<u16>,
/// Path to a custom private key to use for authentication
#[clap(short = 'k', long = "private-key")]
pub path_to_private_key: Option<PathBuf>,
#[clap(subcommand)]
pub commands: DeviceCommands,
}

13
adb_cli/src/utils.rs Normal file
View File

@@ -0,0 +1,13 @@
pub fn setup_logger(debug: bool) {
// RUST_LOG variable has more priority then "--debug" flag
if std::env::var("RUST_LOG").is_err() {
let level = match debug {
true => "trace",
false => "info",
};
std::env::set_var("RUST_LOG", level);
}
env_logger::init();
}

View File

@@ -54,7 +54,7 @@ use adb_client::{ADBServer, ADBDeviceExt};
let mut server = ADBServer::default();
let mut device = server.get_device().expect("cannot get device");
device.shell_command(["df", "-h"],std::io::stdout());
device.shell_command(&["df", "-h"], &mut std::io::stdout());
```
#### Push a file to the device
@@ -81,7 +81,7 @@ use adb_client::{ADBUSBDevice, ADBDeviceExt};
let vendor_id = 0x04e8;
let product_id = 0x6860;
let mut device = ADBUSBDevice::new(vendor_id, product_id).expect("cannot find device");
device.shell_command(["df", "-h"],std::io::stdout());
device.shell_command(&["df", "-h"], &mut std::io::stdout());
```
#### (USB) Push a file to the device
@@ -95,7 +95,7 @@ let vendor_id = 0x04e8;
let product_id = 0x6860;
let mut device = ADBUSBDevice::new(vendor_id, product_id).expect("cannot find device");
let mut input = File::open(Path::new("/tmp/f")).expect("Cannot open file");
device.push(&mut input, "/data/local/tmp");
device.push(&mut input, &"/data/local/tmp");
```
#### (TCP) Get a shell from device
@@ -107,5 +107,5 @@ use adb_client::{ADBTcpDevice, ADBDeviceExt};
let device_ip = IpAddr::V4(Ipv4Addr::new(192, 168, 0, 10));
let device_port = 43210;
let mut device = ADBTcpDevice::new(SocketAddr::new(device_ip, device_port)).expect("cannot find device");
device.shell(std::io::stdin(), std::io::stdout());
device.shell(&mut std::io::stdin(), Box::new(std::io::stdout()));
```

View File

@@ -1,31 +1,28 @@
use std::io::{Read, Seek, Write};
use std::io::{Cursor, Read, Write};
use std::path::Path;
use image::{ImageBuffer, ImageFormat, Rgba};
use crate::models::AdbStatResponse;
use crate::{RebootType, Result};
/// Trait representing all features available on both [`crate::ADBServerDevice`] and [`crate::ADBUSBDevice`]
pub trait ADBDeviceExt {
/// Runs command in a shell on the device, and write its output and error streams into output.
fn shell_command<S: ToString, W: Write>(
&mut self,
command: impl IntoIterator<Item = S>,
output: W,
) -> Result<()>;
fn shell_command(&mut self, command: &[&str], output: &mut dyn Write) -> Result<()>;
/// Starts an interactive shell session on the device.
/// Input data is read from reader and write to writer.
/// W has a 'static bound as it is internally used in a thread.
fn shell<R: Read, W: Write + Send + 'static>(&mut self, reader: R, writer: W) -> Result<()>;
fn shell(&mut self, reader: &mut dyn Read, writer: Box<(dyn Write + Send)>) -> Result<()>;
/// Display the stat information for a remote file
fn stat(&mut self, remote_path: &str) -> Result<AdbStatResponse>;
/// Pull the remote file pointed to by `source` and write its contents into `output`
fn pull<A: AsRef<str>, W: Write>(&mut self, source: A, output: W) -> Result<()>;
fn pull(&mut self, source: &dyn AsRef<str>, output: &mut dyn Write) -> Result<()>;
/// Push `stream` to `path` on the device.
fn push<R: Read, A: AsRef<str>>(&mut self, stream: R, path: A) -> Result<()>;
fn push(&mut self, stream: &mut dyn Read, path: &dyn AsRef<str>) -> Result<()>;
/// Reboot the device using given reboot type
fn reboot(&mut self, reboot_type: RebootType) -> Result<()>;
@@ -34,7 +31,7 @@ pub trait ADBDeviceExt {
fn run_activity(&mut self, package: &str, activity: &str) -> Result<Vec<u8>> {
let mut output = Vec::new();
self.shell_command(
["am", "start", &format!("{package}/{package}.{activity}")],
&["am", "start", &format!("{package}/{package}.{activity}")],
&mut output,
)?;
@@ -42,13 +39,35 @@ pub trait ADBDeviceExt {
}
/// Install an APK pointed to by `apk_path` on device.
fn install<P: AsRef<Path>>(&mut self, apk_path: P) -> Result<()>;
fn install(&mut self, apk_path: &dyn AsRef<Path>) -> Result<()>;
/// Inner method requesting framebuffer from an Android device
fn framebuffer_inner(&mut self) -> Result<ImageBuffer<Rgba<u8>, Vec<u8>>>;
/// Dump framebuffer of this device into given path
fn framebuffer<P: AsRef<Path>>(&mut self, path: P) -> Result<()>;
fn framebuffer(&mut self, path: &dyn AsRef<Path>) -> Result<()> {
// Big help from AOSP source code (<https://android.googlesource.com/platform/system/adb/+/refs/heads/main/framebuffer_service.cpp>)
let img = self.framebuffer_inner()?;
Ok(img.save(path.as_ref())?)
}
/// Dump framebuffer of this device and return corresponding bytes.
///
/// Output data format is currently only `PNG`.
fn framebuffer_bytes<W: Write + Seek>(&mut self, writer: W) -> Result<()>;
fn framebuffer_bytes(&mut self) -> Result<Vec<u8>> {
let img = self.framebuffer_inner()?;
let mut vec = Cursor::new(Vec::new());
img.write_to(&mut vec, ImageFormat::Png)?;
Ok(vec.into_inner())
}
/// Return a boxed instance representing this trait
fn boxed(self) -> Box<dyn ADBDeviceExt>
where
Self: Sized,
Self: 'static,
{
Box::new(self)
}
}

View File

@@ -1,18 +1,17 @@
use crate::{models::AdbStatResponse, ADBDeviceExt, ADBMessageTransport, RebootType, Result};
use std::io::{Read, Write};
use std::{
io::{Read, Write},
path::Path,
};
use super::ADBMessageDevice;
impl<T: ADBMessageTransport> ADBDeviceExt for ADBMessageDevice<T> {
fn shell_command<S: ToString, W: Write>(
&mut self,
command: impl IntoIterator<Item = S>,
output: W,
) -> Result<()> {
fn shell_command(&mut self, command: &[&str], output: &mut dyn Write) -> Result<()> {
self.shell_command(command, output)
}
fn shell<R: Read, W: Write + Send + 'static>(&mut self, reader: R, writer: W) -> Result<()> {
fn shell(&mut self, reader: &mut dyn Read, writer: Box<(dyn Write + Send)>) -> Result<()> {
self.shell(reader, writer)
}
@@ -20,11 +19,11 @@ impl<T: ADBMessageTransport> ADBDeviceExt for ADBMessageDevice<T> {
self.stat(remote_path)
}
fn pull<A: AsRef<str>, W: Write>(&mut self, source: A, output: W) -> Result<()> {
fn pull(&mut self, source: &dyn AsRef<str>, output: &mut dyn Write) -> Result<()> {
self.pull(source, output)
}
fn push<R: Read, A: AsRef<str>>(&mut self, stream: R, path: A) -> Result<()> {
fn push(&mut self, stream: &mut dyn Read, path: &dyn AsRef<str>) -> Result<()> {
self.push(stream, path)
}
@@ -32,15 +31,11 @@ impl<T: ADBMessageTransport> ADBDeviceExt for ADBMessageDevice<T> {
self.reboot(reboot_type)
}
fn install<P: AsRef<std::path::Path>>(&mut self, apk_path: P) -> Result<()> {
fn install(&mut self, apk_path: &dyn AsRef<Path>) -> Result<()> {
self.install(apk_path)
}
fn framebuffer<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<()> {
self.framebuffer(path)
}
fn framebuffer_bytes<W: Write + std::io::Seek>(&mut self, writer: W) -> Result<()> {
self.framebuffer_bytes(writer)
fn framebuffer_inner(&mut self) -> Result<image::ImageBuffer<image::Rgba<u8>, Vec<u8>>> {
self.framebuffer_inner()
}
}

View File

@@ -1,5 +1,6 @@
use std::net::SocketAddr;
use std::io::Write;
use std::path::Path;
use std::{io::Read, net::SocketAddr};
use super::adb_message_device::ADBMessageDevice;
use super::models::MessageCommand;
@@ -67,20 +68,12 @@ impl ADBTcpDevice {
impl ADBDeviceExt for ADBTcpDevice {
#[inline]
fn shell_command<S: ToString, W: std::io::Write>(
&mut self,
command: impl IntoIterator<Item = S>,
output: W,
) -> Result<()> {
fn shell_command(&mut self, command: &[&str], output: &mut dyn Write) -> Result<()> {
self.inner.shell_command(command, output)
}
#[inline]
fn shell<R: std::io::Read, W: std::io::Write + Send + 'static>(
&mut self,
reader: R,
writer: W,
) -> Result<()> {
fn shell(&mut self, reader: &mut dyn Read, writer: Box<(dyn Write + Send)>) -> Result<()> {
self.inner.shell(reader, writer)
}
@@ -90,12 +83,12 @@ impl ADBDeviceExt for ADBTcpDevice {
}
#[inline]
fn pull<A: AsRef<str>, W: std::io::Write>(&mut self, source: A, output: W) -> Result<()> {
fn pull(&mut self, source: &dyn AsRef<str>, output: &mut dyn Write) -> Result<()> {
self.inner.pull(source, output)
}
#[inline]
fn push<R: std::io::Read, A: AsRef<str>>(&mut self, stream: R, path: A) -> Result<()> {
fn push(&mut self, stream: &mut dyn Read, path: &dyn AsRef<str>) -> Result<()> {
self.inner.push(stream, path)
}
@@ -105,18 +98,13 @@ impl ADBDeviceExt for ADBTcpDevice {
}
#[inline]
fn install<P: AsRef<Path>>(&mut self, apk_path: P) -> Result<()> {
fn install(&mut self, apk_path: &dyn AsRef<Path>) -> Result<()> {
self.inner.install(apk_path)
}
#[inline]
fn framebuffer<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
self.inner.framebuffer(path)
}
#[inline]
fn framebuffer_bytes<W: std::io::Write + std::io::Seek>(&mut self, writer: W) -> Result<()> {
self.inner.framebuffer_bytes(writer)
fn framebuffer_inner(&mut self) -> Result<image::ImageBuffer<image::Rgba<u8>, Vec<u8>>> {
self.inner.framebuffer_inner()
}
}

View File

@@ -2,6 +2,8 @@ use rusb::Device;
use rusb::DeviceDescriptor;
use rusb::UsbContext;
use std::fs::read_to_string;
use std::io::Read;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use std::time::Duration;
@@ -249,20 +251,12 @@ impl ADBUSBDevice {
impl ADBDeviceExt for ADBUSBDevice {
#[inline]
fn shell_command<S: ToString, W: std::io::Write>(
&mut self,
command: impl IntoIterator<Item = S>,
output: W,
) -> Result<()> {
fn shell_command(&mut self, command: &[&str], output: &mut dyn Write) -> Result<()> {
self.inner.shell_command(command, output)
}
#[inline]
fn shell<R: std::io::Read, W: std::io::Write + Send + 'static>(
&mut self,
reader: R,
writer: W,
) -> Result<()> {
fn shell<'a>(&mut self, reader: &mut dyn Read, writer: Box<(dyn Write + Send)>) -> Result<()> {
self.inner.shell(reader, writer)
}
@@ -272,12 +266,12 @@ impl ADBDeviceExt for ADBUSBDevice {
}
#[inline]
fn pull<A: AsRef<str>, W: std::io::Write>(&mut self, source: A, output: W) -> Result<()> {
fn pull(&mut self, source: &dyn AsRef<str>, output: &mut dyn Write) -> Result<()> {
self.inner.pull(source, output)
}
#[inline]
fn push<R: std::io::Read, A: AsRef<str>>(&mut self, stream: R, path: A) -> Result<()> {
fn push(&mut self, stream: &mut dyn Read, path: &dyn AsRef<str>) -> Result<()> {
self.inner.push(stream, path)
}
@@ -287,18 +281,13 @@ impl ADBDeviceExt for ADBUSBDevice {
}
#[inline]
fn install<P: AsRef<Path>>(&mut self, apk_path: P) -> Result<()> {
fn install(&mut self, apk_path: &dyn AsRef<Path>) -> Result<()> {
self.inner.install(apk_path)
}
#[inline]
fn framebuffer<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
self.inner.framebuffer(path)
}
#[inline]
fn framebuffer_bytes<W: std::io::Write + std::io::Seek>(&mut self, writer: W) -> Result<()> {
self.inner.framebuffer_bytes(writer)
fn framebuffer_inner(&mut self) -> Result<image::ImageBuffer<image::Rgba<u8>, Vec<u8>>> {
self.inner.framebuffer_inner()
}
}

View File

@@ -1,7 +1,7 @@
use std::io::{Cursor, Read, Write};
use std::io::{Cursor, Read};
use byteorder::{LittleEndian, ReadBytesExt};
use image::{ImageBuffer, ImageFormat, Rgba};
use image::{ImageBuffer, Rgba};
use crate::{
device::{adb_message_device::ADBMessageDevice, MessageCommand},
@@ -10,17 +10,7 @@ use crate::{
};
impl<T: ADBMessageTransport> ADBMessageDevice<T> {
pub fn framebuffer<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<()> {
let img = self.framebuffer_inner()?;
Ok(img.save(path.as_ref())?)
}
pub fn framebuffer_bytes<W: Write + std::io::Seek>(&mut self, mut writer: W) -> Result<()> {
let img = self.framebuffer_inner()?;
Ok(img.write_to(&mut writer, ImageFormat::Png)?)
}
fn framebuffer_inner(&mut self) -> Result<ImageBuffer<Rgba<u8>, Vec<u8>>> {
pub(crate) fn framebuffer_inner(&mut self) -> Result<ImageBuffer<Rgba<u8>, Vec<u8>>> {
self.open_session(b"framebuffer:\0")?;
let response = self.recv_and_reply_okay()?;

View File

@@ -1,4 +1,4 @@
use std::fs::File;
use std::{fs::File, path::Path};
use rand::Rng;
@@ -9,10 +9,10 @@ use crate::{
};
impl<T: ADBMessageTransport> ADBMessageDevice<T> {
pub(crate) fn install<P: AsRef<std::path::Path>>(&mut self, apk_path: P) -> Result<()> {
let mut apk_file = File::open(&apk_path)?;
pub(crate) fn install(&mut self, apk_path: &dyn AsRef<Path>) -> Result<()> {
let mut apk_file = File::open(apk_path)?;
check_extension_is_apk(&apk_path)?;
check_extension_is_apk(apk_path)?;
let file_size = apk_file.metadata()?.len();

View File

@@ -9,22 +9,8 @@ use crate::{
impl<T: ADBMessageTransport> ADBMessageDevice<T> {
/// Runs 'command' in a shell on the device, and write its output and error streams into output.
pub(crate) fn shell_command<S: ToString, W: Write>(
&mut self,
command: impl IntoIterator<Item = S>,
mut output: W,
) -> Result<()> {
let response = self.open_session(
format!(
"shell:{}\0",
command
.into_iter()
.map(|v| v.to_string())
.collect::<Vec<_>>()
.join(" "),
)
.as_bytes(),
)?;
pub(crate) fn shell_command(&mut self, command: &[&str], output: &mut dyn Write) -> Result<()> {
let response = self.open_session(format!("shell:{}\0", command.join(" "),).as_bytes())?;
if response.header().command() != MessageCommand::Okay {
return Err(RustADBError::ADBRequestFailed(format!(
@@ -47,11 +33,10 @@ impl<T: ADBMessageTransport> ADBMessageDevice<T> {
/// Starts an interactive shell session on the device.
/// Input data is read from [reader] and write to [writer].
/// [W] has a 'static bound as it is internally used in a thread.
pub(crate) fn shell<R: Read, W: Write + Send + 'static>(
pub(crate) fn shell(
&mut self,
mut reader: R,
mut writer: W,
mut reader: &mut dyn Read,
mut writer: Box<(dyn Write + Send)>,
) -> Result<()> {
self.open_session(b"shell:\0")?;

View File

@@ -12,11 +12,7 @@ use crate::{
use super::ADBServerDevice;
impl ADBDeviceExt for ADBServerDevice {
fn shell_command<S: ToString, W: Write>(
&mut self,
command: impl IntoIterator<Item = S>,
mut output: W,
) -> Result<()> {
fn shell_command(&mut self, command: &[&str], output: &mut dyn Write) -> Result<()> {
let supported_features = self.host_features()?;
if !supported_features.contains(&HostFeatures::ShellV2)
&& !supported_features.contains(&HostFeatures::Cmd)
@@ -28,13 +24,7 @@ impl ADBDeviceExt for ADBServerDevice {
self.connect()?
.send_adb_request(AdbServerCommand::TransportSerial(serial))?;
self.get_transport_mut()
.send_adb_request(AdbServerCommand::ShellCommand(
command
.into_iter()
.map(|v| v.to_string())
.collect::<Vec<_>>()
.join(" "),
))?;
.send_adb_request(AdbServerCommand::ShellCommand(command.join(" ")))?;
const BUFFER_SIZE: usize = 4096;
loop {
@@ -62,10 +52,10 @@ impl ADBDeviceExt for ADBServerDevice {
self.stat(remote_path)
}
fn shell<R: Read, W: Write + Send + 'static>(
fn shell(
&mut self,
mut reader: R,
mut writer: W,
mut reader: &mut dyn Read,
mut writer: Box<(dyn Write + Send)>,
) -> Result<()> {
let supported_features = self.host_features()?;
if !supported_features.contains(&HostFeatures::ShellV2)
@@ -115,7 +105,7 @@ impl ADBDeviceExt for ADBServerDevice {
Ok(())
}
fn pull<A: AsRef<str>, W: Write>(&mut self, source: A, mut output: W) -> Result<()> {
fn pull(&mut self, source: &dyn AsRef<str>, mut output: &mut dyn Write) -> Result<()> {
self.pull(source, &mut output)
}
@@ -123,19 +113,15 @@ impl ADBDeviceExt for ADBServerDevice {
self.reboot(reboot_type)
}
fn push<R: Read, A: AsRef<str>>(&mut self, stream: R, path: A) -> Result<()> {
fn push(&mut self, stream: &mut dyn Read, path: &dyn AsRef<str>) -> Result<()> {
self.push(stream, path)
}
fn install<P: AsRef<Path>>(&mut self, apk_path: P) -> Result<()> {
fn install(&mut self, apk_path: &dyn AsRef<Path>) -> Result<()> {
self.install(apk_path)
}
fn framebuffer<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
self.framebuffer(path)
}
fn framebuffer_bytes<W: Write + std::io::Seek>(&mut self, writer: W) -> Result<()> {
self.framebuffer_bytes(writer)
fn framebuffer_inner(&mut self) -> Result<image::ImageBuffer<image::Rgba<u8>, Vec<u8>>> {
self.framebuffer_inner()
}
}

View File

@@ -1,10 +1,7 @@
use std::{
io::{Read, Seek, Write},
path::Path,
};
use std::io::Read;
use byteorder::{LittleEndian, ReadBytesExt};
use image::{ImageBuffer, ImageFormat, Rgba};
use image::{ImageBuffer, Rgba};
use crate::{
models::{AdbServerCommand, FrameBufferInfoV1, FrameBufferInfoV2},
@@ -12,23 +9,8 @@ use crate::{
};
impl ADBServerDevice {
/// Dump framebuffer of this device into given ['path']
/// Big help from source code (<https://android.googlesource.com/platform/system/adb/+/refs/heads/main/framebuffer_service.cpp>)
pub(crate) fn framebuffer<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
let img = self.framebuffer_inner()?;
Ok(img.save(path.as_ref())?)
}
/// Dump framebuffer of this device and return corresponding bytes.
///
/// Output data format is currently only `PNG`.
pub(crate) fn framebuffer_bytes<W: Write + Seek>(&mut self, mut writer: W) -> Result<()> {
let img = self.framebuffer_inner()?;
Ok(img.write_to(&mut writer, ImageFormat::Png)?)
}
/// Inner method requesting framebuffer from Android device
fn framebuffer_inner(&mut self) -> Result<ImageBuffer<Rgba<u8>, Vec<u8>>> {
pub(crate) fn framebuffer_inner(&mut self) -> Result<ImageBuffer<Rgba<u8>, Vec<u8>>> {
let serial: String = self.identifier.clone();
self.connect()?
.send_adb_request(AdbServerCommand::TransportSerial(serial))?;

View File

@@ -51,6 +51,6 @@ impl<W: Write> Write for LogFilter<W> {
impl ADBServerDevice {
/// Get logs from device
pub fn get_logs<W: Write>(&mut self, output: W) -> Result<()> {
self.shell_command(["exec logcat"], LogFilter::new(output))
self.shell_command(&["exec logcat"], &mut LogFilter::new(output))
}
}

View File

@@ -70,7 +70,7 @@ impl<R: Read> Read for ADBRecvCommandReader<R> {
impl ADBServerDevice {
/// Receives path to stream from the device.
pub fn pull<A: AsRef<str>>(&mut self, path: A, stream: &mut dyn Write) -> Result<()> {
pub fn pull(&mut self, path: &dyn AsRef<str>, stream: &mut dyn Write) -> Result<()> {
let serial = self.identifier.clone();
self.connect()?
.send_adb_request(AdbServerCommand::TransportSerial(serial))?;