From c72dcd62468fc5666644b047f63827024e5ea7b3 Mon Sep 17 00:00:00 2001 From: Kevin K Date: Sat, 22 Feb 2020 22:46:10 -0500 Subject: [PATCH] uses clap instead of custom cli arg parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit uses [clap](https://github.com/clap-rs/clap) to handle command line arugments instead of a custom rolled solution. The benefit that `clap` handles all validation as well as providing many "quality of life" or UX improvements such as hidden alias, suggestions on typo, auto-generated help, etc. A previous branch of this project used [structopt](https://github.com/TeXitoi/structopt) (which internally uses `clap`). `structopt` makes heavy use of Rust's Custom Derive feature to make the code less verbose and more ergonomic. `clap` is nearing it's v3 release, which pulls `structopt` into the mainline code base. This commit does not use the v3 `structopt`-like feature-set since that release is still in alpha. This meanas the code is more verbose (yet just as performant), and could be changed to the `structopt`-like features once `clap` v3 is officially released. This commit also does not utilize some of `clap`'s other features like the ability to generate [Shell Completion scripts](https://docs.rs/clap/2.33.0/clap/struct.App.html#method.gen_completions) which could be added later if desired. This commit is *nearly* a 1:1 translation from the original home rolled, to using `clap`. I say "nearly" because `clap` adds many features on top of what the home rolled solution provided "for free" as in with no additional configuration. Those features include: * Argument validation: `--routes`, `--dns-servers`, and `--port` all now have built in validation. If someone enters an IP that isn't valid, or a port that isn't valid an error is returned with a friendly message prior to advancing into `gnirehtet` code. `--routes` also checks the CIDR for validity as well. * Auto generated help message * Suggestions on typos (this feature could be turned off to reduce the binary size, see below) * [Hidden Command aliases](https://docs.rs/clap/2.33.0/clap/struct.App.html#method.alias); The following hidden aliases have been added for commands: * `rt` which maps to `run`: I noticed `gnirehtet rt` used to be used for `gnirehtet run` * Long options for arguments (i.e. `--port` as well as `-p`) * [Hidden Argument aliases](https://docs.rs/clap/2.33.0/clap/struct.Arg.html#method.alias); the following hidden aliases have been added for arguments (which provides help for common typos): * `--dns-server` -> `--dns` -> `--dns-servers`: All of these map to the same thing * `--route` -> `--routes` * Long and Short help messages: `clap` allows one to specify a short message for arguments and commands when `-h` is used, and display a longer more comprehensive message when `--help` is used. * Multiple ways to get help: `clap` provides `-h`|`--help` for the base `gnirehtet` command, as well as all it's subcommands, i.e. `gnirehtet run --help`. There is also a `help` subcommand `gnirehtet help` which can be used alone or to display other commands, i.e. `gnirehtet help run`. This helps because people assume various forms of help, and should be able to find the help messages no matter how they originally try it. Binary Size: I see that binary size is a concern. This is the size after this commit: ``` ❯ ls -l target/release/gnirehtet .rwxrwxr-x 2.2M kevin 23 Feb 13:22 target/release/gnirehtet ❯ strip -g target/release/gnirehtet ❯ ls -l target/release/gnirehtet .rwxrwxr-x 1.1M kevin 23 Feb 13:22 target/release/gnirehtet ❯ strip -s target/release/gnirehtet ❯ ls -l target/release/gnirehtet .rwxrwxr-x 973k kevin 23 Feb 13:22 target/release/gnirehtet ``` So it does increase the binary size, but not dramatically. By further turning off [`clap`'s default cargo features](https://github.com/clap-rs/clap/#optional-dependencies--features), the size may be able to be reduced even more. However, I also am a firm believer that these slight increases are well worth the features they provide. Relates to #243 wip: clap --- relay-rust/Cargo.toml | 1 + relay-rust/src/cli_args.rs | 372 ++++++++++++++++---------- relay-rust/src/main.rs | 519 +++++-------------------------------- 3 files changed, 299 insertions(+), 593 deletions(-) diff --git a/relay-rust/Cargo.toml b/relay-rust/Cargo.toml index 8fa2828a..c88df3c9 100644 --- a/relay-rust/Cargo.toml +++ b/relay-rust/Cargo.toml @@ -16,6 +16,7 @@ chrono = "0.4" # for formatting timestamp in logs byteorder = "1.3" # for reading/writing binary rand = "0.7" # for random TCP sequence number ctrlc = { version = "3.0", features = ["termination"] } # for handling Ctrl+C +clap = "2" # CLI Argument Parsing [profile.release] lto = true # link-time optimization diff --git a/relay-rust/src/cli_args.rs b/relay-rust/src/cli_args.rs index 6fb757fc..8be56d75 100644 --- a/relay-rust/src/cli_args.rs +++ b/relay-rust/src/cli_args.rs @@ -14,168 +14,258 @@ * limitations under the License. */ -pub const PARAM_NONE: u8 = 0; -pub const PARAM_SERIAL: u8 = 1; -pub const PARAM_DNS_SERVERS: u8 = 1 << 1; -pub const PARAM_ROUTES: u8 = 1 << 2; -pub const PARAM_PORT: u8 = 1 << 3; +use std::net::Ipv4Addr; -pub const DEFAULT_PORT: u16 = 31416; +use clap::{ + crate_authors, crate_version, value_t_or_exit, App, AppSettings, Arg, ArgMatches, SubCommand, +}; -pub struct CommandLineArguments { - serial: Option, - dns_servers: Option, - routes: Option, - port: u16, -} - -impl CommandLineArguments { - // simple String as errors is sufficient, we never need to inspect them - pub fn parse>(accepted_parameters: u8, args: Vec) -> Result { - let mut serial = None; - let mut dns_servers = None; - let mut routes = None; - let mut port = 0; +pub const DEFAULT_PORT: &str = "31416"; - let mut iter = args.into_iter(); - while let Some(arg) = iter.next() { - let arg = arg.into(); - if (accepted_parameters & PARAM_DNS_SERVERS) != 0 && "-d" == arg { - if dns_servers.is_some() { - return Err(String::from("DNS servers already set")); - } - if let Some(value) = iter.next() { - dns_servers = Some(value.into()); - } else { - return Err(String::from("Missing -d parameter")); - } - } else if (accepted_parameters & PARAM_ROUTES) != 0 && "-r" == arg { - if routes.is_some() { - return Err(String::from("Routes already set")); - } - if let Some(value) = iter.next() { - routes = Some(value.into()); - } else { - return Err(String::from("Missing -r parameter")); - } - } else if (accepted_parameters & PARAM_PORT) != 0 && "-p" == arg { - if port != 0 { - return Err(String::from("Port already set")); - } - if let Some(value) = iter.next() { - port = value.into().parse().unwrap(); - if port == 0 { - return Err(String::from("Invalid port: 0")); - } - } else { - return Err(String::from("Missing -p parameter")); - } - } else if (accepted_parameters & PARAM_SERIAL) != 0 && serial.is_none() { - serial = Some(arg); +fn valid_port(s: String) -> Result<(), String> { + s.parse::() + .map_err(|_| format!("{} is not a valid number between 1-65,535", s)) + .and_then(|port| { + if port == 0 { + Err(String::from( + "0 is not a valid port number (must be between 1 and 65,535 inclusive)", + )) } else { - return Err(format!("Unexpected argument: \"{}\"", arg)); + Ok(()) } - } - if port == 0 { - port = DEFAULT_PORT; - } - Ok(Self { - serial, - dns_servers, - routes, - port, }) - } - - pub fn serial(&self) -> Option<&str> { - self.serial.as_ref().map(String::as_str) - } - - pub fn dns_servers(&self) -> Option<&str> { - self.dns_servers.as_ref().map(String::as_str) - } +} - pub fn routes(&self) -> Option<&str> { - self.routes.as_ref().map(String::as_str) +fn valid_ip(s: String) -> Result<(), String> { + for ip in s.split(",") { + if let Some(ip) = ip.next() { + match ip.parse::() { + Ok(_) => (), + Err(_) => return Err(format!("{} is not a valid IPv4 Addres", ip)), + } + } } - pub fn port(&self) -> u16 { - self.port - } + Ok(()) } -#[cfg(test)] -mod tests { - use super::*; - - const ACCEPT_ALL: u8 = PARAM_SERIAL | PARAM_DNS_SERVERS | PARAM_ROUTES; - - #[test] - fn test_no_args() { - let args = CommandLineArguments::parse(ACCEPT_ALL, Vec::<&str>::new()).unwrap(); - assert!(args.serial.is_none()); - assert!(args.dns_servers.is_none()); +fn valid_route(s: String) -> Result<(), String> { + for route in s.split(",") { + let mut r_split = route.split("/"); + if let Some(ip) = r_split.next() { + match ip.parse::() { + Ok(_) => (), + Err(_) => return Err(format!("{} is not a valid IPv4 Addres", ip)), + } + } else { + return Err(String::from( + "each route must be in IP/CIDR format, such as 24.24.24.24/8", + )); + } + if let Some(cidr) = r_split.next() { + match cidr.parse::() { + Ok(c) => { + if c > 32 { + return Err(format!( + "{} is not a valid CIDR (must be between 0 and 32 inclusive)", + c + )); + } + } + Err(_) => { + return Err(format!( + "{} is not a valid CIDR (must be between 0 and 32 inclusive)", + cidr + )) + } + } + } else { + return Err(String::from( + "each route must be in IP/CIDR format, such as 24.24.24.24/8", + )); + } } - #[test] - fn test_serial_only() { - let raw_args = vec!["myserial"]; - let args = CommandLineArguments::parse(ACCEPT_ALL, raw_args).unwrap(); - assert_eq!("myserial", args.serial.unwrap()); - } + Ok(()) +} - #[test] - fn test_invalid_paramater() { - let raw_args = vec!["myserial", "other"]; - assert!(CommandLineArguments::parse(ACCEPT_ALL, raw_args).is_err()); - } +pub(crate) fn build() -> App<'static, 'static> { + let serial = Arg::with_name("serial") + .help("The serial of the device (from `adb devices`) to install on") + .long_help( + "The serial of the device (from `adb devices`) to install \ + on. If multiple devices are connected, then this option \ + must be used. If only one device is connectd, the default \ + is to simply connect to it.", + ) + .takes_value(true); + // @TODO validator for IP/CIDR + let route = Arg::with_name("routes") + .help("Only reverse tether the specified routes") + .long_help( + "Only reverse tether the specified routes, \ + whereas the default is to reverse all traffic. \ + Multiple routes may be specified by delimiting \ + with a comma (',')", + ) + .takes_value(true) + .short("r") + .long("routes") + .alias("route") + .use_delimiter(false) + .validator(valid_route) + .default_value("0.0.0.0/0"); + let dns = Arg::with_name("dns-servers") + .help("Make the device use the specified DNS server(s)") + .long_help( + "Make the device use the specified DNS server(s). \ + The default uses Google public DNS. Multiple DNS \ + servers may be specified by delimiting with a comma \ + (',')", + ) + .takes_value(true) + .use_delimiter(false) + .short("d") + .long("dns-servers") + .alias("dns-server") + .alias("dns") + .validator(valid_ip) + .default_value("8.8.8.8"); + let port = Arg::with_name("port") + .help("Make the relay server listen on the specified port") + .takes_value(true) + .short("p") + .long("port") + .default_value(DEFAULT_PORT) + .validator(valid_port); - #[test] - fn test_dns_servers_only() { - let raw_args = vec!["-d", "8.8.8.8"]; - let args = CommandLineArguments::parse(ACCEPT_ALL, raw_args).unwrap(); - assert!(args.serial.is_none()); - assert_eq!("8.8.8.8", args.dns_servers.unwrap()); - } + App::new("gnirehtet") + .author(crate_authors!()) + .version(crate_version!()) + .setting(AppSettings::VersionlessSubcommands) + .setting(AppSettings::SubcommandRequiredElseHelp) + .set_term_width(80) + .subcommand( + SubCommand::with_name("run") + .alias("rt") + .about("Enable reverse tethering for a specific device") + .long_about( + "Enable reverse tethering for a specific device:{n} \ + - install the client if necessary{n} \ + - start the client{n} \ + - start the relay server{n} \ + - on Ctrl+C, stop both the relay server and the client.", + ) + .arg(&serial) + .arg(&dns) + .arg(&port) + .arg(&route), + ) + .subcommand( + SubCommand::with_name("install") + .about("Install the client on the device and exit") + .arg(&serial), + ) + .subcommand( + SubCommand::with_name("uninstall") + .about("Uninstall the client from the Android device and exit") + .arg(&serial), + ) + .subcommand( + SubCommand::with_name("reinstall") + .about("Uninstall then reinstall client on the device") + .arg(&serial), + ) + .subcommand( + SubCommand::with_name("autorun") + .about("Enable reverse tethering for all devices") + .long_about( + "Enable reverse tethering for all devices:{n} \ + - monitor for connected devices and autostart clients{n} \ + - start the relay server", + ) + .arg(&dns) + .arg(&port) + .arg(&route), + ) + .subcommand( + SubCommand::with_name("start") + .about( + "Start a client on the device and exit (10.0.2.2 is mapped to the host 'localhost')", + ) + .arg(&serial) + .arg(&dns) + .arg(&port) + .arg(&route), + ) + .subcommand( + SubCommand::with_name("autostart") + .about("Listen for device connections and start a client on every detected device") + .arg(&dns) + .arg(&port) + .arg(&route), + ) + .subcommand( + SubCommand::with_name("stop") + .about("Stop the client on the device and exit") + .arg(&serial), + ) + .subcommand( + SubCommand::with_name("restart") + .about("Stop client (if running) and then restart on a specific device") + .arg(&serial) + .arg(&dns) + .arg(&port) + .arg(&route), + ) + .subcommand( + SubCommand::with_name("tunnel") + .about("Set up the 'adb reverse' tunnel") + .long_about("Set up the 'adb reverse' tunnel.{n} \ + Note: If a device is unplugged then plugged back while gnirehtet is{n}\ + active, resetting the tunnel is sufficient to get the{n}\ + connection back.", + ) + .arg(&serial) + .arg(&port), + ) + .subcommand( + SubCommand::with_name("relay") + .about("Start the relay server in the current terminal.") + .arg(&port), + ) +} - #[test] - fn test_serial_and_dns_servers() { - let raw_args = vec!["myserial", "-d", "8.8.8.8"]; - let args = CommandLineArguments::parse(ACCEPT_ALL, raw_args).unwrap(); - assert_eq!("myserial", args.serial.unwrap()); - assert_eq!("8.8.8.8", args.dns_servers.unwrap()); - } +#[derive(Clone, Default)] +pub(crate) struct Args { + pub(crate) serial: Option, + pub(crate) dns_servers: Option, + pub(crate) routes: Option, + pub(crate) port: u16, +} - #[test] - fn test_dns_servers_and_serial() { - let raw_args = vec!["-d", "8.8.8.8", "myserial"]; - let args = CommandLineArguments::parse(ACCEPT_ALL, raw_args).unwrap(); - assert_eq!("myserial", args.serial.unwrap()); - assert_eq!("8.8.8.8", args.dns_servers.unwrap()); +impl Args { + pub(crate) fn serial(&self) -> Option<&str> { + self.serial.as_deref() } - - #[test] - fn test_serial_with_no_dns_servers_parameter() { - let raw_args = vec!["myserial", "-d"]; - assert!(CommandLineArguments::parse(ACCEPT_ALL, raw_args).is_err()); + pub(crate) fn routes(&self) -> Option<&str> { + self.routes.as_deref() } - - #[test] - fn test_no_dns_servers_parameter() { - let raw_args = vec!["-d"]; - assert!(CommandLineArguments::parse(ACCEPT_ALL, raw_args).is_err()); + pub(crate) fn dns_servers(&self) -> Option<&str> { + self.dns_servers.as_deref() } - - #[test] - fn test_routes_parameter() { - let raw_args = vec!["-r", "1.2.3.0/24"]; - let args = CommandLineArguments::parse(ACCEPT_ALL, raw_args).unwrap(); - assert_eq!("1.2.3.0/24", args.routes.unwrap()); + pub(crate) fn port(&self) -> u16 { + self.port } +} - #[test] - fn test_no_routes_parameter() { - let raw_args = vec!["-r"]; - assert!(CommandLineArguments::parse(ACCEPT_ALL, raw_args).is_err()); +impl<'a, 'b> From<&'a ArgMatches<'b>> for Args { + fn from(m: &'a ArgMatches<'b>) -> Self { + Args { + serial: m.value_of("serial").map(ToOwned::to_owned), + dns_servers: m.value_of("dns-servers").map(ToOwned::to_owned), + routes: m.value_of("routes").map(ToOwned::to_owned), + port: value_t_or_exit!(m.value_of("port"), u16), + } } } diff --git a/relay-rust/src/main.rs b/relay-rust/src/main.rs index a0ffc382..fe0883fd 100644 --- a/relay-rust/src/main.rs +++ b/relay-rust/src/main.rs @@ -18,6 +18,7 @@ extern crate chrono; extern crate ctrlc; #[macro_use] extern crate log; +extern crate clap; extern crate relaylib; mod adb_monitor; @@ -26,9 +27,8 @@ mod execution_error; mod logger; use crate::adb_monitor::AdbMonitor; -use crate::cli_args::CommandLineArguments; +use crate::cli_args::Args; use crate::execution_error::{Cmd, CommandExecutionError, ProcessIoError, ProcessStatusError}; -use std::env; use std::process::{self, exit}; use std::thread; use std::time::Duration; @@ -36,324 +36,29 @@ use std::time::Duration; const TAG: &str = "Main"; const REQUIRED_APK_VERSION_CODE: &str = "7"; -const COMMANDS: &[&dyn Command] = &[ - &InstallCommand, - &UninstallCommand, - &ReinstallCommand, - &RunCommand, - &AutorunCommand, - &StartCommand, - &AutostartCommand, - &StopCommand, - &RestartCommand, - &TunnelCommand, - &RelayCommand, -]; - -trait Command { - fn command(&self) -> &'static str; - fn accepted_parameters(&self) -> u8; - fn description(&self) -> &'static str; - fn execute(&self, args: &CommandLineArguments) -> Result<(), CommandExecutionError>; -} - -struct InstallCommand; -struct UninstallCommand; -struct ReinstallCommand; -struct RunCommand; -struct AutorunCommand; -struct StartCommand; -struct AutostartCommand; -struct StopCommand; -struct RestartCommand; -struct TunnelCommand; -struct RelayCommand; - -impl Command for InstallCommand { - fn command(&self) -> &'static str { - "install" - } - - fn accepted_parameters(&self) -> u8 { - cli_args::PARAM_SERIAL - } - - fn description(&self) -> &'static str { - "Install the client on the Android device and exit.\n\ - If several devices are connected via adb, then serial must be\n\ - specified." - } - - fn execute(&self, args: &CommandLineArguments) -> Result<(), CommandExecutionError> { - cmd_install(args.serial()) - } -} - -impl Command for UninstallCommand { - fn command(&self) -> &'static str { - "uninstall" - } - - fn accepted_parameters(&self) -> u8 { - cli_args::PARAM_SERIAL - } - - fn description(&self) -> &'static str { - "Uninstall the client from the Android device and exit.\n\ - If several devices are connected via adb, then serial must be\n\ - specified." - } - - fn execute(&self, args: &CommandLineArguments) -> Result<(), CommandExecutionError> { - cmd_uninstall(args.serial()) - } -} - -impl Command for ReinstallCommand { - fn command(&self) -> &'static str { - "reinstall" - } - - fn accepted_parameters(&self) -> u8 { - cli_args::PARAM_SERIAL - } - - fn description(&self) -> &'static str { - "Uninstall then install." - } - - fn execute(&self, args: &CommandLineArguments) -> Result<(), CommandExecutionError> { - cmd_reinstall(args.serial()) - } -} - -impl Command for RunCommand { - fn command(&self) -> &'static str { - "run" - } - - fn accepted_parameters(&self) -> u8 { - cli_args::PARAM_SERIAL - | cli_args::PARAM_DNS_SERVERS - | cli_args::PARAM_ROUTES - | cli_args::PARAM_PORT - } - - fn description(&self) -> &'static str { - "Enable reverse tethering for exactly one device:\n \ - - install the client if necessary;\n \ - - start the client;\n \ - - start the relay server;\n \ - - on Ctrl+C, stop both the relay server and the client." - } - - fn execute(&self, args: &CommandLineArguments) -> Result<(), CommandExecutionError> { - cmd_run( - args.serial(), - args.dns_servers(), - args.routes(), - args.port(), - ) - } -} - -impl Command for AutorunCommand { - fn command(&self) -> &'static str { - "autorun" - } - - fn accepted_parameters(&self) -> u8 { - cli_args::PARAM_DNS_SERVERS | cli_args::PARAM_ROUTES | cli_args::PARAM_PORT - } - - fn description(&self) -> &'static str { - "Enable reverse tethering for all devices:\n \ - - monitor devices and start clients (autostart);\n \ - - start the relay server." - } - - fn execute(&self, args: &CommandLineArguments) -> Result<(), CommandExecutionError> { - cmd_autorun(args.dns_servers(), args.routes(), args.port()) - } -} - -impl Command for StartCommand { - fn command(&self) -> &'static str { - "start" - } - - fn accepted_parameters(&self) -> u8 { - cli_args::PARAM_SERIAL - | cli_args::PARAM_DNS_SERVERS - | cli_args::PARAM_ROUTES - | cli_args::PARAM_PORT - } - - fn description(&self) -> &'static str { - "Start a client on the Android device and exit.\n\ - If several devices are connected via adb, then serial must be\n\ - specified.\n\ - If -d is given, then make the Android device use the specified\n\ - DNS server(s). Otherwise, use 8.8.8.8 (Google public DNS).\n\ - If -r is given, then only reverse tether the specified routes.\n\ - Otherwise, use 0.0.0.0/0 (redirect the whole traffic).\n\ - If -p is given, then make the relay server listen on the specified\n\ - port. Otherwise, use port 31416.\n\ - If the client is already started, then do nothing, and ignore\n\ - the other parameters.\n\ - 10.0.2.2 is mapped to the host 'localhost'." - } - - fn execute(&self, args: &CommandLineArguments) -> Result<(), CommandExecutionError> { - cmd_start( - args.serial(), - args.dns_servers(), - args.routes(), - args.port(), - ) - } -} - -impl Command for AutostartCommand { - fn command(&self) -> &'static str { - "autostart" - } - - fn accepted_parameters(&self) -> u8 { - cli_args::PARAM_DNS_SERVERS | cli_args::PARAM_ROUTES | cli_args::PARAM_PORT - } - - fn description(&self) -> &'static str { - "Listen for device connexions and start a client on every detected\n\ - device.\n\ - Accept the same parameters as the start command (excluding the\n\ - serial, which will be taken from the detected device)." - } - - fn execute(&self, args: &CommandLineArguments) -> Result<(), CommandExecutionError> { - cmd_autostart(args.dns_servers(), args.routes(), args.port()) - } -} - -impl Command for StopCommand { - fn command(&self) -> &'static str { - "stop" - } - - fn accepted_parameters(&self) -> u8 { - cli_args::PARAM_SERIAL - } - - fn description(&self) -> &'static str { - "Stop the client on the Android device and exit.\n\ - If several devices are connected via adb, then serial must be\n\ - specified." - } - - fn execute(&self, args: &CommandLineArguments) -> Result<(), CommandExecutionError> { - cmd_stop(args.serial()) - } -} - -impl Command for RestartCommand { - fn command(&self) -> &'static str { - "restart" - } - - fn accepted_parameters(&self) -> u8 { - cli_args::PARAM_SERIAL - | cli_args::PARAM_DNS_SERVERS - | cli_args::PARAM_ROUTES - | cli_args::PARAM_PORT - } - - fn description(&self) -> &'static str { - "Stop then start." - } - - fn execute(&self, args: &CommandLineArguments) -> Result<(), CommandExecutionError> { - cmd_stop(args.serial())?; - cmd_start( - args.serial(), - args.dns_servers(), - args.routes(), - args.port(), - )?; - Ok(()) - } -} - -impl Command for TunnelCommand { - fn command(&self) -> &'static str { - "tunnel" - } - - fn accepted_parameters(&self) -> u8 { - cli_args::PARAM_SERIAL | cli_args::PARAM_PORT - } - - fn description(&self) -> &'static str { - "Set up the 'adb reverse' tunnel.\n\ - If a device is unplugged then plugged back while gnirehtet is\n\ - active, resetting the tunnel is sufficient to get the\n\ - connection back." - } - - fn execute(&self, args: &CommandLineArguments) -> Result<(), CommandExecutionError> { - cmd_tunnel(args.serial(), args.port()) - } -} - -impl Command for RelayCommand { - fn command(&self) -> &'static str { - "relay" - } - - fn accepted_parameters(&self) -> u8 { - cli_args::PARAM_NONE | cli_args::PARAM_PORT - } - - fn description(&self) -> &'static str { - "Start the relay server in the current terminal." - } - - fn execute(&self, args: &CommandLineArguments) -> Result<(), CommandExecutionError> { - cmd_relay(args.port())?; - Ok(()) - } -} - -fn cmd_install(serial: Option<&str>) -> Result<(), CommandExecutionError> { +fn cmd_install(args: &Args) -> Result<(), CommandExecutionError> { info!(target: TAG, "Installing gnirehtet client..."); - exec_adb(serial, vec!["install", "-r", "gnirehtet.apk"]) + exec_adb(args.serial(), vec!["install", "-r", "gnirehtet.apk"]) } -fn cmd_uninstall(serial: Option<&str>) -> Result<(), CommandExecutionError> { +fn cmd_uninstall(args: &Args) -> Result<(), CommandExecutionError> { info!(target: TAG, "Uninstalling gnirehtet client..."); - exec_adb(serial, vec!["uninstall", "com.genymobile.gnirehtet"]) + exec_adb(args.serial(), vec!["uninstall", "com.genymobile.gnirehtet"]) } -fn cmd_reinstall(serial: Option<&str>) -> Result<(), CommandExecutionError> { - cmd_uninstall(serial)?; - cmd_install(serial)?; - Ok(()) +fn cmd_reinstall(args: &Args) -> Result<(), CommandExecutionError> { + cmd_uninstall(args).and(cmd_install(args)) } -fn cmd_run( - serial: Option<&str>, - dns_servers: Option<&str>, - routes: Option<&str>, - port: u16, -) -> Result<(), CommandExecutionError> { +fn cmd_run(args: Args) -> Result<(), CommandExecutionError> { // start in parallel so that the relay server is ready when the client connects - async_start(serial, dns_servers, routes, port); + async_start(args.clone()); - let ctrlc_serial = serial.map(String::from); + let ctrlc_args = args.clone(); ctrlc::set_handler(move || { info!(target: TAG, "Interrupted"); - let serial = ctrlc_serial.as_ref().map(String::as_ref); - if let Err(err) = cmd_stop(serial) { + if let Err(err) = cmd_stop(&ctrlc_args) { error!(target: TAG, "Cannot stop client: {}", err); } @@ -361,44 +66,30 @@ fn cmd_run( }) .expect("Error setting Ctrl-C handler"); - cmd_relay(port) + cmd_relay(&args) } -fn cmd_autorun( - dns_servers: Option<&str>, - routes: Option<&str>, - port: u16, -) -> Result<(), CommandExecutionError> { - { - let autostart_dns_servers = dns_servers.map(String::from); - let autostart_routes = routes.map(String::from); - thread::spawn(move || { - let dns_servers = autostart_dns_servers.as_ref().map(String::as_ref); - let routes = autostart_routes.as_ref().map(String::as_ref); - if let Err(err) = cmd_autostart(dns_servers, routes, port) { - error!(target: TAG, "Cannot auto start clients: {}", err); - } - }); - } +fn cmd_autorun(args: &Args) -> Result<(), CommandExecutionError> { + let thread_args = args.clone(); + thread::spawn(move || { + if let Err(err) = cmd_autostart(thread_args) { + error!(target: TAG, "Cannot auto start clients: {}", err); + } + }); - cmd_relay(port) + cmd_relay(&args) } -fn cmd_start( - serial: Option<&str>, - dns_servers: Option<&str>, - routes: Option<&str>, - port: u16, -) -> Result<(), CommandExecutionError> { - if must_install_client(serial)? { - cmd_install(serial)?; +fn cmd_start(args: &Args) -> Result<(), CommandExecutionError> { + if must_install_client(args.serial())? { + cmd_install(args)?; // wait a bit after the app is installed so that intent actions are correctly // registered thread::sleep(Duration::from_millis(500)); } info!(target: TAG, "Starting client..."); - cmd_tunnel(serial, port)?; + cmd_tunnel(args)?; let mut adb_args = vec![ "shell", @@ -409,35 +100,29 @@ fn cmd_start( "-n", "com.genymobile.gnirehtet/.GnirehtetActivity", ]; - if let Some(dns_servers) = dns_servers { + if let Some(dns_servers) = args.dns_servers() { adb_args.append(&mut vec!["--esa", "dnsServers", dns_servers]); } - if let Some(routes) = routes { + if let Some(routes) = args.routes() { adb_args.append(&mut vec!["--esa", "routes", routes]); } - exec_adb(serial, adb_args) + exec_adb(args.serial(), adb_args) } -fn cmd_autostart( - dns_servers: Option<&str>, - routes: Option<&str>, - port: u16, -) -> Result<(), CommandExecutionError> { - let start_dns_servers = dns_servers.map(String::from); - let start_routes = routes.map(String::from); +fn cmd_autostart(args: Args) -> Result<(), CommandExecutionError> { let mut adb_monitor = AdbMonitor::new(Box::new(move |serial: &str| { - let dns_servers = start_dns_servers.as_ref().map(String::as_ref); - let routes = start_routes.as_ref().map(String::as_ref); - async_start(Some(serial), dns_servers, routes, port) + async_start(Args { + serial: Some(serial.into()), + ..args.clone() + }) })); - adb_monitor.monitor()?; - Ok(()) + adb_monitor.monitor().map_err(Into::into) } -fn cmd_stop(serial: Option<&str>) -> Result<(), CommandExecutionError> { +fn cmd_stop(args: &Args) -> Result<(), CommandExecutionError> { info!(target: TAG, "Stopping client..."); exec_adb( - serial, + args.serial(), vec![ "shell", "am", @@ -450,32 +135,29 @@ fn cmd_stop(serial: Option<&str>) -> Result<(), CommandExecutionError> { ) } -fn cmd_tunnel(serial: Option<&str>, port: u16) -> Result<(), CommandExecutionError> { +fn cmd_tunnel(args: &Args) -> Result<(), CommandExecutionError> { exec_adb( - serial, + args.serial(), vec![ "reverse", "localabstract:gnirehtet", - format!("tcp:{}", port).as_str(), + format!("tcp:{}", args.port()).as_str(), ], ) } -fn cmd_relay(port: u16) -> Result<(), CommandExecutionError> { - info!(target: TAG, "Starting relay server on port {}...", port); - relaylib::relay(port)?; - Ok(()) +fn cmd_relay(args: &Args) -> Result<(), CommandExecutionError> { + info!( + target: TAG, + "Starting relay server on port {}...", + args.port() + ); + relaylib::relay(args.port()).map_err(Into::into) } -fn async_start(serial: Option<&str>, dns_servers: Option<&str>, routes: Option<&str>, port: u16) { - let start_serial = serial.map(String::from); - let start_dns_servers = dns_servers.map(String::from); - let start_routes = routes.map(String::from); +fn async_start(args: Args) { thread::spawn(move || { - let serial = start_serial.as_ref().map(String::as_ref); - let dns_servers = start_dns_servers.as_ref().map(String::as_ref); - let routes = start_routes.as_ref().map(String::as_ref); - if let Err(err) = cmd_start(serial, dns_servers, routes, port) { + if let Err(err) = cmd_start(&args) { error!(target: TAG, "Cannot start client: {}", err); } }); @@ -554,93 +236,26 @@ fn must_install_client(serial: Option<&str>) -> Result { - // args now contains only the command parameters - let arguments = - CommandLineArguments::parse(command.accepted_parameters(), args.collect()); - match arguments { - Ok(arguments) => { - if let Err(err) = command.execute(&arguments) { - error!(target: TAG, "Execution error: {}", err); - exit(3); - } - } - Err(err) => { - error!(target: TAG, "{}", err); - print_command_usage(command); - exit(2); - } - } - } - None => { - if command_name == "rt" { - error!( - target: TAG, - "The 'rt' command has been renamed to 'run'. Try 'gnirehtet run' instead." - ); - print_command_usage(&RunCommand); - } else { - error!(target: TAG, "Unknown command: {}", command_name); - print_usage(); - } - exit(1); - } - } - } else { - print_usage(); + let matches = cli_args::build().get_matches(); + let res = match matches.subcommand() { + ("install", Some(sub_matches)) => cmd_install(&Args::from(sub_matches)), + ("uninstall", Some(sub_matches)) => cmd_uninstall(&Args::from(sub_matches)), + ("reinstall", Some(sub_matches)) => cmd_reinstall(&Args::from(sub_matches)), + ("run", Some(sub_matches)) => cmd_run(Args::from(sub_matches)), + ("autorun", Some(sub_matches)) => cmd_autorun(&Args::from(sub_matches)), + ("start", Some(sub_matches)) => cmd_start(&Args::from(sub_matches)), + ("autostart", Some(sub_matches)) => cmd_autostart(Args::from(sub_matches)), + ("stop", Some(sub_matches)) => cmd_stop(&Args::from(sub_matches)), + ("tunnel", Some(sub_matches)) => cmd_tunnel(&Args::from(sub_matches)), + ("relay", Some(sub_matches)) => cmd_relay(&Args::from(sub_matches)), + ("", None) => unreachable!(), // impossible due to clap::AppSettings::SubcommandRequiredElseHelp + _ => unreachable!(), + }; + + if let Err(e) = res { + eprintln!("error: {}", e); + exit(1); } }