From 8cfa834f981ce985d706616bfe35972a34ca6078 Mon Sep 17 00:00:00 2001 From: Ahmad Mostafa Date: Tue, 4 Aug 2026 21:29:00 +0300 Subject: [PATCH] Added command line options, serial and state filtering, and other improvements --- Cargo.lock | 213 ++++++++++++++++++++++++++++++++++++++++++--- Cargo.toml | 4 +- src/adb_monitor.rs | 119 ++++++++++++++++--------- src/byte_buffer.rs | 4 + src/main.rs | 87 +++++++++++++++--- 5 files changed, 359 insertions(+), 68 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 51bfa7e..3e89079 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,32 +1,223 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + [[package]] name = "autoadb" version = "0.1.0" dependencies = [ - "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder", + "clap", + "indexmap", + "log", ] [[package]] name = "byteorder" -version = "1.3.2" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "clap" +version = "4.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301b56658598e48f3648647ac6fc887be7e7108eddfa4e9b63fcf3ec58c0cadf" +dependencies = [ + "clap_builder", + "clap_derive", +] [[package]] -name = "cfg-if" -version = "0.1.10" +name = "clap_builder" +version = "4.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94a65403d1a1bd28f7dc68eb8506e8874808ee5eecb59298de588e2e1407a078" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "log" -version = "0.4.8" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-ident", ] -[metadata] -"checksum byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5" -"checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" -"checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/Cargo.toml b/Cargo.toml index a63f371..c33459b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,4 +8,6 @@ edition = "2018" [dependencies] log = "0.4" -byteorder = "1.3" +byteorder = "1.5" +clap = { version = "4.6.5", features = ["derive"] } +indexmap = "2.14.0" diff --git a/src/adb_monitor.rs b/src/adb_monitor.rs index 94d1e9a..7029ab5 100644 --- a/src/adb_monitor.rs +++ b/src/adb_monitor.rs @@ -17,6 +17,7 @@ */ use crate::byte_buffer::ByteBuffer; +use indexmap::IndexMap; use log::*; use std::io::{self, Write}; use std::net::{SocketAddr, TcpStream}; @@ -24,25 +25,41 @@ use std::process; use std::str; use std::thread; use std::time::Duration; +use clap::ValueEnum; const TAG: &str = "AdbMonitor"; +#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] +pub enum DeviceState{ + Unknown, + Offline, + Recovery, + Sideload, + Bootloader, + Unauthorized, + Authorizing, + Connecting, + Device, +} + pub trait AdbMonitorCallback { - fn on_new_device_connected(&self, serial: &str); + fn on_device_state_changed(&self, serial: &str, state: DeviceState); } impl AdbMonitorCallback for F where - F: Fn(&str), + F: Fn(&str, DeviceState), { - fn on_new_device_connected(&self, serial: &str) { - self(serial); + fn on_device_state_changed(&self, serial: &str, state: DeviceState) { + self(serial, state); } } + pub struct AdbMonitor { callback: Box, buf: ByteBuffer, - connected_devices: Vec, + devices: IndexMap, + skip_first_packet_callback: bool, } impl AdbMonitor { @@ -51,11 +68,12 @@ impl AdbMonitor { const RETRY_DELAY_ADB_DAEMON_OK: u64 = 1000; const RETRY_DELAY_ADB_DAEMON_KO: u64 = 5000; - pub fn new(callback: Box) -> Self { + pub fn new(callback: Box, skip_first_packet_callback: bool) -> Self { Self { callback, buf: ByteBuffer::new(Self::BUFFER_SIZE), - connected_devices: Vec::new(), + devices: IndexMap::new(), + skip_first_packet_callback, } } @@ -69,6 +87,8 @@ impl AdbMonitor { } fn track_devices(&mut self) -> io::Result<()> { + self.buf.reset(); + let adbd_addr = SocketAddr::from(([127, 0, 0, 1], 5037)); let mut stream = TcpStream::connect(adbd_addr)?; self.track_devices_on_stream(&mut stream) @@ -77,9 +97,11 @@ impl AdbMonitor { fn track_devices_on_stream(&mut self, stream: &mut TcpStream) -> io::Result<()> { stream.write_all(Self::TRACK_DEVICES_REQUEST)?; if self.consume_okay(stream)? { + let mut is_first = true; loop { let packet = self.next_packet(stream)?; - self.handle_packet(packet.as_str()); + self.handle_packet(packet.as_str(), self.skip_first_packet_callback && is_first); + is_first = false; } } Ok(()) @@ -156,31 +178,42 @@ impl AdbMonitor { } } - fn handle_packet(&mut self, packet: &str) { - let current_connected_devices = self.parse_connected_devices(packet); - for serial in ¤t_connected_devices { - if !self.connected_devices.contains(serial) { - self.callback.on_new_device_connected(serial.as_str()); + fn handle_packet(&mut self, packet: &str, suppress_callback: bool) { + let new_devices = self.parse_devices(packet); + + if !suppress_callback { + for (serial, state) in &new_devices { + let existing_state = self.devices.get(serial); + if existing_state != Some(state) && !(existing_state.is_none() && *state == DeviceState::Offline) { + self.callback.on_device_state_changed(serial.as_str(), *state); + } } } - self.connected_devices = current_connected_devices; + + self.devices = new_devices; } - fn parse_connected_devices(&self, packet: &str) -> Vec { - packet - .lines() - .filter_map(|line| { - let mut split = line.split_whitespace(); - if let Some(serial) = split.next() { - if let Some(state) = split.next() { - if state == "device" { - return Some(serial.to_string()); - } - } - } - None - }) - .collect() + fn parse_devices(&self, packet: &str) -> IndexMap { + packet.lines().filter_map(|line| { + let mut parts = line.split_whitespace(); + + let serial = parts.next()?; + let state_str = parts.next()?; + + let state = match state_str { + "offline" => DeviceState::Offline, + "recovery" => DeviceState::Recovery, + "sideload" => DeviceState::Sideload, + "bootloader" => DeviceState::Bootloader, + "unauthorized" => DeviceState::Unauthorized, + "authorizing" => DeviceState::Authorizing, + "connecting" => DeviceState::Connecting, + "device" => DeviceState::Device, + _ => DeviceState::Unknown, + }; + + Some((serial.to_string(), state)) + }).collect() } fn parse_length(data: &[u8]) -> io::Result { @@ -307,10 +340,10 @@ mod tests { let serial = Rc::new(RefCell::new(None)); let serial_clone = serial.clone(); - let mut monitor = AdbMonitor::new(Box::new(move |serial: &str| { + let mut monitor = AdbMonitor::new(Box::new(move |serial: &str, _| { serial_clone.replace(Some(serial.to_string())); - })); - monitor.handle_packet("0123456789ABCDEF\tdevice\n"); + }), false); + monitor.handle_packet("0123456789ABCDEF\tdevice\n", false); assert_eq!("0123456789ABCDEF", serial.borrow().as_ref().unwrap()); } @@ -320,10 +353,10 @@ mod tests { let serial = Rc::new(RefCell::new(None)); let serial_clone = serial.clone(); - let mut monitor = AdbMonitor::new(Box::new(move |serial: &str| { + let mut monitor = AdbMonitor::new(Box::new(move |serial: &str, _| { serial_clone.replace(Some(serial.to_string())); - })); - monitor.handle_packet("0123456789ABCDEF\toffline\n"); + }), false); + monitor.handle_packet("0123456789ABCDEF\toffline\n", false); assert!(serial.borrow().is_none()); } @@ -333,10 +366,10 @@ mod tests { let serials = Rc::new(RefCell::new(Vec::new())); let serials_clone = serials.clone(); - let mut monitor = AdbMonitor::new(Box::new(move |serial: &str| { + let mut monitor = AdbMonitor::new(Box::new(move |serial: &str, _| { serials_clone.borrow_mut().push(serial.to_string()); - })); - monitor.handle_packet("0123456789ABCDEF\tdevice\nFEDCBA9876543210\tdevice\n"); + }), false); + monitor.handle_packet("0123456789ABCDEF\tdevice\nFEDCBA9876543210\tdevice\n", false); let vec = serials.borrow(); assert_eq!(2, vec.len()); @@ -349,12 +382,12 @@ mod tests { let serials = Rc::new(RefCell::new(Vec::new())); let serials_clone = serials.clone(); - let mut monitor = AdbMonitor::new(Box::new(move |serial: &str| { + let mut monitor = AdbMonitor::new(Box::new(move |serial: &str, _| { serials_clone.borrow_mut().push(serial.to_string()); - })); - monitor.handle_packet("0123456789ABCDEF\tdevice\nFEDCBA9876543210\tdevice\n"); - monitor.handle_packet("0123456789ABCDEF\tdevice\n"); - monitor.handle_packet("0123456789ABCDEF\tdevice\nFEDCBA9876543210\tdevice\n"); + }), false); + monitor.handle_packet("0123456789ABCDEF\tdevice\nFEDCBA9876543210\tdevice\n", false); + monitor.handle_packet("0123456789ABCDEF\tdevice\n", false); + monitor.handle_packet("0123456789ABCDEF\tdevice\nFEDCBA9876543210\tdevice\n", false); let vec = serials.borrow(); assert_eq!(3, vec.len()); diff --git a/src/byte_buffer.rs b/src/byte_buffer.rs index dcebc9e..0fcc45b 100644 --- a/src/byte_buffer.rs +++ b/src/byte_buffer.rs @@ -77,6 +77,10 @@ impl ByteBuffer { } } } + + pub fn reset(&mut self){ + self.head = 0; + } } #[cfg(test)] diff --git a/src/main.rs b/src/main.rs index d573705..be0f0ef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,16 +17,33 @@ mod adb_monitor; mod byte_buffer; -use crate::adb_monitor::{AdbMonitor, AdbMonitorCallback}; -use std::env; +use crate::adb_monitor::{AdbMonitor, AdbMonitorCallback, DeviceState}; use std::process::Command; +use clap::Parser; + struct AutoAdb { command: Vec, + state: DeviceState, + usb_only: bool, + tcp_only: bool, + serials: Vec, } impl AdbMonitorCallback for AutoAdb { - fn on_new_device_connected(&self, serial: &str) { + fn on_device_state_changed(&self, serial: &str, state: DeviceState) { + if self.state != state { + return; + } + + let is_tcp = serial.contains(":") || serial.ends_with("._tcp"); + if (self.usb_only && is_tcp) || (self.tcp_only && !is_tcp) { + return; + } + if !self.serials.is_empty() && !self.serials.contains(&serial.to_string()){ + return; + } + let cmd = self .command .iter() @@ -39,21 +56,65 @@ impl AdbMonitorCallback for AutoAdb { } }) .collect::>(); - println!("Detected device {}", serial); - let process = Command::new(&cmd[0]).args(cmd.iter().skip(1)).spawn(); + + println!("Detected device {}: {:?}", serial, state); + + let process = Command::new(&cmd[0]) + .env("ANDROID_SERIAL", serial) + .args(cmd.iter().skip(1)) + .spawn(); + if let Err(err) = process { - eprintln!("Could not execute {:?}: {}", cmd, err); + eprintln!("Could not execute `{}`: {}", cmd.join(" "), err); } } } +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Cli { + /// Device state to watch for + /// + /// Please note that `offline` will not trigger for devices + /// that were never connected while autoadb was running. + #[arg(value_enum, long, value_name = "STATE", default_value_t = DeviceState::Device)] + on: DeviceState, + + /// Do not run the command for devices already in the target state when autoadb starts + #[arg(short, long)] + ignore_initial: bool, + + /// Only monitor USB devices + #[arg(short, long, conflicts_with = "tcp_only")] + usb_only: bool, + + /// Only monitor TCP/IP devices + #[arg(short, long)] + tcp_only: bool, + + /// Filter by device serial(s), accepts a comma-separated list, can be repeated + #[arg(short, long, value_delimiter = ',')] + serial: Vec, + + /// Command to execute when a device enters the given state + /// + /// "{}" in the arguments will be replaced with the device serial. + /// The `ANDROID_SERIAL` environment variable will also be set to + /// the device serial for this command. + #[arg(required = true, num_args = 1.., trailing_var_arg = true)] + command: Vec, +} + fn main() { - let command = env::args().skip(1).collect::>(); - if command.is_empty() { - eprintln!("No arguments given"); - return; - } - let auto_adb = AutoAdb { command }; - let mut adb_monitor = AdbMonitor::new(Box::new(auto_adb)); + let cli = Cli::parse(); + + let auto_adb = AutoAdb { + command: cli.command, + state: cli.on, + usb_only: cli.usb_only, + tcp_only: cli.tcp_only, + serials: cli.serial + }; + let mut adb_monitor = AdbMonitor::new(Box::new(auto_adb), cli.ignore_initial); adb_monitor.monitor(); }