Skip to content

Commit

Permalink
main: Parse per-service cmdline arguments
Browse files Browse the repository at this point in the history
Previously all the params had to be unique between
services because clap didn't yet support what we want:
clap-rs/clap#2222

It still doesn't, but we can hack our way by parsing
the args string manually, and passing different parts to
different sub-parsers.

$ cargo run -- --help
Cabal Online Replacement Services

Usage: cabal-mgr [OPTIONS]
          [-s|--service <SERVICE1> <SERVICE1_OPTIONS>]
          [-s|--service <SERVICE2...>]

Services:
  crypto-mgr  RockAndRoll equivalent
  event-mgr   EventMgr equivalent
  proxy

Options:
  -r, --resources-dir <RESOURCES_DIR>
          [default: .]

  -h, --help
          Print help (see a summary with '-h')

  -V, --version
          Print version

Examples:
  ./cabal_mgr -s crypto-mgr --service event-mgr
  • Loading branch information
darsto committed Mar 19, 2024
1 parent 2f9990d commit d7e511b
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 46 deletions.
149 changes: 137 additions & 12 deletions server/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,154 @@

use std::path::PathBuf;

use clap::{Parser, ValueEnum};
use clap::{Parser, Subcommand, ValueEnum};

#[derive(ValueEnum, Debug, Clone, PartialEq)]
// Enum used for --service string parsing and --help pretty printing
#[derive(Subcommand, ValueEnum, Debug, Clone, PartialEq)]
#[clap(rename_all = "kebab_case")]
pub enum Service {
enum ServiceConfigArg {
/// RockAndRoll equivalent
CryptoMgr,
/// EventMgr equivalent
EventMgr,
Proxy,
}

/// A service configuration
#[derive(Debug)]
pub enum Service {
CryptoMgr(crate::crypto_mgr::CryptoArgs),
EventMgr(crate::event_mgr::EventArgs),
ProxyMgr,
}

impl Service {
fn parse_from_args(stype: ServiceConfigArg, args: std::slice::Iter<String>) -> Self {
match stype {
ServiceConfigArg::CryptoMgr => {
Self::CryptoMgr(crate::crypto_mgr::CryptoArgs::parse_from(args))
}
ServiceConfigArg::EventMgr => {
Self::EventMgr(crate::event_mgr::EventArgs::parse_from(args))
}
ServiceConfigArg::Proxy => todo!(),
}
}
}

/// Common (non-service-specific) configuration
#[derive(Parser, Debug, Default)]
#[clap(name = "cabal-mgr", version)]
pub struct CommonConfig {
#[clap(default_value = ".")]
#[arg(short = 'r', long)]
pub resources_dir: PathBuf,
}

// Clap representation of our argument parsing. This is only used for --help
// and printing some error messages. We want to support running multiple services
// in one app, but clap currently doesn't support that:
// https://github.com/clap-rs/clap/issues/2222
// We work around it by parsing the args string manually, and passing different parts
// to different sub-parsers.

/// Cabal Online Replacement Services
#[derive(Parser, Debug)]
#[command(display_name = "cabal-mgr", bin_name = "cabal-mgr")]
#[command(version, about, long_about, verbatim_doc_comment)]
#[command(subcommand_value_name = "\x08 \n\
\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20[-s|--service <SERVICE1> <SERVICE1_OPTIONS>]\n\
\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20[-s|--service <SERVICE2...>]\x1b")]
#[command(subcommand_help_heading = "Services")]
#[command(disable_help_subcommand = true)]
#[command(after_long_help = "\x1b[1;4mExamples:\x1b[0m\n\
\x20\x20./cabal_mgr -s crypto-mgr --service event-mgr")]
struct Args {
#[command(subcommand)]
service: ServiceConfigArg,
#[clap(flatten)]
common: CommonConfig,
}

// Parser for --service parsing; nothing else
#[derive(Parser, Debug)]
struct ServiceConfig {
#[arg(short = 's', long = "service", value_name = "service")]
inner: ServiceConfigArg,
}

#[derive(Debug)]
pub struct Config {
#[clap(short = 's', long = "service", value_delimiter = ',', num_args = 1..)]
pub services: Vec<Service>,
pub common: CommonConfig,
}

#[clap(flatten)]
pub event_mgr_args: crate::event_mgr::Args,
pub fn parse() -> Config {
let args: Vec<String> = std::env::args().collect();
parse_from(&args)
}

#[clap(flatten)]
pub proxy_args: crate::proxy::ProxyArgs,
pub fn parse_from(args: &[String]) -> Config {
let mut services: Vec<Service> = Vec::new();
let mut common_cfg: Option<CommonConfig> = None;

#[clap(default_value = ".")]
#[arg(short = 'r', long)]
pub resources_dir: PathBuf,
let mut cur_service: Option<(usize, ServiceConfigArg)> = None;
let mut iter = args.iter().enumerate();
while let Some((idx, arg)) = iter.next() {
if arg == "-s" || arg == "--service" {
if let Some(cur_service) = cur_service {
// end collecting the args of the previous service and try to parse them
let service_args = &args[cur_service.0..idx];
println!("{:?} args: {:?}", cur_service.1, service_args); // TODO remove
let service_cfg = Service::parse_from_args(cur_service.1, service_args.iter());
services.push(service_cfg);
} else {
// this is the first -s we see, so parse the previous args
// as common, generic args
let full_cfg =
Args::parse_from(args.iter().take(idx).chain(["crypto-mgr".into()].iter()));
common_cfg = Some(full_cfg.common);
}

let mut service_config_args = vec![&args[0], arg];
if let Some((_, service_name)) = iter.next() {
service_config_args.push(service_name);
}

// delegating all input parsing to clap gives us consistent error
// handling and consistent error messages
let service = ServiceConfig::parse_from(service_config_args);
cur_service = Some((idx + 2, service.inner));
}
}

if let Some(cur_service) = cur_service {
let service_args = &args[cur_service.0..args.len()];
println!("{:?} args: {:?}", cur_service.1, service_args); // TODO remove
let service_cfg = Service::parse_from_args(cur_service.1, service_args.iter());
services.push(service_cfg);
} else {
// there were no services specified, so we should fail;
// but the user might be running --help or --version, or simply no arguments
// at all. just let clippy handle this
Args::parse_from(args.iter());
// in case the user passed a valid service name (without --service or -s),
// clippy will parse it correctly and won't terminate. We know there wasn't any
// --help or --version specified, and we should just complain about the unknown
// argument which is the service name - we let clippy do it by parsing just the
// common args now. This prints incomplete "Usage: " line, but a proper error
// message
CommonConfig::parse_from(args.iter());
unreachable!();
}

Config {
services,
common: common_cfg.unwrap_or_default(),
}
}

pub fn parse_from_str(str: &str) -> Config {
let args = str.split_ascii_whitespace();
let iter = std::env::args().take(1).chain(args.map(|s| s.into()));
parse_from(&iter.collect::<Vec<String>>())
}
13 changes: 9 additions & 4 deletions server/src/crypto_mgr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
use crate::packet_stream::PacketStream;
use crate::ThreadLocalExecutor;
use aria::BlockExt;
use clap::Parser;
use log::{debug, error, info, trace};
use packet::*;

Expand All @@ -17,17 +18,20 @@ use std::{net::TcpListener, sync::Arc};
use anyhow::{bail, Context, Result};
use smol::Async;

#[derive(clap::Parser, Debug, Default)]
pub struct Args {}
#[derive(Parser, Debug)]
pub struct CryptoArgs {}

pub struct Listener {
tcp_listener: Async<TcpListener>,
args: Arc<crate::args::Config>,
}

impl Listener {
pub fn new(tcp_listener: Async<TcpListener>, args: Arc<crate::args::Config>) -> Self {
Self { tcp_listener, args }
pub fn new(tcp_listener: Async<TcpListener>, args: &Arc<crate::args::Config>) -> Self {
Self {
tcp_listener,
args: args.clone(),
}
}

pub async fn listen(&mut self) -> Result<()> {
Expand Down Expand Up @@ -208,6 +212,7 @@ impl Connection {

let path = self
.args
.common
.resources_dir
.join("resources/esym")
.join(req.srchash.0)
Expand Down
9 changes: 5 additions & 4 deletions server/src/event_mgr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

use crate::packet_stream::PacketStream;
use crate::ThreadLocalExecutor;
use clap::Parser;
use log::{error, info, trace};
use packet::*;

Expand All @@ -14,19 +15,19 @@ use std::{net::TcpListener, sync::Arc};
use anyhow::{bail, Result};
use smol::Async;

#[derive(clap::Parser, Debug, Default)]
pub struct Args {}
#[derive(Parser, Debug)]
pub struct EventArgs {}

pub struct Listener {
tcp_listener: Async<TcpListener>,
_args: Arc<crate::args::Config>,
}

impl Listener {
pub fn new(tcp_listener: Async<TcpListener>, args: Arc<crate::args::Config>) -> Self {
pub fn new(tcp_listener: Async<TcpListener>, args: &Arc<crate::args::Config>) -> Self {
Self {
tcp_listener,
_args: args,
_args: args.clone(),
}
}

Expand Down
18 changes: 7 additions & 11 deletions server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
// Binary part of the application.
// Everything else is in lib.rs so it can be unit tested.

use clap::Parser;
use futures::future;
use server::{setup_log, ThreadLocalExecutor};
use smol::Async;
Expand All @@ -13,21 +12,18 @@ use std::{net::TcpListener, sync::Arc};
fn main() {
setup_log(false);

let args = Arc::new(server::args::Config::parse());
if args.services.is_empty() {
eprintln!("At least one --service must be specified. See --help");
return;
}
let args = Arc::new(server::args::parse());
assert!(!args.services.is_empty());

let async_ex = ThreadLocalExecutor::new().unwrap();
if !args
if args
.services
.iter()
.any(|f| *f == server::args::Service::EventMgr)
.any(|f| matches!(f, server::args::Service::EventMgr { .. }))
{
let sock = Async::<TcpListener>::bind(([127, 0, 0, 1], 38171)) //
.expect("Cannot bind to 38171");
let mut event_mgr_listener = server::event_mgr::Listener::new(sock, args.clone());
let mut event_mgr_listener = server::event_mgr::Listener::new(sock, &args);
async_ex
.spawn(async move { event_mgr_listener.listen().await })
.detach();
Expand All @@ -36,11 +32,11 @@ fn main() {
if !args
.services
.iter()
.any(|f| *f == server::args::Service::CryptoMgr)
.any(|f| matches!(f, server::args::Service::CryptoMgr { .. }))
{
let sock = Async::<TcpListener>::bind(([127, 0, 0, 1], 32001)) //
.expect("Cannot bind to 32001");
let mut crypto_mgr_listener = server::crypto_mgr::Listener::new(sock, args.clone());
let mut crypto_mgr_listener = server::crypto_mgr::Listener::new(sock, &args);
async_ex
.spawn(async move { crypto_mgr_listener.listen().await })
.detach();
Expand Down
4 changes: 1 addition & 3 deletions server/src/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,7 @@ impl Listener {

loop {
let (upstream, _) = self.tcp_listener.accept().await?;
let downstream =
Async::<TcpStream>::connect(([10, 2, 0, 143], self.args.proxy_args.conn_port))
.await?;
let downstream = Async::<TcpStream>::connect(([10, 2, 0, 143], 333)).await?;
let upstream_id = upstream.as_raw_fd();
let downstream_id = downstream.as_raw_fd();

Expand Down
10 changes: 3 additions & 7 deletions server/tests/test_crypto_mgr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,13 +186,9 @@ async fn start_client_test() {
async fn start_server() -> Result<()> {
let tcp_listener = Async::<TcpListener>::bind(([127, 0, 0, 1], 32001)) //
.expect("Cannot bind to 32001");
let args = Arc::new(server::args::Config {
services: vec![server::args::Service::CryptoMgr],
resources_dir: PathBuf::from(env!("CARGO_MANIFEST_DIR")),
..Default::default()
});

let mut listener = server::crypto_mgr::Listener::new(tcp_listener, args);
let mut args = server::args::parse_from_str("-s crypto-mgr");
args.common.resources_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let mut listener = server::crypto_mgr::Listener::new(tcp_listener, &Arc::new(args));
listener.listen().await
}

Expand Down
9 changes: 4 additions & 5 deletions server/tests/test_event_mgr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use server::ThreadLocalExecutor;

use std::net::{TcpListener, TcpStream};
use std::os::fd::AsRawFd;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;

Expand Down Expand Up @@ -83,12 +84,10 @@ async fn start_client_test() {
async fn start_server() -> Result<()> {
let tcp_listener = Async::<TcpListener>::bind(([127, 0, 0, 1], 38171)) //
.expect("Cannot bind to 38171");
let args = Arc::new(server::args::Config {
services: vec![server::args::Service::EventMgr],
..Default::default()
});

let mut listener = server::event_mgr::Listener::new(tcp_listener, args);
let mut args = server::args::parse_from_str("-s event-mgr");
args.common.resources_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let mut listener = server::event_mgr::Listener::new(tcp_listener, &Arc::new(args));
listener.listen().await
}

Expand Down

0 comments on commit d7e511b

Please sign in to comment.