//! The `apsis-web` service binary. //! //! The process binds an HTTP/1.1 listener and answers requests from three //! sources of content that are all embedded in the binary: //! //! - the concept documentation and the knowledge graph data, read from the //! `content/` directory at compile time; //! - the interface documentation of the `apsis-rs` orbit layer, extracted //! from its source doc comments and embedded as `content/api.json`; //! - the frontend build output under `frontend/dist/`, embedded by //! `build.rs`. //! //! Because every asset is embedded, the binary is self-contained: it serves //! the same content from a copy that has no source tree next to it. //! //! The `--host` and `--port` options bind the listener, and the language of //! the user interface is selected in the browser and defaults to Chinese. mod api; mod docs; mod embedded; mod graph; mod http; mod i18n; mod json; mod state; mod tools; use std::net::SocketAddr; use std::process::ExitCode; use std::sync::Arc; use crate::api::Router; use crate::http::{Server, ServerConfig}; use crate::state::AppState; /// Default bind host, which accepts connections from the local machine /// only. const DEFAULT_HOST: &str = "127.0.0.1"; /// Default bind port. const DEFAULT_PORT: u16 = 8080; /// Entry point. fn main() -> ExitCode { let options = match Options::from_environment() { Ok(options) => options, Err(message) => { eprintln!("apsis-web: {message}"); return ExitCode::FAILURE; } }; let state = Arc::new(AppState::new()); let router = Arc::new(Router::new(state)); let config = ServerConfig { workers: options.workers, ..ServerConfig::default() }; let server = match Server::bind_address(options.address, config) { Ok(server) => server, Err(error) => { eprintln!("apsis-web: cannot bind {}: {error}", options.address); return ExitCode::FAILURE; } }; let bound = server .local_address() .map(|address| address.to_string()) .unwrap_or_else(|_| options.address.to_string()); println!("apsis-web listening on http://{bound}"); if !embedded::is_populated() { println!("apsis-web: no frontend build is embedded; serving the placeholder page"); } match server.run(router) { Ok(()) => ExitCode::SUCCESS, Err(error) => { eprintln!("apsis-web: server stopped: {error}"); ExitCode::FAILURE } } } /// Command line options, read from arguments and then from the environment. struct Options { /// Address to bind. address: SocketAddr, /// Number of worker threads. workers: usize, } impl Options { /// Builds the options from the process arguments. /// /// Arguments override the environment variables `APSIS_WEB_HOST`, /// `APSIS_WEB_PORT`, `APSIS_WEB_ADDRESS`, and `APSIS_WEB_WORKERS`. /// `--help` and `--version` print and exit. /// /// The host and the port may be given separately with `--host` and /// `--port`, or together with `--address`. Giving both `--address` and /// either of the two separate options is an error, because the order in /// which they would apply is not defined. fn from_environment() -> Result { let mut host = std::env::var("APSIS_WEB_HOST").unwrap_or_else(|_| DEFAULT_HOST.to_string()); let mut port = match std::env::var("APSIS_WEB_PORT") { Ok(value) => parse_port(&value).map_err(|error| format!("APSIS_WEB_PORT: {error}"))?, Err(_) => DEFAULT_PORT, }; let mut combined: Option = match std::env::var("APSIS_WEB_ADDRESS") { Ok(value) => { Some(parse_address(&value).map_err(|error| format!("APSIS_WEB_ADDRESS: {error}"))?) } Err(_) => None, }; let mut workers = match std::env::var("APSIS_WEB_WORKERS") { Ok(value) => value .parse() .map_err(|_| "APSIS_WEB_WORKERS must be a positive integer".to_string())?, Err(_) => 4, }; if workers == 0 { return Err("APSIS_WEB_WORKERS must be greater than zero".to_string()); } let mut host_given = false; let mut port_given = false; let mut arguments = std::env::args().skip(1); while let Some(argument) = arguments.next() { match argument.as_str() { "--host" => { host = arguments .next() .ok_or_else(|| "--host needs a value".to_string())?; host_given = true; } "--port" | "-p" => { let value = arguments .next() .ok_or_else(|| "--port needs a value".to_string())?; port = parse_port(&value).map_err(|error| format!("--port: {error}"))?; port_given = true; } "--address" | "-a" => { let value = arguments .next() .ok_or_else(|| "--address needs a value".to_string())?; combined = Some(parse_address(&value).map_err(|error| format!("--address: {error}"))?); } "--workers" | "-w" => { let value = arguments .next() .ok_or_else(|| "--workers needs a value".to_string())?; workers = value .parse() .map_err(|_| "--workers needs a positive integer".to_string())?; } "--help" | "-h" => { println!("{}", USAGE); std::process::exit(0); } "--version" | "-V" => { println!("apsis-web {}", env!("CARGO_PKG_VERSION")); std::process::exit(0); } other => return Err(format!("unknown argument {other:?}; try --help")), } } if workers == 0 { return Err("--workers must be greater than zero".to_string()); } let address = if host_given || port_given { if combined.is_some() { return Err( "--address cannot be combined with --host or --port; use one form".to_string(), ); } resolve(&host, port).map_err(|error| format!("--host: {error}"))? } else if let Some(address) = combined { address } else { resolve(&host, port).map_err(|error| format!("APSIS_WEB_HOST: {error}"))? }; Ok(Self { address, workers }) } } /// Parses a port number. fn parse_port(value: &str) -> Result { value .trim() .parse::() .map_err(|_| format!("{value:?} is not a port number")) } /// Parses an address written as `host:port`. fn parse_address(value: &str) -> Result { value .trim() .parse::() .map_err(|_| format!("{value:?} is not a host and port")) } /// Resolves a host and a port into a socket address. /// /// A host may be a literal address, a bracketed or bare IPv6 literal, or a /// name that the operating system resolves. A name that resolves to several /// addresses uses the first one, which is the order the resolver reports and /// therefore the preferred one. fn resolve(host: &str, port: u16) -> Result { let host = host.trim(); if host.is_empty() { return Err("the host is empty".to_string()); } // A literal address, including a bracketed IPv6 literal, needs no name // resolution. let literal = host.trim_start_matches('[').trim_end_matches(']'); if let Ok(ip) = literal.parse::() { return Ok(SocketAddr::new(ip, port)); } // A name that already carries a port, such as `localhost:9000`, is // accepted so that a value written in the combined form still works. if let Ok(address) = host.parse::() { return Ok(address); } use std::net::ToSocketAddrs; match (host, port).to_socket_addrs() { Ok(mut addresses) => addresses .next() .ok_or_else(|| format!("{host:?} does not resolve to an address")), Err(error) => Err(format!("{host:?} does not resolve: {error}")), } } /// Usage text. const USAGE: &str = "\ apsis-web: web service for the ApsisRS computational kernels USAGE: apsis-web [OPTIONS] OPTIONS: -a, --address address to bind (default 127.0.0.1:8080) --host interface to bind (default 127.0.0.1) -p, --port port to bind (default 8080) -w, --workers worker threads (default 4) -h, --help print this help -V, --version print the version ENVIRONMENT: APSIS_WEB_ADDRESS bind address, as HOST:PORT APSIS_WEB_HOST interface to bind APSIS_WEB_PORT port to bind APSIS_WEB_WORKERS worker threads EXAMPLES: apsis-web --host 0.0.0.0 --port 8080 apsis-web --address 127.0.0.1:9000";