diff --git a/adb_cli/src/main.rs b/adb_cli/src/main.rs index d4e0861..163b323 100644 --- a/adb_cli/src/main.rs +++ b/adb_cli/src/main.rs @@ -48,7 +48,7 @@ fn run_command(mut device: Box, command: DeviceCommands) -> AD device.shell(&mut std::io::stdin(), Box::new(std::io::stdout()))?; } } else { - device.shell_command(&commands.join(" "), &mut std::io::stdout())?; + device.shell_command(&commands.join(" "), Some(&mut std::io::stdout()), None)?; } } DeviceCommands::Pull { diff --git a/adb_cli/src/models/adb_cli_error.rs b/adb_cli/src/models/adb_cli_error.rs index e479fcf..c2de549 100644 --- a/adb_cli/src/models/adb_cli_error.rs +++ b/adb_cli/src/models/adb_cli_error.rs @@ -43,6 +43,7 @@ impl From for ADBCliError { | RustADBError::FramebufferConversionError | RustADBError::UnimplementedFramebufferImageVersion(_) | RustADBError::IOError(_) + | RustADBError::ADBShellV2ParseError(_) | RustADBError::ADBRequestFailed(_) | RustADBError::UnknownDeviceState(_) | RustADBError::Utf8StrError(_) diff --git a/adb_client/src/adb_device_ext.rs b/adb_client/src/adb_device_ext.rs index f8dbac5..b94da9a 100644 --- a/adb_client/src/adb_device_ext.rs +++ b/adb_client/src/adb_device_ext.rs @@ -9,7 +9,12 @@ use crate::{RebootType, Result}; /// Trait representing all features available on ADB devices. pub trait ADBDeviceExt { /// Runs command in a shell on the device, and write its output and error streams into output. - fn shell_command(&mut self, command: &dyn AsRef, output: &mut dyn Write) -> Result<()>; + fn shell_command( + &mut self, + command: &dyn AsRef, + stdout: Option<&mut dyn Write>, + stderr: Option<&mut dyn Write>, + ) -> Result>; /// Starts an interactive shell session on the device. /// Input data is read from reader and write to writer. @@ -49,14 +54,15 @@ pub trait ADBDeviceExt { activity: &dyn AsRef, ) -> Result> { let mut output = Vec::new(); - self.shell_command( + let _status = self.shell_command( &format!( "am start {}/{}.{}", package.as_ref(), package.as_ref(), activity.as_ref() ), - &mut output, + Some(&mut output), + None, )?; Ok(output) diff --git a/adb_client/src/error.rs b/adb_client/src/error.rs index 94859af..c315967 100644 --- a/adb_client/src/error.rs +++ b/adb_client/src/error.rs @@ -9,6 +9,9 @@ pub enum RustADBError { /// Indicates that an error occurred with I/O. #[error(transparent)] IOError(#[from] std::io::Error), + /// Indicates that an error occurred during ADB shell v2 parsing. + #[error("ADB shell v2 parsing error: {0}")] + ADBShellV2ParseError(String), /// Indicates that an error occurred when sending ADB request. #[error("ADB request failed - {0}")] ADBRequestFailed(String), diff --git a/adb_client/src/message_devices/adb_message_device_commands.rs b/adb_client/src/message_devices/adb_message_device_commands.rs index 58cb959..c7374f0 100644 --- a/adb_client/src/message_devices/adb_message_device_commands.rs +++ b/adb_client/src/message_devices/adb_message_device_commands.rs @@ -12,8 +12,13 @@ use std::{ impl ADBDeviceExt for ADBMessageDevice { #[inline] - fn shell_command(&mut self, command: &dyn AsRef, output: &mut dyn Write) -> Result<()> { - self.shell_command(command, output) + fn shell_command( + &mut self, + command: &dyn AsRef, + stdout: Option<&mut dyn Write>, + stderr: Option<&mut dyn Write>, + ) -> Result> { + self.shell_command(command, stdout, stderr) } #[inline] diff --git a/adb_client/src/message_devices/commands/shell.rs b/adb_client/src/message_devices/commands/shell.rs index f63e0d0..7a34a7c 100644 --- a/adb_client/src/message_devices/commands/shell.rs +++ b/adb_client/src/message_devices/commands/shell.rs @@ -15,8 +15,9 @@ impl ADBMessageDevice { pub(crate) fn shell_command( &mut self, command: &dyn AsRef, - output: &mut dyn Write, - ) -> Result<()> { + mut stdout: Option<&mut dyn Write>, + _stderr: Option<&mut dyn Write>, + ) -> Result> { let mut session = self.open_session(&ADBLocalCommand::ShellCommand( command.as_ref().to_string(), Vec::new(), @@ -27,10 +28,13 @@ impl ADBMessageDevice { if message.header().command() == MessageCommand::Clse { break; } - output.write_all(&message.into_payload())?; + // should this just write for ::Write messages? + if let Some(ref mut stdout) = stdout { + stdout.write_all(&message.into_payload())?; + } } - Ok(()) + Ok(None) } /// Starts an interactive shell session on the device. diff --git a/adb_client/src/message_devices/tcp/adb_tcp_device.rs b/adb_client/src/message_devices/tcp/adb_tcp_device.rs index e8751fa..a26020d 100644 --- a/adb_client/src/message_devices/tcp/adb_tcp_device.rs +++ b/adb_client/src/message_devices/tcp/adb_tcp_device.rs @@ -102,8 +102,13 @@ impl ADBTcpDevice { impl ADBDeviceExt for ADBTcpDevice { #[inline] - fn shell_command(&mut self, command: &dyn AsRef, output: &mut dyn Write) -> Result<()> { - self.inner.shell_command(command, output) + fn shell_command( + &mut self, + command: &dyn AsRef, + stdout: Option<&mut dyn Write>, + stderr: Option<&mut dyn Write>, + ) -> Result> { + self.inner.shell_command(command, stdout, stderr) } #[inline] diff --git a/adb_client/src/message_devices/usb/README.md b/adb_client/src/message_devices/usb/README.md index 4160913..a7d857a 100644 --- a/adb_client/src/message_devices/usb/README.md +++ b/adb_client/src/message_devices/usb/README.md @@ -8,7 +8,7 @@ use adb_client::{usb::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", &mut std::io::stdout()); +device.shell_command(&"df -h", Some(&mut std::io::stdout()), None); ``` ## Push a file to the device diff --git a/adb_client/src/message_devices/usb/adb_usb_device.rs b/adb_client/src/message_devices/usb/adb_usb_device.rs index 6a0bc9a..af91781 100644 --- a/adb_client/src/message_devices/usb/adb_usb_device.rs +++ b/adb_client/src/message_devices/usb/adb_usb_device.rs @@ -236,8 +236,13 @@ impl ADBUSBDevice { impl ADBDeviceExt for ADBUSBDevice { #[inline] - fn shell_command(&mut self, command: &dyn AsRef, output: &mut dyn Write) -> Result<()> { - self.inner.shell_command(command, output) + fn shell_command( + &mut self, + command: &dyn AsRef, + stdout: Option<&mut dyn Write>, + stderr: Option<&mut dyn Write>, + ) -> Result> { + self.inner.shell_command(command, stdout, stderr) } #[inline] diff --git a/adb_client/src/server_device/README.md b/adb_client/src/server_device/README.md index 0371858..cd17530 100644 --- a/adb_client/src/server_device/README.md +++ b/adb_client/src/server_device/README.md @@ -7,7 +7,7 @@ use adb_client::{server::ADBServer, ADBDeviceExt}; let mut server = ADBServer::default(); let mut device = server.get_device().expect("cannot get device"); -device.shell_command(&"df -h", &mut std::io::stdout()); +device.shell_command(&"df -h", Some(&mut std::io::stdout()), None); ``` ## Push a file to the device diff --git a/adb_client/src/server_device/adb_server_device_commands.rs b/adb_client/src/server_device/adb_server_device_commands.rs index 7921838..574ac74 100644 --- a/adb_client/src/server_device/adb_server_device_commands.rs +++ b/adb_client/src/server_device/adb_server_device_commands.rs @@ -3,6 +3,8 @@ use std::{ path::Path, }; +use byteorder::ReadBytesExt; + use crate::{ ADBDeviceExt, ADBListItemType, Result, RustADBError, models::{ADBCommand, ADBLocalCommand, AdbStatResponse, HostFeatures, RemountInfo}, @@ -12,8 +14,36 @@ use super::ADBServerDevice; const BUFFER_SIZE: usize = 65535; +#[derive(Eq, PartialEq)] +enum ShellChannel { + Stdout, + Stderr, + ExitStatus, +} + +impl TryFrom for ShellChannel { + type Error = std::io::Error; + + fn try_from(value: u8) -> std::result::Result { + match value { + 1 => Ok(ShellChannel::Stdout), + 2 => Ok(ShellChannel::Stderr), + 3 => Ok(ShellChannel::ExitStatus), + _ => Err(std::io::Error::new( + ErrorKind::InvalidData, + "Invalid channel", + )), + } + } +} + impl ADBDeviceExt for ADBServerDevice { - fn shell_command(&mut self, command: &dyn AsRef, output: &mut dyn Write) -> Result<()> { + fn shell_command( + &mut self, + command: &dyn AsRef, + mut stdout: Option<&mut dyn Write>, + mut stderr: Option<&mut dyn Write>, + ) -> Result> { let supported_features = self.host_features()?; if !supported_features.contains(&HostFeatures::ShellV2) && !supported_features.contains(&HostFeatures::Cmd) @@ -44,17 +74,86 @@ impl ADBDeviceExt for ADBServerDevice { args, )))?; + // Now decode the shell v2 protocol packets, reference: + // https://android.googlesource.com/platform/packages/modules/adb/+/refs/heads/main/shell_protocol.h + + let mut exit = None; + let mut input = std::io::BufReader::new(self.transport.get_raw_connection()?); + let mut buffer = vec![0; BUFFER_SIZE].into_boxed_slice(); loop { - match self.transport.get_raw_connection()?.read(&mut buffer) { - Ok(size) => { - if size == 0 { - return Ok(()); + // 1 byte of channel + // 4 bytes of payload size + let mut pckt_metadata = vec![0; 5]; + if let Err(err) = input.read_exact(&mut pckt_metadata) { + match err.kind() { + ErrorKind::UnexpectedEof | ErrorKind::BrokenPipe => return Ok(None), + _ => return Err(RustADBError::IOError(err)), + } + } + + let (channel, payload_size) = { + let channel = pckt_metadata[0]; + let payload_size = u32::from_le_bytes(pckt_metadata[1..5].try_into()?) as usize; + (ShellChannel::try_from(channel)?, payload_size) + }; + + if payload_size == 0 { + continue; + } + + match channel { + ShellChannel::Stdout | ShellChannel::Stderr => { + let mut remainder = payload_size; + while remainder > 0 { + let to_read = std::cmp::min(remainder, BUFFER_SIZE); + match input.read(&mut buffer[0..to_read]) { + Ok(size) => { + if size == 0 { + return Ok(exit); + } + + match channel { + ShellChannel::Stdout => { + if let Some(stdout) = stdout.as_mut() { + stdout.write_all(&buffer[..size])?; + } + } + ShellChannel::Stderr => { + // first stderr if existing, else a merged output into stdout + if let Some(writer) = stderr.as_mut() { + writer.write_all(&buffer[..size])?; + } else if let Some(writer) = stdout.as_mut() { + writer.write_all(&buffer[..size])?; + } + } + ShellChannel::ExitStatus => { + // unreachable + } + } + + remainder -= size; + } + Err(e) => { + return Err(RustADBError::IOError(e)); + } + } } - output.write_all(&buffer[..size])?; } - Err(e) => { - return Err(RustADBError::IOError(e)); + ShellChannel::ExitStatus => { + if payload_size != 1 { + return Err(RustADBError::ADBShellV2ParseError(format!( + "Spurious exit status packet with size of {payload_size} (should be 1)" + ))); + } + + match input.read_u8() { + Ok(status) => exit = Some(status), + Err(err) => match err.kind() { + ErrorKind::UnexpectedEof | ErrorKind::BrokenPipe => return Ok(None), + _ => return Err(RustADBError::IOError(err)), + }, + } } } } diff --git a/adb_client/src/server_device/commands/logcat.rs b/adb_client/src/server_device/commands/logcat.rs index aadfa3b..914308b 100644 --- a/adb_client/src/server_device/commands/logcat.rs +++ b/adb_client/src/server_device/commands/logcat.rs @@ -49,6 +49,7 @@ impl Write for LogFilter { impl ADBServerDevice { /// Get logs from device pub fn get_logs(&mut self, output: W) -> Result<()> { - self.shell_command(&"exec logcat", &mut LogFilter::new(output)) + let _status = self.shell_command(&"exec logcat", Some(&mut LogFilter::new(output)), None); + Ok(()) } } diff --git a/pyadb_client/src/adb_server_device.rs b/pyadb_client/src/adb_server_device.rs index 7f56bb5..0b81440 100644 --- a/pyadb_client/src/adb_server_device.rs +++ b/pyadb_client/src/adb_server_device.rs @@ -22,7 +22,7 @@ impl PyADBServerDevice { /// Run shell commands on device and return the output (stdout + stderr merged) pub fn shell_command(&mut self, command: &str) -> Result> { let mut output = Vec::new(); - self.0.shell_command(&command, &mut output)?; + self.0.shell_command(&command, Some(&mut output), None)?; Ok(output) } diff --git a/pyadb_client/src/adb_usb_device.rs b/pyadb_client/src/adb_usb_device.rs index 505bf7d..1e6a576 100644 --- a/pyadb_client/src/adb_usb_device.rs +++ b/pyadb_client/src/adb_usb_device.rs @@ -23,7 +23,7 @@ impl PyADBUSBDevice { /// Run shell commands on device and return the output (stdout + stderr merged) pub fn shell_command(&mut self, command: &str) -> Result> { let mut output = Vec::new(); - self.0.shell_command(&command, &mut output)?; + self.0.shell_command(&command, Some(&mut output), None)?; Ok(output) }