Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f08e5e16d | ||
|
|
6a2d652f60 | ||
|
|
b7ae0b1155 | ||
|
|
ec0ae681ac | ||
|
|
61ba07ecf0 | ||
|
|
c835f20263 | ||
|
|
b51965f5af | ||
|
|
66b0e4c71c |
@@ -7,9 +7,9 @@ authors = ["Corentin LIAUD"]
|
||||
edition = "2021"
|
||||
homepage = "https://github.com/cocool97/adb_client"
|
||||
keywords = ["adb", "android", "tcp", "usb"]
|
||||
license-file = "LICENSE"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/cocool97/adb_client"
|
||||
version = "2.0.2"
|
||||
version = "2.0.4"
|
||||
|
||||
# To build locally when working on a new release
|
||||
[patch.crates-io]
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) [year] [fullname]
|
||||
Copyright (c) 2023-2024 Corentin LIAUD
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
<a href="https://deps.rs/repo/github/cocool97/adb_client">
|
||||
<img alt="dependency status" src="https://deps.rs/repo/github/cocool97/adb_client/status.svg"/>
|
||||
</a>
|
||||
<a href="https://opensource.org/licenses/MIT">
|
||||
<img alt="dependency status" src="https://img.shields.io/badge/License-MIT-yellow.svg"/>
|
||||
</a>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ authors.workspace = true
|
||||
description = "Rust ADB (Android Debug Bridge) CLI"
|
||||
edition.workspace = true
|
||||
keywords.workspace = true
|
||||
license-file.workspace = true
|
||||
license.workspace = true
|
||||
name = "adb_cli"
|
||||
readme = "README.md"
|
||||
repository.workspace = true
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use clap::Parser;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::models::RebootTypeCommand;
|
||||
|
||||
@@ -16,6 +17,15 @@ pub enum LocalCommand {
|
||||
Stat { path: String },
|
||||
/// Spawn an interactive shell or run a list of commands on the device
|
||||
Shell { commands: Vec<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)]
|
||||
@@ -28,4 +38,9 @@ pub enum LocalCommand {
|
||||
/// 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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -34,9 +34,23 @@ pub enum UsbCommands {
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -65,6 +65,10 @@ fn main() -> Result<()> {
|
||||
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()?
|
||||
@@ -91,6 +95,10 @@ fn main() -> Result<()> {
|
||||
};
|
||||
device.get_logs(writer)?;
|
||||
}
|
||||
LocalCommand::Install { path } => {
|
||||
log::info!("Starting installation of APK {}...", path.display());
|
||||
device.install(path)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Command::Host(host) => {
|
||||
@@ -215,6 +223,14 @@ fn main() -> Result<()> {
|
||||
device.push(&mut input, &path)?;
|
||||
log::info!("Uploaded {filename} to {path}");
|
||||
}
|
||||
UsbCommands::Run { package, activity } => {
|
||||
let output = device.run_activity(&package, &activity)?;
|
||||
std::io::stdout().write_all(&output)?;
|
||||
}
|
||||
UsbCommands::Install { path } => {
|
||||
log::info!("Starting installation of APK {}...", path.display());
|
||||
device.install(path)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ authors.workspace = true
|
||||
description = "Rust ADB (Android Debug Bridge) client library"
|
||||
edition.workspace = true
|
||||
keywords.workspace = true
|
||||
license-file.workspace = true
|
||||
license.workspace = true
|
||||
name = "adb_client"
|
||||
readme = "README.md"
|
||||
repository.workspace = true
|
||||
@@ -25,4 +25,4 @@ rsa = { version = "0.3.0" }
|
||||
rusb = { version = "0.9.4", features = ["vendored"] }
|
||||
serde = { version = "1.0.210", features = ["derive"] }
|
||||
serde_repr = "0.1.19"
|
||||
thiserror = { version = "1.0.64" }
|
||||
thiserror = { version = "2.0.1" }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::io::{Read, Write};
|
||||
use std::path::Path;
|
||||
|
||||
use crate::models::AdbStatResponse;
|
||||
use crate::{RebootType, Result};
|
||||
@@ -28,4 +29,18 @@ pub trait ADBDeviceExt {
|
||||
|
||||
/// Reboots the device using given reboot type
|
||||
fn reboot(&mut self, reboot_type: RebootType) -> Result<()>;
|
||||
|
||||
/// Run `activity` from `package` on device. Return the command output.
|
||||
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}")],
|
||||
&mut output,
|
||||
)?;
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// Install an APK pointed to by `apk_path` on device.
|
||||
fn install<P: AsRef<Path>>(&mut self, apk_path: P) -> Result<()>;
|
||||
}
|
||||
|
||||
@@ -87,4 +87,7 @@ pub enum RustADBError {
|
||||
/// Cannot convert given data from slice
|
||||
#[error(transparent)]
|
||||
TryFromSliceError(#[from] std::array::TryFromSliceError),
|
||||
/// Given path does not represent an APK
|
||||
#[error("wrong file extension: {0}")]
|
||||
WrongFileExtension(String),
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ mod utils;
|
||||
|
||||
pub use adb_device_ext::ADBDeviceExt;
|
||||
pub use error::{Result, RustADBError};
|
||||
pub use models::{AdbVersion, DeviceLong, DeviceShort, DeviceState, RebootType};
|
||||
pub use models::{AdbStatResponse, AdbVersion, DeviceLong, DeviceShort, DeviceState, RebootType};
|
||||
pub use server::*;
|
||||
pub use transports::*;
|
||||
pub use usb::ADBUSBDevice;
|
||||
|
||||
@@ -16,6 +16,7 @@ pub(crate) enum AdbServerCommand {
|
||||
Pair(SocketAddrV4, String),
|
||||
TransportAny,
|
||||
TransportSerial(String),
|
||||
Install(u64),
|
||||
// Local commands
|
||||
ShellCommand(String),
|
||||
Shell,
|
||||
@@ -61,6 +62,7 @@ impl Display for AdbServerCommand {
|
||||
AdbServerCommand::Reverse(remote, local) => {
|
||||
write!(f, "reverse:forward:{remote};{local}")
|
||||
}
|
||||
AdbServerCommand::Install(size) => write!(f, "exec:cmd package 'install' -S {size}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,14 @@ use std::{
|
||||
use byteorder::LittleEndian;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Represents a `stat` response
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct AdbStatResponse {
|
||||
/// File permissions
|
||||
pub file_perm: u32,
|
||||
/// File size, in bytes
|
||||
pub file_size: u32,
|
||||
/// File modification time
|
||||
pub mod_time: u32,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use std::io::{Read, Write};
|
||||
use std::{
|
||||
io::{Read, Write},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
constants::BUFFER_SIZE,
|
||||
@@ -121,4 +124,8 @@ impl ADBDeviceExt for ADBServerDevice {
|
||||
fn push<R: Read, A: AsRef<str>>(&mut self, stream: R, path: A) -> Result<()> {
|
||||
self.push(stream, path)
|
||||
}
|
||||
|
||||
fn install<P: AsRef<Path>>(&mut self, apk_path: P) -> Result<()> {
|
||||
self.install(apk_path)
|
||||
}
|
||||
}
|
||||
|
||||
41
adb_client/src/server/device_commands/install.rs
Normal file
41
adb_client/src/server/device_commands/install.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use std::{fs::File, io::Read, path::Path};
|
||||
|
||||
use crate::{models::AdbServerCommand, utils::check_extension_is_apk, ADBServerDevice, Result};
|
||||
|
||||
impl ADBServerDevice {
|
||||
/// Install an APK on device
|
||||
pub fn install<P: AsRef<Path>>(&mut self, apk_path: P) -> Result<()> {
|
||||
let mut apk_file = File::open(&apk_path)?;
|
||||
|
||||
check_extension_is_apk(&apk_path)?;
|
||||
|
||||
let file_size = apk_file.metadata()?.len();
|
||||
|
||||
let serial: String = self.identifier.clone();
|
||||
self.connect()?
|
||||
.send_adb_request(AdbServerCommand::TransportSerial(serial))?;
|
||||
|
||||
self.get_transport_mut()
|
||||
.send_adb_request(AdbServerCommand::Install(file_size))?;
|
||||
|
||||
let mut raw_connection = self.get_transport_mut().get_raw_connection()?;
|
||||
|
||||
std::io::copy(&mut apk_file, &mut raw_connection)?;
|
||||
|
||||
let mut data = [0; 1024];
|
||||
let read_amount = self.get_transport().get_raw_connection()?.read(&mut data)?;
|
||||
|
||||
match &data[0..read_amount] {
|
||||
b"Success\n" => {
|
||||
log::info!(
|
||||
"APK file {} successfully installed",
|
||||
apk_path.as_ref().display()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
d => Err(crate::RustADBError::ADBRequestFailed(String::from_utf8(
|
||||
d.to_vec(),
|
||||
)?)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
mod forward;
|
||||
mod framebuffer;
|
||||
mod host_features;
|
||||
mod install;
|
||||
mod list;
|
||||
mod logcat;
|
||||
mod reboot;
|
||||
|
||||
@@ -32,7 +32,7 @@ impl<R: Read> Read for ADBRecvCommandReader<R> {
|
||||
match &header[..] {
|
||||
b"DATA" => {
|
||||
let length = self.inner.read_u32::<LittleEndian>()? as usize;
|
||||
let effective_read = self.inner.read(buf)?;
|
||||
let effective_read = self.inner.read(&mut buf[0..length])?;
|
||||
self.remaining_data_bytes_to_read = length - effective_read;
|
||||
|
||||
Ok(effective_read)
|
||||
|
||||
@@ -57,7 +57,7 @@ fn search_adb_devices() -> Result<Option<(u16, u16)>> {
|
||||
}
|
||||
}
|
||||
|
||||
match (found_devices.get(0), found_devices.get(1)) {
|
||||
match (found_devices.first(), found_devices.get(1)) {
|
||||
(None, _) => Ok(None),
|
||||
(Some(identifiers), None) => Ok(Some(*identifiers)),
|
||||
(Some((vid1, pid1)), Some((vid2, pid2))) => Err(RustADBError::DeviceNotFound(format!(
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
use crate::{
|
||||
models::AdbStatResponse,
|
||||
usb::{ADBUsbMessage, USBCommand, USBSubcommand},
|
||||
utils::check_extension_is_apk,
|
||||
ADBDeviceExt, ADBUSBDevice, RebootType, Result, RustADBError,
|
||||
};
|
||||
use rand::Rng;
|
||||
use std::io::{Read, Write};
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{Read, Write},
|
||||
};
|
||||
|
||||
use super::USBShellWriter;
|
||||
use super::{USBShellWriter, USBWriter};
|
||||
|
||||
impl ADBDeviceExt for ADBUSBDevice {
|
||||
fn shell_command<S: ToString, W: Write>(
|
||||
@@ -194,4 +198,48 @@ impl ADBDeviceExt for ADBUSBDevice {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn install<P: AsRef<std::path::Path>>(&mut self, apk_path: P) -> Result<()> {
|
||||
let mut apk_file = File::open(&apk_path)?;
|
||||
|
||||
check_extension_is_apk(&apk_path)?;
|
||||
|
||||
let file_size = apk_file.metadata()?.len();
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
let local_id = rng.gen();
|
||||
|
||||
let message = ADBUsbMessage::new(
|
||||
USBCommand::Open,
|
||||
local_id,
|
||||
0,
|
||||
format!("exec:cmd package 'install' -S {}\0", file_size)
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
);
|
||||
self.transport.write_message(message)?;
|
||||
|
||||
let response = self.transport.read_message()?;
|
||||
let remote_id = response.header().arg0();
|
||||
|
||||
let mut writer = USBWriter::new(self.transport.clone(), local_id, remote_id);
|
||||
|
||||
std::io::copy(&mut apk_file, &mut writer)?;
|
||||
|
||||
let final_status = self.transport.read_message()?;
|
||||
|
||||
match final_status.into_payload().as_slice() {
|
||||
b"Success\n" => {
|
||||
log::info!(
|
||||
"APK file {} successfully installed",
|
||||
apk_path.as_ref().display()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
d => Err(crate::RustADBError::ADBRequestFailed(String::from_utf8(
|
||||
d.to_vec(),
|
||||
)?)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@ mod adb_usb_device;
|
||||
mod adb_usb_device_commands;
|
||||
mod adb_usb_message;
|
||||
mod usb_commands;
|
||||
mod usb_shell;
|
||||
mod usb_shell_writer;
|
||||
mod usb_writer;
|
||||
|
||||
pub use adb_rsa_key::ADBRsaKey;
|
||||
pub use adb_usb_device::ADBUSBDevice;
|
||||
pub use adb_usb_message::{ADBUsbMessage, ADBUsbMessageHeader};
|
||||
pub use usb_commands::{USBCommand, USBSubcommand};
|
||||
pub use usb_shell::USBShellWriter;
|
||||
pub use usb_shell_writer::USBShellWriter;
|
||||
pub use usb_writer::USBWriter;
|
||||
|
||||
53
adb_client/src/usb/usb_writer.rs
Normal file
53
adb_client/src/usb/usb_writer.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use std::io::{ErrorKind, Write};
|
||||
|
||||
use crate::USBTransport;
|
||||
|
||||
use super::{ADBUsbMessage, USBCommand};
|
||||
|
||||
/// Wraps a `Writer` to hide underlying ADB protocol write logic.
|
||||
///
|
||||
/// Read received responses to check that message has been received.
|
||||
pub struct USBWriter {
|
||||
transport: USBTransport,
|
||||
local_id: u32,
|
||||
remote_id: u32,
|
||||
}
|
||||
|
||||
impl USBWriter {
|
||||
pub fn new(transport: USBTransport, local_id: u32, remote_id: u32) -> Self {
|
||||
Self {
|
||||
transport,
|
||||
local_id,
|
||||
remote_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for USBWriter {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
let message = ADBUsbMessage::new(
|
||||
USBCommand::Write,
|
||||
self.local_id,
|
||||
self.remote_id,
|
||||
buf.to_vec(),
|
||||
);
|
||||
self.transport
|
||||
.write_message(message)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
||||
|
||||
match self.transport.read_message() {
|
||||
Ok(response) => match response.header().command() {
|
||||
USBCommand::Okay => Ok(buf.len()),
|
||||
c => Err(std::io::Error::new(
|
||||
ErrorKind::Other,
|
||||
format!("wrong response received: {c}"),
|
||||
)),
|
||||
},
|
||||
Err(e) => Err(std::io::Error::new(ErrorKind::Other, e)),
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::{ffi::OsStr, path::Path};
|
||||
|
||||
use crate::{Result, RustADBError};
|
||||
|
||||
pub fn u32_from_le(value: &[u8]) -> Result<u32> {
|
||||
@@ -7,3 +9,16 @@ pub fn u32_from_le(value: &[u8]) -> Result<u32> {
|
||||
.map_err(|_| RustADBError::ConversionError)?,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn check_extension_is_apk<P: AsRef<Path>>(path: P) -> Result<()> {
|
||||
if let Some(extension) = path.as_ref().extension() {
|
||||
if ![OsStr::new("apk")].contains(&extension) {
|
||||
return Err(RustADBError::WrongFileExtension(format!(
|
||||
"{} is not an APK file",
|
||||
extension.to_string_lossy()
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user