sirenia: Add cli module to libsirenia to avoid duplication.
This refactors code from sirenia::cli into a reusable form in
libsirenia::cli so it can be used by cronista.
BUG=None
TEST=cargo test --workspace
Cq-Depend: chromium:2602628
Change-Id: Ibf736493a6ab68f5636e18ac14602daf4c7d0a37
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform2/+/2601643
Tested-by: Allen Webb <allenwebb@google.com>
Reviewed-by: Nicole Anderson-Au <nvaa@google.com>
Commit-Queue: Allen Webb <allenwebb@google.com>
diff --git a/sirenia/Cargo.toml b/sirenia/Cargo.toml
index 600ed96..b12b952 100644
--- a/sirenia/Cargo.toml
+++ b/sirenia/Cargo.toml
@@ -39,3 +39,4 @@
libsirenia = { path = "libsirenia" } # provided by ebuild
serde = { version = "1.0.114", features = ["derive"] }
sys_util = { path = "../../platform/crosvm/sys_util" } # provided by ebuild
+thiserror = "1.0.20"
diff --git a/sirenia/libsirenia/Cargo.toml b/sirenia/libsirenia/Cargo.toml
index 5b36cbc..d2f2dd6 100644
--- a/sirenia/libsirenia/Cargo.toml
+++ b/sirenia/libsirenia/Cargo.toml
@@ -7,6 +7,7 @@
[dependencies]
flexbuffers = "0.1.1"
+getopts = "0.2"
libc = "0.2.44"
libchromeos = { path = "../../libchromeos-rs" } # provided by ebuild
minijail = { path = "../../../aosp/external/minijail/rust/minijail" } # provided by ebuild
diff --git a/sirenia/libsirenia/src/cli/mod.rs b/sirenia/libsirenia/src/cli/mod.rs
new file mode 100644
index 0000000..31e6801
--- /dev/null
+++ b/sirenia/libsirenia/src/cli/mod.rs
@@ -0,0 +1,228 @@
+// Copyright 2020 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use std::marker::PhantomData;
+use std::process::exit;
+
+use getopts::{self, Matches, Options};
+use thiserror::Error as ThisError;
+
+use crate::transport::{self, TransportType, LOOPBACK_DEFAULT};
+
+#[derive(ThisError, Debug)]
+pub enum Error {
+ #[error("failed to parse transport type: {0}")]
+ TransportParse(transport::Error),
+}
+
+/// The result of an operation in this crate.
+pub type Result<T> = std::result::Result<T, Error>;
+
+const DEFAULT_TRANSPORT_TYPE_SHORT_NAME: &str = "U";
+const DEFAULT_TRANSPORT_TYPE_LONG_NAME: &str = "server-url";
+const DEFAULT_TRANSPORT_TYPE_DESC: &str = "URL to the server";
+
+const HELP_OPTION_SHORT_NAME: &str = "h";
+
+/// A TransportType command-line option that allows for multiple transport options to be defined for
+/// the same set of arguments.
+pub struct TransportTypeOption {
+ short_name: String,
+}
+
+impl TransportTypeOption {
+ /// Add the default TransportTypeOption to the specified Options.
+ pub fn default(mut opts: &mut Options) -> Self {
+ Self::new(
+ DEFAULT_TRANSPORT_TYPE_SHORT_NAME,
+ DEFAULT_TRANSPORT_TYPE_LONG_NAME,
+ DEFAULT_TRANSPORT_TYPE_DESC,
+ LOOPBACK_DEFAULT,
+ &mut opts,
+ )
+ }
+
+ /// Add a customized TransportTypeOption to the specified Options.
+ /// Prefer Self::default unless it has already been used.
+ pub fn new(
+ short_name: &str,
+ long_name: &str,
+ desc: &str,
+ hint: &str,
+ opts: &mut Options,
+ ) -> Self {
+ opts.optflagopt(short_name, long_name, desc, hint);
+ TransportTypeOption {
+ short_name: short_name.to_string(),
+ }
+ }
+
+ /// Checks the command line argument matches and returns:
+ /// * Ok(None) - if the option wasn't set.
+ /// * Ok(Some(_: TransportType)) - if parsing succeeded.
+ /// * Err(Error::TransportParse(_)) - if parsing failed.
+ pub fn from_matches(&self, matches: &Matches) -> Result<Option<TransportType>> {
+ match matches.opt_str(&self.short_name) {
+ Some(value) => value
+ .parse::<TransportType>()
+ .map_err(Error::TransportParse)
+ .map(Some),
+ None => Ok(None),
+ }
+ }
+}
+
+trait PrivateTrait {}
+
+/// A help option that wraps commonly used logic. Specifically, it defines the "-h", "--help"
+/// options and provides a helper for showing the usage strings when the option is set or when
+/// the command line options fail to parse.
+pub struct HelpOption {
+ // Force use of the constructor through a private field.
+ phantom: PhantomData<dyn PrivateTrait>,
+}
+
+impl HelpOption {
+ /// Adds a newly created HelpOption to the specified Options.
+ pub fn new(opts: &mut Options) -> Self {
+ let help = HelpOption {
+ phantom: PhantomData,
+ };
+ opts.optflag(help.get_short_name(), "help", "Show this help string.");
+ help
+ }
+
+ /// Return the short_name used by the HelpOption for direct use with Options.
+ pub fn get_short_name(&self) -> &str {
+ HELP_OPTION_SHORT_NAME
+ }
+
+ /// Mocks process::exit for testability. See: parse_and_check_self
+ fn parse_and_check_self_impl<F: Fn(i32) -> ()>(
+ &self,
+ opts: &Options,
+ args: &[String],
+ get_usage: fn() -> String,
+ exit_fn: F,
+ ) -> Option<Matches> {
+ let matches = opts.parse(args).map(Some).unwrap_or_else(|e| {
+ eprintln!("{}", e);
+ println!("{}", opts.usage(&get_usage()));
+ exit_fn(1);
+ None
+ })?;
+
+ if matches.opt_present(self.get_short_name()) {
+ println!("{}", opts.usage(&get_usage()));
+ exit_fn(0);
+ }
+
+ Some(matches)
+ }
+
+ /// A wrapper around Options::parse that handles printing the help string on a parsing error or
+ /// when the help option is set.
+ pub fn parse_and_check_self(
+ &self,
+ opts: &Options,
+ args: &[String],
+ get_usage: fn() -> String,
+ ) -> Matches {
+ self.parse_and_check_self_impl(opts, args, get_usage, |x| exit(x))
+ .unwrap()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ use std::cell::RefCell;
+ use std::rc::Rc;
+
+ fn test_usage() -> String {
+ "test usage string".to_string()
+ }
+
+ fn get_counted_mock_exit(expected: i32) -> (Rc<RefCell<usize>>, impl Fn(i32)) {
+ let counter = Rc::new(RefCell::new(0 as usize));
+ (counter.clone(), move |code: i32| {
+ assert_eq!(code, expected);
+ *counter.borrow_mut() += 1;
+ })
+ }
+
+ #[test]
+ fn transporttypeoption_frommatches_notset() {
+ let test_args: Vec<String> = Vec::new();
+
+ let mut opts = Options::new();
+ let url_option = TransportTypeOption::default(&mut opts);
+
+ let matches = opts.parse(&test_args).unwrap();
+
+ assert!(matches!(url_option.from_matches(&matches), Ok(None)));
+ }
+
+ #[test]
+ fn transporttypeoption_frommatches_valid() {
+ let test_args: Vec<String> = vec!["-U".to_string(), LOOPBACK_DEFAULT.to_string()];
+
+ let mut opts = Options::new();
+ let url_option = TransportTypeOption::default(&mut opts);
+
+ let matches = opts.parse(&test_args).unwrap();
+ assert!(matches!(url_option.from_matches(&matches), Ok(Some(_))));
+ }
+
+ #[test]
+ fn transporttypeoption_frommatches_notvalid() {
+ let test_args: Vec<String> =
+ vec!["-U".to_string(), "not a valid transport type".to_string()];
+
+ let mut opts = Options::new();
+ let url_option = TransportTypeOption::default(&mut opts);
+
+ let matches = opts.parse(&test_args).unwrap();
+ assert!(matches!(
+ url_option.from_matches(&matches),
+ Err(Error::TransportParse(_))
+ ));
+ }
+
+ #[test]
+ fn helpoption_parseandcheckself_notset() {
+ let test_args: Vec<String> = Vec::new();
+
+ let mut opts = Options::new();
+ let help_option = HelpOption::new(&mut opts);
+ help_option.parse_and_check_self_impl(&opts, &test_args, test_usage, |_| panic!());
+ }
+
+ #[test]
+ fn helpoption_parseandcheckself_set() {
+ let test_args: Vec<String> = vec!["-h".to_string()];
+ let (counter, exit_fn) = get_counted_mock_exit(0);
+
+ let mut opts = Options::new();
+ let help_option = HelpOption::new(&mut opts);
+ let matches =
+ help_option.parse_and_check_self_impl(&opts, &test_args, test_usage, |x| exit_fn(x));
+ assert!(matches.is_some());
+ assert_eq!(*counter.borrow(), 1);
+ }
+
+ #[test]
+ fn helpoption_parseandcheckself_invalid() {
+ let test_args: Vec<String> = vec!["--not-an-option".to_string()];
+ let (counter, exit_fn) = get_counted_mock_exit(1);
+
+ let mut opts = Options::new();
+ let help_option = HelpOption::new(&mut opts);
+ let matches =
+ help_option.parse_and_check_self_impl(&opts, &test_args, test_usage, |x| exit_fn(x));
+ assert!(matches.is_none());
+ assert_eq!(*counter.borrow(), 1);
+ }
+}
diff --git a/sirenia/libsirenia/src/lib.rs b/sirenia/libsirenia/src/lib.rs
index 09f824e..eea97f1 100644
--- a/sirenia/libsirenia/src/lib.rs
+++ b/sirenia/libsirenia/src/lib.rs
@@ -5,6 +5,7 @@
//! Ties together the various modules that make up the Sirenia library used by
//! both Trichechus and Dugong.
+pub mod cli;
pub mod communication;
pub mod linux;
pub mod sandbox;
diff --git a/sirenia/src/cli/mod.rs b/sirenia/src/cli/mod.rs
index 2315d45..60c132c 100644
--- a/sirenia/src/cli/mod.rs
+++ b/sirenia/src/cli/mod.rs
@@ -6,31 +6,19 @@
//! Trichechus and Dugong daemons.
use std::env::current_exe;
-use std::fmt::{self, Display};
-use std::process::exit;
use getopts::{self, Options};
use libchromeos::vsock::{SocketAddr as VSocketAddr, VsockCid};
-use libsirenia::transport::{self, TransportType, DEFAULT_SERVER_PORT, LOOPBACK_DEFAULT};
+use libsirenia::cli::{self, HelpOption, TransportTypeOption};
+use libsirenia::transport::{TransportType, DEFAULT_SERVER_PORT};
+use thiserror::Error as ThisError;
use super::build_info::BUILD_TIMESTAMP;
-#[derive(Debug)]
+#[derive(ThisError, Debug)]
pub enum Error {
- /// Error parsing command line options.
- CLIParse(getopts::Fail),
- TransportParse(transport::Error),
-}
-
-impl Display for Error {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- use self::Error::*;
-
- match self {
- CLIParse(e) => write!(f, "failed to parse the command line options: {}", e),
- TransportParse(e) => write!(f, "failed to parse transport type: {}", e),
- }
- }
+ #[error("failed to get transport type option: {0}")]
+ FromMatches(cli::Error),
}
/// The result of an operation in this crate.
@@ -74,30 +62,17 @@
connection_type: default_connection,
};
- let url_name = "U";
-
let mut opts = Options::new();
- opts.optflagopt(
- url_name,
- "server-url",
- "URL to the server",
- LOOPBACK_DEFAULT,
- );
- opts.optflag("h", "help", "Show this help string.");
- let matches = opts.parse(&args[..]).map_err(|e| {
- println!("{}", opts.usage(&get_name_and_version_string()));
- Error::CLIParse(e)
- })?;
+ let help_option = HelpOption::new(&mut opts);
+ let url_option = TransportTypeOption::default(&mut opts);
- if matches.opt_present("h") {
- println!("{}", opts.usage(&get_name_and_version_string()));
- exit(0);
- }
+ let matches = help_option.parse_and_check_self(&opts, &args[..], get_name_and_version_string);
- if let Some(value) = matches.opt_str(url_name) {
- config.connection_type = value
- .parse::<TransportType>()
- .map_err(Error::TransportParse)?
+ if let Some(value) = url_option
+ .from_matches(&matches)
+ .map_err(Error::FromMatches)?
+ {
+ config.connection_type = value;
};
Ok(config)
}
@@ -109,16 +84,6 @@
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
#[test]
- fn initialize_common_arguments_invalid_args() {
- let value: [String; 1] = ["-foo".to_string()];
- let act_result = initialize_common_arguments(&value);
- match &act_result {
- Err(Error::CLIParse(_)) => (),
- _ => panic!("Got unexpected result: {:?}", &act_result),
- }
- }
-
- #[test]
fn initialize_common_arguments_ip_valid() {
let exp_socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)), 1234);
let exp_result = CommonConfig {
diff --git a/sirenia/src/dugong.rs b/sirenia/src/dugong.rs
index 4fe318b..3026940 100644
--- a/sirenia/src/dugong.rs
+++ b/sirenia/src/dugong.rs
@@ -215,9 +215,6 @@
error!("{}", s);
println!("{}", s);
}
- _ => {
- panic!("unexpected message received: {:?}", message);
- }
}
}
}