From ba125d8a9783934793e4f5289571c871429dd7b2 Mon Sep 17 00:00:00 2001 From: Christoph Herzog Date: Thu, 9 Nov 2023 09:53:13 +0100 Subject: [PATCH] feat: Use clap for CLI argument parsing Switches arg parsing to the clap crate. Also adds a "serve" subcommand, in preparation for adding additional commands. For backwards compatibility, not specifying a subcommand still works. --- Cargo.lock | 113 ++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/main.rs | 116 ++++++++++++++++++++++++++++++++++++++++---------- src/server.rs | 72 ++++++++++++++----------------- 4 files changed, 239 insertions(+), 63 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b0cd53445..c00bbbe43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,6 +36,54 @@ dependencies = [ "libc 0.2.149", ] +[[package]] +name = "anstream" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", +] + [[package]] name = "anyhow" version = "1.0.75" @@ -231,12 +279,58 @@ dependencies = [ "libloading", ] +[[package]] +name = "clap" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac495e00dcec98c83465d5ad66c5c4fabd652fd6686e7c6269b117e729a6f17b" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77ed9a32a62e6ca27175d00d29d05ca32e396ea1eb5fb01d8256b669cec7663" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + [[package]] name = "closure" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6173fd61b610d15a7566dd7b7620775627441c4ab9dac8906e17cb93a24b782" +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "colored" version = "2.0.4" @@ -648,6 +742,12 @@ version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -1958,6 +2058,12 @@ dependencies = [ "syn 2.0.38", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "swc_atoms" version = "0.5.9" @@ -2636,6 +2742,12 @@ dependencies = [ "byteorder", ] +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "uuid" version = "1.5.0" @@ -2774,6 +2886,7 @@ dependencies = [ "async-trait", "base64 0.21.5", "bytes", + "clap", "form_urlencoded", "futures", "h2", diff --git a/Cargo.toml b/Cargo.toml index 45f118765..28cb9fe12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,7 @@ rand = "0.8.5" sha1 = "0.10.6" sha2 = "0.10.8" md5 = "0.7.0" +clap = { version = "4.4.7", features = ["derive", "env"] } [patch.crates-io] tokio = { git = "https://github.com/wasix-org/tokio.git", branch = "epoll" } diff --git a/src/main.rs b/src/main.rs index d645661ad..6ff6596af 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,8 +2,13 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ -use anyhow::{bail, Context}; -use runtime::config::Config; +use std::{ + net::{IpAddr, SocketAddr}, + path::PathBuf, +}; + +use anyhow::Context as _; +use clap::Parser; #[macro_use] extern crate ion_proc; @@ -39,35 +44,100 @@ async fn main() { } async fn run() -> Result<(), anyhow::Error> { + // Initialize logging. if std::env::var("RUST_LOG").is_err() { + // Set default log level. std::env::set_var("RUST_LOG", "wasmer_winter=info,warn"); } - tracing_subscriber::fmt::init(); - tracing::info!("starting webserver"); + let args = match Args::try_parse() { + Ok(a) => a, + Err(err1) => { + // Fall back to parsing the serve command for backwards compatibility. - let code = if let Ok(v) = std::env::var("JS_CODE") { - v - } else { - let mut args = std::env::args().skip(1); + match CmdServe::try_parse_from(&mut std::env::args_os()) { + Ok(a) => Args { cmd: Cmd::Serve(a) }, + Err(_) => { + // Neither the main args nor the serve command args could be parsed. + // Report the original error for full help. + err1.exit(); + } + } + } + }; - let path = if let Some(p) = args.next() { - p - } else if let Ok(v) = std::env::var("JS_PATH") { - v - } else { - bail!("No path to JS file provided: either pass it as the first argument or set the JS_PATH environment variable"); - }; + match args.cmd { + Cmd::Serve(cmd) => { + let code = std::fs::read_to_string(&cmd.js_path).with_context(|| { + format!( + "Could not read Javascript file at '{}'", + cmd.js_path.display() + ) + })?; - std::fs::read_to_string(&path) - .with_context(|| format!("Could not read js file at '{}'", path))? - }; + // let listen = cmd.liste - runtime::config::CONFIG - .set(Config::default().log_level(runtime::config::LogLevel::Error)) - .unwrap(); - // TODO: make max_threads configurable - crate::server::run_server(ion_runner::IonRunner::new_request_handler(16, code)).await + let interface = if let Some(iface) = cmd.interface { + iface + } else if let Ok(value) = std::env::var("LISTEN_IP") { + value + .parse() + .context(format!("Invalid interface in LISTEN_IP: '{value}'"))? + } else { + std::net::Ipv4Addr::UNSPECIFIED.into() + }; + + let port = if let Some(port) = cmd.port { + port + } else if let Ok(value) = std::env::var("PORT") { + value + .parse() + .context(format!("Invalid port in PORT: '{value}'"))? + } else { + 8080 + }; + + let addr: SocketAddr = (interface, port).into(); + let config = crate::server::ServerConfig { addr }; + + runtime::config::CONFIG + .set(runtime::config::Config::default().log_level(runtime::config::LogLevel::Error)) + .unwrap(); + let runner = ion_runner::WatchRunner::new(cmd.js_path.clone(), cmd.max_js_threads); + crate::server::run_server(config, runner).await +/// winterjs CLI +#[derive(clap::Parser, Debug)] +#[clap(version)] +struct Args { + #[clap(subcommand)] + cmd: Cmd, +} + +/// Available commands. +#[derive(clap::Subcommand, Debug)] +enum Cmd { + Serve(CmdServe), +} + +/// Start a WinterJS webserver serving the given JS app. +#[derive(clap::Parser, Debug)] +struct CmdServe { + /// The port to listen on. + #[clap(short, long, env = "WINTERJS_PORT")] + port: Option, + + /// The interface to listen on. + /// Defaults to 127.0.0.1 + #[clap(long, default_value = "127.0.0.1", env = "WINTERJS_INTERFACE")] + interface: Option, + + /// Maximum amount of Javascript worker threads to spawn. + #[clap(long, default_value = "16", env = "WINTERJS_MAX_JS_THREADS")] + max_js_threads: usize, + + /// Path to a Javascript file to serve. + #[clap(env = "JS_PATH")] + js_path: PathBuf, } diff --git a/src/server.rs b/src/server.rs index 99b43e619..71b7240d3 100644 --- a/src/server.rs +++ b/src/server.rs @@ -8,6 +8,38 @@ use hyper::server::conn::AddrStream; use hyper::service::{make_service_fn, service_fn}; use hyper::{Body, Request, Response, Server}; +#[derive(Clone, Debug)] +pub struct ServerConfig { + pub addr: SocketAddr, +} + +pub async fn run_server( + config: ServerConfig, + handler: H, +) -> Result<(), anyhow::Error> { + let context = AppContext { handler }; + + let make_service = make_service_fn(move |conn: &AddrStream| { + let context = context.clone(); + + let addr = conn.remote_addr(); + + // Create a `Service` for responding to the request. + let service = service_fn(move |req| handle(context.clone(), addr, req)); + + // Return the service to hyper. + async move { Ok::<_, Infallible>(service) } + }); + + let addr = config.addr; + tracing::info!(listen=%addr, "starting server on '{addr}'"); + + Server::bind(&addr) + .serve(make_service) + .await + .context("hyper server failed") +} + #[async_trait] pub trait RequestHandler: Send + Clone + 'static { async fn handle( @@ -59,43 +91,3 @@ async fn handle_inner( .await .context("JavaScript failed") } - -pub async fn run_server(handler: H) -> Result<(), anyhow::Error> { - let context = AppContext { handler }; - - let make_service = make_service_fn(move |conn: &AddrStream| { - let context = context.clone(); - - let addr = conn.remote_addr(); - - // Create a `Service` for responding to the request. - let service = service_fn(move |req| handle(context.clone(), addr, req)); - - // Return the service to hyper. - async move { Ok::<_, Infallible>(service) } - }); - - let ip = std::env::var("LISTEN_IP") - .map(|x| { - x.parse() - .context(format!("Invalid listen IP value {x}")) - .unwrap() - }) - .unwrap_or(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED)); - - let port = std::env::var("PORT") - .map(|x| { - x.parse() - .context(format!("Invalid port value {x}")) - .unwrap() - }) - .unwrap_or(8080u16); - - let addr = SocketAddr::from((ip, port)); - tracing::info!(listen=%addr, "starting server on {addr}"); - - Server::bind(&addr) - .serve(make_service) - .await - .context("hyper server failed") -}