Skip to content

Commit

Permalink
feat: Use clap for CLI argument parsing
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
theduke committed Nov 9, 2023
1 parent c645a29 commit ba125d8
Show file tree
Hide file tree
Showing 4 changed files with 239 additions and 63 deletions.
113 changes: 113 additions & 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 @@ -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" }
Expand Down
116 changes: 93 additions & 23 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<u16>,

/// The interface to listen on.
/// Defaults to 127.0.0.1
#[clap(long, default_value = "127.0.0.1", env = "WINTERJS_INTERFACE")]
interface: Option<IpAddr>,

/// 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,
}
72 changes: 32 additions & 40 deletions src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<H: RequestHandler + Send + Sync>(
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(
Expand Down Expand Up @@ -59,43 +91,3 @@ async fn handle_inner(
.await
.context("JavaScript failed")
}

pub async fn run_server<H: RequestHandler + Send + Sync>(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")
}

0 comments on commit ba125d8

Please sign in to comment.