Skip to content

Commit

Permalink
allow specifying multiple listen directives at the cli
Browse files Browse the repository at this point in the history
  • Loading branch information
lilioid committed Mar 25, 2024
1 parent 90056e7 commit 721ac62
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 123 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ minifb = { version = "0.25.0", optional = true }
image = { version = "0.25.0", optional = true }
tracing-subscriber = { version = "0.3.17", optional = true }
clap = { version = "4.0.30", optional = true, features = [ "derive" ] }
url = "2.5.0"

[dev-dependencies]
quickcheck = "1.0.3"
Expand Down
113 changes: 24 additions & 89 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
use clap::builder::{PossibleValue, RangedU64ValueParser, TypedValueParser, ValueParserFactory};
use clap::error::ErrorKind;
use clap::{Arg, ArgAction, Args, Error, Parser, Subcommand};
use clap::{ArgAction, Args, Parser, Subcommand};
use pixeldike::pixmap::Color;
use std::ffi::OsStr;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::str::FromStr;
use url::Url;

/// Command-Line arguments as a well formatted struct, parsed using clap.
#[derive(Parser, Debug, Clone)]
Expand Down Expand Up @@ -38,19 +37,11 @@ pub(crate) enum Command {

#[derive(Args, Debug, Clone)]
pub(crate) struct ServerOpts {
/// bind address on which a tcp server should be started
#[arg(long = "tcp")]
pub tcp_bind_addr: Option<SocketAddr>,

/// bind address on which a tcp server should be started
#[cfg(feature = "udp")]
#[arg(long = "udp")]
pub udp_bind_addr: Option<SocketAddr>,

/// port on which to start a websocket listener
#[cfg(feature = "ws")]
#[arg(long = "ws")]
pub ws_bind_addr: Option<SocketAddr>,
/// Url on which to bind a server
///
/// Valid protocols are "tcp://", "udp://" and "ws://".
#[arg(long = "listen")]
pub listen: Vec<Url>,

/// width of the pixmap
#[arg(short = 'x', long = "width", default_value = "800")]
Expand Down Expand Up @@ -132,9 +123,13 @@ pub(crate) struct CommonClientOps {
#[arg(long = "server")]
pub server: SocketAddr,
/// The width of the rectangle that should be drawn
///
/// Possible values: ["fill", <number>]
#[arg(long = "width", default_value = "fill")]
pub width: TargetDimension,
/// The height of the rectangle that should be drawn
///
/// Possible values: ["fill", <number>]
#[arg(long = "height", default_value = "fill")]
pub height: TargetDimension,
/// Offset from the left of the canvas edge to start drawing
Expand Down Expand Up @@ -176,43 +171,17 @@ pub(crate) enum TargetDimension {
Specific(usize),
}

impl ValueParserFactory for TargetDimension {
type Parser = TargetDimensionParser;

fn value_parser() -> Self::Parser {
TargetDimensionParser
}
}

#[derive(Debug, Copy, Clone)]
pub(crate) struct TargetDimensionParser;

impl TypedValueParser for TargetDimensionParser {
type Value = TargetDimension;

fn parse_ref(&self, cmd: &clap::Command, arg: Option<&Arg>, value: &OsStr) -> Result<Self::Value, Error> {
let str_value = value.to_str().ok_or(cmd.clone().error(
ErrorKind::InvalidValue,
format!(
"{} argument is neither 'fill' nor a number",
arg.unwrap().get_id()
),
))?;
impl FromStr for TargetDimension {
type Err = <usize as FromStr>::Err;

if str_value.eq_ignore_ascii_case("fill") {
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.eq_ignore_ascii_case("fill") {
Ok(TargetDimension::Fill)
} else {
RangedU64ValueParser::new()
.parse_ref(cmd, arg, value)
.map(|int_value: usize| TargetDimension::Specific(int_value))
let v = usize::from_str(s)?;
Ok(TargetDimension::Specific(v))
}
}

fn possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>> {
Some(Box::new(
[PossibleValue::new("fill"), PossibleValue::new("<number>")].into_iter(),
))
}
}

#[derive(Debug, Clone)]
Expand All @@ -222,51 +191,17 @@ pub(crate) enum TargetColor {
Specific(Color),
}

impl ValueParserFactory for TargetColor {
type Parser = TargetColorParser;

fn value_parser() -> Self::Parser {
TargetColorParser
}
}

#[derive(Debug, Copy, Clone)]
pub(crate) struct TargetColorParser;
impl FromStr for TargetColor {
type Err = <u32 as FromStr>::Err;

impl TypedValueParser for TargetColorParser {
type Value = TargetColor;

fn parse_ref(&self, cmd: &clap::Command, arg: Option<&Arg>, value: &OsStr) -> Result<Self::Value, Error> {
let make_error = || {
cmd.clone().error(
ErrorKind::InvalidValue,
format!(
"{} argument is neither 'random', 'random-per-iteration' nor a valid hex color",
arg.unwrap().get_id()
),
)
};

let str_value = value.to_str().ok_or(make_error())?;

if str_value.eq_ignore_ascii_case("random") {
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.eq_ignore_ascii_case("random") {
Ok(TargetColor::RandomOnce)
} else if str_value.eq_ignore_ascii_case("random-per-iteration") {
} else if s.eq_ignore_ascii_case("random-per-iteration") {
Ok(TargetColor::RandomPerIteration)
} else {
let color = u32::from_str_radix(str_value, 16).map_err(|_| make_error())?;
let color = u32::from_str_radix(s, 16)?;
Ok(TargetColor::Specific(color.into()))
}
}

fn possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>> {
Some(Box::new(
[
PossibleValue::new("random"),
PossibleValue::new("random-per-iteration"),
PossibleValue::new("<hex-color>"),
]
.into_iter(),
))
}
}
118 changes: 84 additions & 34 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use clap::Parser;
use image::imageops::FilterType;
use rand::prelude::*;
use std::net::SocketAddr;
use std::net::ToSocketAddrs;
use std::sync::Arc;
use std::time::Duration;
use tokio::io::AsyncWriteExt;
Expand All @@ -21,7 +22,7 @@ use image::io::Reader as ImageReader;
use itertools::Itertools;
use pixeldike::net::clients::{GenClient, TcpClient};
use pixeldike::net::protocol::{Request, Response};
use pixeldike::net::servers::{GenServer, TcpServerOptions};
use pixeldike::net::servers::{GenServer, TcpServer, TcpServerOptions};
#[cfg(feature = "udp")]
use pixeldike::net::servers::{UdpServer, UdpServerOptions};
#[cfg(feature = "ws")]
Expand Down Expand Up @@ -176,39 +177,88 @@ async fn start_server(opts: &cli::ServerOpts) {
.expect("Coult not start task for framebuffer rendering");
}

if let Some(bind_addr) = &opts.tcp_bind_addr {
let pixmap = pixmap.clone();
let server = pixeldike::net::servers::TcpServer::new(TcpServerOptions {
bind_addr: bind_addr.to_owned(),
});
server
.start(pixmap, &mut join_set)
.await
.expect("Could not start tcp server");
}

#[cfg(feature = "udp")]
if let Some(udp_bind_addr) = &opts.udp_bind_addr {
let pixmap = pixmap.clone();
let server = UdpServer::new(UdpServerOptions {
bind_addr: udp_bind_addr.to_owned(),
});
server
.start_many(pixmap, 16, &mut join_set)
.await
.expect("Could not start udp server");
}

#[cfg(feature = "ws")]
if let Some(ws_bind_addr) = &opts.ws_bind_addr {
let pixmap = pixmap.clone();
let server = WsServer::new(WsServerOptions {
bind_addr: ws_bind_addr.to_owned(),
});
server
.start(pixmap, &mut join_set)
.await
.expect("Could not start websocket server");
// configure and start all servers
for url in &opts.listen {
match url.scheme() {
#[cfg(feature = "tcp")]
"tcp" => {
if !url.username().is_empty() {
tracing::warn!(
"{} listen directive specifies credentials which is not supported by the TCP server",
url
)
}
if !url.path().is_empty() {
tracing::warn!(
"{} listen directive specifies a path which is not supported by the TCP server",
url
);
}
for bind_addr in (url.host_str().unwrap(), url.port().unwrap_or(1234))
.to_socket_addrs()
.expect("Could not resolve socket addr from listener url")
{
TcpServer::new(TcpServerOptions { bind_addr })
.start(pixmap.clone(), &mut join_set)
.await
.expect(&format!("Could not start tcp server on {}", url));
}
}
#[cfg(feature = "udp")]
"udp" => {
if !url.username().is_empty() {
tracing::info!("{}", url.authority());
tracing::warn!(
"{} listen directive specifies credentials which is not supported by the UDP server",
url
)
}
if !url.path().is_empty() {
tracing::warn!(
"{} listen directive specifies a path which is not supported by the UDP server",
url
);
}
for bind_addr in (url.host_str().unwrap(), url.port().unwrap_or(1234))
.to_socket_addrs()
.expect("Could not resolve socket addr from listener url")
{
UdpServer::new(UdpServerOptions { bind_addr })
.start(pixmap.clone(), &mut join_set)
.await
.expect(&format!("Could not start tcp server on {}", url));
}
}
#[cfg(feature = "ws")]
"ws" => {
if !url.username().is_empty() {
tracing::info!("{}", url.authority());
tracing::warn!(
"{} listen directive specifies credentials which is not supported by the WebSocket server",
url
)
}
if url.path() != "/" {
tracing::warn!(
"{} listen directive specifies a path which is not supported by the WebSocket server. The WebSocket is instead available on all paths.",
url
);
}

for bind_addr in (url.host_str().unwrap(), url.port().unwrap_or(1235))
.to_socket_addrs()
.expect("Could not resolve socket addr from listener url")
{
WsServer::new(WsServerOptions { bind_addr })
.start(pixmap.clone(), &mut join_set)
.await
.expect(&format!("Could not start tcp server on {}", url));
}
}
proto => {
panic!("Unsupported server protocol {}", proto);
}
}
}

// wait until one tasks exits
Expand Down

0 comments on commit 721ac62

Please sign in to comment.