"memchr",
]
+[[package]]
+name = "ansi_term"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
+dependencies = [
+ "winapi",
+]
+
[[package]]
name = "anyhow"
version = "1.0.38"
"event-listener",
]
+[[package]]
+name = "async-native-tls"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e9e7a929bd34c68a82d58a4de7f86fffdaf97fb2af850162a7bb19dd7269b33"
+dependencies = [
+ "async-std",
+ "native-tls",
+ "thiserror",
+ "url",
+]
+
[[package]]
name = "async-process"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+[[package]]
+name = "bitflags"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
+
[[package]]
name = "block-buffer"
version = "0.9.0"
"generic-array",
]
+[[package]]
+name = "clap"
+version = "2.33.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
+dependencies = [
+ "ansi_term",
+ "atty",
+ "bitflags",
+ "strsim",
+ "textwrap",
+ "unicode-width",
+ "vec_map",
+]
+
[[package]]
name = "combine"
version = "4.5.2"
"version_check",
]
+[[package]]
+name = "core-foundation"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b"
+
[[package]]
name = "cpuid-bool"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
[[package]]
name = "form_urlencoded"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
+[[package]]
+name = "native-tls"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8d96b2e1c8da3957d58100b09f102c6d9cfdfced01b7ec5a8974044bb09dbd4"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
[[package]]
name = "nb-connect"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
+[[package]]
+name = "openssl"
+version = "0.10.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "038d43985d1ddca7a9900630d8cd031b56e4794eecc2e9ea39dd17aa04399a70"
+dependencies = [
+ "bitflags",
+ "cfg-if 1.0.0",
+ "foreign-types",
+ "lazy_static",
+ "libc",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "921fc71883267538946025deffb622905ecad223c28efbfdef9bb59a0175f3e6"
+dependencies = [
+ "autocfg",
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
[[package]]
name = "parking"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+[[package]]
+name = "pkg-config"
+version = "0.3.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c"
+
[[package]]
name = "pokerwave"
version = "0.1.0"
dependencies = [
"async-std",
+ "clap",
"env_logger",
"futures",
"getrandom 0.2.2",
"tide",
"tide-rustls",
"tide-websockets",
+ "toml",
]
[[package]]
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eeb8f8d059ead7805e171fc22de8348a3d611c0f985aaa4f5cf6c0dfc7645407"
dependencies = [
+ "async-native-tls",
"async-std",
"async-trait",
"bytes 1.0.1",
"dtoa",
"futures-util",
"itoa",
+ "native-tls",
"percent-encoding",
"pin-project-lite 0.2.6",
"sha1",
"url",
]
+[[package]]
+name = "redox_syscall"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9"
+dependencies = [
+ "bitflags",
+]
+
[[package]]
name = "regex"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581"
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
[[package]]
name = "ring"
version = "0.16.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
+[[package]]
+name = "schannel"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
+dependencies = [
+ "lazy_static",
+ "winapi",
+]
+
[[package]]
name = "sct"
version = "0.6.0"
"untrusted",
]
+[[package]]
+name = "security-framework"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dfd318104249865096c8da1dfabf09ddbb6d0330ea176812a62ec75e40c4166"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dee48cdde5ed250b0d3252818f646e174ab414036edb884dde62d80a3ac6082d"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
[[package]]
name = "semver"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0"
+[[package]]
+name = "strsim"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
+
[[package]]
name = "subtle"
version = "2.4.0"
"unicode-xid",
]
+[[package]]
+name = "tempfile"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "rand 0.8.3",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi",
+]
+
[[package]]
name = "termcolor"
version = "1.1.2"
"winapi-util",
]
+[[package]]
+name = "textwrap"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
+dependencies = [
+ "unicode-width",
+]
+
[[package]]
name = "thiserror"
version = "1.0.24"
"tokio",
]
+[[package]]
+name = "toml"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
+dependencies = [
+ "serde",
+]
+
[[package]]
name = "tungstenite"
version = "0.11.1"
"tinyvec",
]
+[[package]]
+name = "unicode-width"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
+
[[package]]
name = "unicode-xid"
version = "0.2.1"
"sval",
]
+[[package]]
+name = "vcpkg"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b00bca6106a5e23f3eee943593759b7fcddb00554332e856d990c893966879fb"
+
[[package]]
name = "vec-arena"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eafc1b9b2dfc6f5529177b62cf806484db55b32dc7c9658a118e11bbeb33061d"
+[[package]]
+name = "vec_map"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
+
[[package]]
name = "version_check"
version = "0.9.2"
[dependencies]
async-std = { version = "1", features = ["attributes"] }
+clap = "2"
env_logger = "0.8"
log = "0.4"
futures = "0.3"
pin-project = "1.0"
rand = "0.8"
rand_chacha = "0.3"
-redis = { version = "0.20", features = ["async-std-comp"] }
+redis = { version = "0.20", features = ["async-std-tls-comp"] }
serde = "1"
serde_derive = "1"
serde_json = "1"
tide = { version = "0.16.0", default-features = false, features = ["h1-server"] }
tide-rustls = "0.2"
tide-websockets = "0.3"
+toml = "0.5"
--- /dev/null
+[log]
+filter = "info"
+style = "auto"
+
+[redis]
+#addr = "redis://pokerwave:59f278cd2a96756d67884976f54c1e490d00cb4f7ff62c24a380680e5f5098a3@localhost/0"
+
+[server]
+bind = ["localhost:4343"]
+site = "site"
+cert = "cert/cert.pem"
+key = "cert/key.pem"
--- /dev/null
+use std::path::PathBuf;
+
+#[derive(Clone, Debug, Default, Deserialize)]
+pub struct Config {
+ #[serde(default)]
+ pub log: LogConfig,
+ #[serde(default)]
+ pub redis: RedisConfig,
+ #[serde(default)]
+ pub server: ServerConfig,
+}
+
+fn default_filter() -> String {
+ "warn".to_string()
+}
+
+fn default_style() -> String {
+ "auto".to_string()
+}
+
+#[derive(Clone, Debug, Deserialize)]
+pub struct LogConfig {
+ #[serde(default = "default_filter")]
+ pub filter: String,
+ #[serde(default = "default_style")]
+ pub style: String,
+}
+
+impl Default for LogConfig {
+ fn default() -> Self {
+ Self { filter: default_filter(), style: default_style() }
+ }
+}
+
+fn default_redis_addr() -> String {
+ "redis://localhost".to_string()
+}
+
+#[derive(Clone, Debug, Deserialize)]
+pub struct RedisConfig {
+ #[serde(default = "default_redis_addr")]
+ pub addr: String,
+}
+
+impl Default for RedisConfig {
+ fn default() -> Self {
+ Self { addr: default_redis_addr() }
+ }
+}
+
+fn default_bind_addr() -> Vec<String> {
+ vec!["localhost:8080".to_string()]
+}
+
+fn default_site_path() -> PathBuf {
+ PathBuf::from("site")
+}
+
+#[derive(Clone, Debug, Deserialize)]
+pub struct ServerConfig {
+ #[serde(default = "default_bind_addr")]
+ pub bind: Vec<String>,
+ #[serde(default = "default_site_path")]
+ pub site: PathBuf,
+ #[serde(default)]
+ pub cert: Option<String>,
+ #[serde(default)]
+ pub key: Option<String>,
+}
+
+impl Default for ServerConfig {
+ fn default() -> Self {
+ Self { bind: default_bind_addr(), site: default_site_path(), cert: None, key: None }
+ }
+}
use std::collections::HashSet;
+use std::fmt::{self, Display};
+use std::str::FromStr;
use async_std::task::spawn;
use futures::{channel::mpsc::Receiver, StreamExt};
game: Box<dyn Game>,
}
+#[derive(Copy, Clone, Debug)]
+pub struct Partition {
+ n: i64,
+ m: i64,
+}
+
+impl Partition {
+ fn start_dealer_for(&self, id: i64) -> bool {
+ id.rem_euclid(self.m) == self.n - 1
+ }
+}
+
+impl FromStr for Partition {
+ type Err = String;
+ fn from_str(str: &str) -> Result<Self, Self::Err> {
+ if let [n, m] = *str.split('/').collect::<Vec<_>>() {
+ match (n.parse::<i64>(), m.parse::<i64>()) {
+ (Ok(n), Ok(m)) if n >= 1 && m >= n => Ok(Partition { n, m }),
+ (Ok(n), Ok(m)) if n < 1 => Err(format!("The partition number N must be >= 1; got {}/{}", n, m)),
+ (Ok(n), Ok(m)) => Err(format!("The partition number N must be <= total partitions M; got {}/{}", n, m)),
+ _ => Err(format!("The partition must be expressed as two integers N/M; got {:?}", str)),
+ }
+ } else {
+ Err(format!("The partition must be expressed in the form N/M; got {:?}", str))
+ }
+ }
+}
+
+impl Display for Partition {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{}/{}", self.n, self.m)
+ }
+}
+
+impl Default for Partition {
+ fn default() -> Self {
+ Partition { n: 1, m: 1 }
+ }
+}
+
enum Termination {
Continue,
Break,
}
}
-pub async fn spawn_dealers(server: Server) -> Result<(), std::io::Error> {
+pub async fn spawn_dealers(server: Server, partition: Partition) -> Result<(), std::io::Error> {
let (mut server_state, mut update_stream) = server.new_state().await;
let mut interests = HashSet::new();
interests.insert(ClientInterest::GameList);
info!("Starting new game {:?}", game);
let id = game.id();
game_list.push(game);
- let (server_state, update_stream) = server.new_state().await;
- if let Ok(dealer) = Dealer::new(server_state, id).await {
- info!("Spawning new dealer for game {}", id);
- spawn(dealer.start(update_stream));
+ if partition.start_dealer_for(id) {
+ let (server_state, update_stream) = server.new_state().await;
+ if let Ok(dealer) = Dealer::new(server_state, id).await {
+ info!("Spawning new dealer for game {}", id);
+ spawn(dealer.start(update_stream));
+ }
+ } else {
+ debug!("Not spawning new dealer for game {} for partition {}", id, partition);
}
}
}
#[macro_use]
extern crate serde_derive;
-use futures::{channel::mpsc::channel, future::select, pin_mut, FutureExt, StreamExt};
+use std::fs::read_to_string;
+use std::future::Future;
+use std::pin::Pin;
+
+use clap::{app_from_crate, crate_authors, crate_description, crate_name, crate_version, AppSettings, Arg, SubCommand};
+use futures::{channel::mpsc::channel, future::pending, select, FutureExt, StreamExt};
use redis::Client;
use signal_hook::consts::signal::*;
use signal_hook_async_std::Signals;
use tide::utils::After;
-use tide::{Body, Error, Response, StatusCode};
-//use tide_rustls::TlsListener;
+use tide::{
+ listener::{ConcurrentListener, ToListener},
+ Body, Response, StatusCode,
+};
+use tide_rustls::TlsListener;
use tide_websockets::WebSocket;
mod api;
mod auth;
mod card;
mod client;
+mod config;
mod dealer;
mod game;
mod pubsub;
mod util;
use crate::client::new_client;
-use crate::dealer::spawn_dealers;
+use crate::config::Config;
+use crate::dealer::{spawn_dealers, Partition};
use crate::pubsub::handle_client_interest_subscriptions;
use crate::server::Server;
-async fn handle_signals(mut signals: Signals) -> Result<(), std::io::Error> {
+async fn handle_signals(mut signals: Signals) {
signals.next().await;
info!("Shutting down...");
- Ok(())
}
-async fn serve_404(response: Response) -> Result<Response, Error> {
+async fn serve_404(response: Response) -> Result<Response, tide::Error> {
match response.status() {
StatusCode::NotFound => Ok(Response::builder(404).body(Body::from_file("site/404.html").await?).content_type("text/html").build()),
_ => Ok(response),
}
#[async_std::main]
-async fn main() -> Result<(), Error> {
+async fn main() -> Result<(), tide::Error> {
env_logger::init();
+ let matches = app_from_crate!()
+ .arg(
+ Arg::with_name("config")
+ .short("c")
+ .long("config")
+ .value_name("FILE")
+ .takes_value(true)
+ .global(true)
+ .help("Path to configuration file in TOML format"),
+ )
+ .setting(AppSettings::VersionlessSubcommands)
+ .subcommand(SubCommand::with_name("server").about("Serve the website and websocket connections"))
+ .subcommand(
+ SubCommand::with_name("dealer").about("Start a dealer for each game").arg(
+ Arg::with_name("partition")
+ .value_name("N/M")
+ .default_value("1/1")
+ .validator(|partition| partition.parse::<Partition>().map(drop))
+ .help("Partition all games into M groups and run dealer N of M"),
+ ),
+ )
+ .subcommand(SubCommand::with_name("all").about("Serve the website, websocket connections and start a dealer for each game (default)"))
+ .get_matches();
+ let config = match matches.value_of_os("config") {
+ Some(path) => toml::from_str(&read_to_string(path)?)?,
+ None => Config::default(),
+ };
+
+ let mut run_server = true;
+ let mut run_dealer = true;
+ let mut partition = Partition::default();
+
+ match matches.subcommand() {
+ ("server", _) => run_dealer = false,
+ ("dealer", Some(args)) => {
+ run_server = false;
+ partition = args.value_of("partition").unwrap().parse().unwrap();
+ }
+ _ => {}
+ }
+
+ let signals = Signals::new(&[SIGINT])?;
+ let signal_handler = handle_signals(signals);
const REGISTER_UPDATE_STREAM_CHANNEL_BUFFER: usize = 128;
let (register_update_stream_tx, register_update_stream_rx) = channel(REGISTER_UPDATE_STREAM_CHANNEL_BUFFER);
- let client = Client::open("redis://localhost/")?;
+ let client = Client::open(config.redis.addr)?;
let connection = client.get_multiplexed_async_std_connection().await?;
let pubsub = client.get_async_std_connection().await?.into_pubsub();
- let mut app = tide::with_state(Server::new(connection, register_update_stream_tx));
-
- app.at("/").serve_dir("site/")?;
- app.at("/").serve_file("site/index.html")?;
- app.at("/api").get(WebSocket::new(new_client));
- app.with(After(serve_404));
-
- let signals = Signals::new(&[SIGINT])?;
- let signal_handler = handle_signals(signals);
-
+ let server = Server::new(connection, register_update_stream_tx);
let handle_client_interest = handle_client_interest_subscriptions(pubsub, register_update_stream_rx);
- let handle_new_games = spawn_dealers(app.state().clone());
-
- /*let listener = TlsListener::build()
- .addrs("localhost:4433")
- .cert("cert/cert.pem")
- .key("cert/key.pem");
-
- let app = app.listen(listener);*/
- let app = app.listen("0.0.0.0:8080");
- pin_mut!(app, handle_client_interest, handle_new_games, signal_handler);
- select(select(app, select(handle_client_interest, handle_new_games).map(|f| f.factor_first().0)).map(|f| f.factor_first().0), signal_handler)
- .await
- .factor_first()
- .0?;
+ let dealer: Pin<Box<dyn Future<Output = Result<(), std::io::Error>>>> =
+ if run_dealer { Box::pin(spawn_dealers(server.clone(), partition)) } else { Box::pin(pending()) };
+
+ let server: Pin<Box<dyn Future<Output = Result<(), std::io::Error>>>> = if run_server {
+ let mut app = tide::with_state(server);
+
+ app.at("/").serve_dir(&config.server.site)?;
+ app.at("/").serve_file(config.server.site.join("index.html"))?;
+ app.at("/api").get(WebSocket::new(new_client));
+ app.with(After(serve_404));
+
+ let mut listener = ConcurrentListener::new();
+ for addrs in config.server.bind {
+ if let (Some(cert), Some(key)) = (&config.server.cert, &config.server.key) {
+ listener.add(TlsListener::build().addrs(addrs).cert(&cert).key(&key))?;
+ } else {
+ listener.add(addrs.to_listener()?)?;
+ }
+ }
+
+ Box::pin(app.listen(listener))
+ } else {
+ Box::pin(pending())
+ };
+
+ select! {
+ _ = signal_handler.fuse() => {},
+ _ = handle_client_interest.fuse() => {},
+ server = server.fuse() => server?,
+ dealer = dealer.fuse() => dealer?,
+ };
info!("Pokerwave shut down gracefully.");
}
}
-pub async fn handle_client_interest_subscriptions(mut connection: PubSub, mut new_clients: Receiver<ClientInterestSender>) -> Result<(), std::io::Error> {
+pub async fn handle_client_interest_subscriptions(mut connection: PubSub, mut new_clients: Receiver<ClientInterestSender>) {
debug!("handle_client_interest: init");
let mut clients: Vec<Client> = Vec::new();
loop {
Action::NoNewClients => {
debug!("handle_client_interest: Action::NoNewClients - current clients: {:?}", clients);
if clients.is_empty() {
- return Ok(());
+ return;
}
}
Action::ConnectionClosed => {
debug!("handle_client_interest: Action::ConnectionClosed - current clients: {:?}", clients);
- return Ok(());
+ return;
}
}
}