web: bind with --host and --port
This commit is contained in:
parent
f1fd4ebddd
commit
9da643b23e
30
README.md
30
README.md
|
|
@ -56,7 +56,9 @@ Environment variables:
|
||||||
|
|
||||||
| Variable | Effect |
|
| Variable | Effect |
|
||||||
|----------|--------|
|
|----------|--------|
|
||||||
| `APSIS_WEB_ADDRESS` | Bind address, default `127.0.0.1:8080` |
|
| `APSIS_WEB_HOST` | Interface to bind, default `127.0.0.1` |
|
||||||
|
| `APSIS_WEB_PORT` | Port to bind, default `8080` |
|
||||||
|
| `APSIS_WEB_ADDRESS` | Bind address as `HOST:PORT`, overriding the two above |
|
||||||
| `APSIS_WEB_WORKERS` | Worker threads, default 4 |
|
| `APSIS_WEB_WORKERS` | Worker threads, default 4 |
|
||||||
| `APSIS_WEB_SKIP_FRONTEND` | Set to `1` to skip the frontend build in a release build |
|
| `APSIS_WEB_SKIP_FRONTEND` | Set to `1` to skip the frontend build in a release build |
|
||||||
| `APSIS_WEB_FRONTEND_DIR` | Frontend directory, default `frontend` |
|
| `APSIS_WEB_FRONTEND_DIR` | Frontend directory, default `frontend` |
|
||||||
|
|
@ -64,9 +66,30 @@ Environment variables:
|
||||||
Command line options override the environment:
|
Command line options override the environment:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
apsis-web --address 0.0.0.0:8080 --workers 8
|
# Bind every interface on port 8080.
|
||||||
|
apsis-web --host 0.0.0.0 --port 8080
|
||||||
|
|
||||||
|
# The combined form is equivalent.
|
||||||
|
apsis-web --address 0.0.0.0:8080
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`--host` and `--port` may be given together. `--address` cannot be combined
|
||||||
|
with either of them, because the order in which they would apply is not
|
||||||
|
defined. A host may be a literal address, an IPv6 literal in brackets, or a
|
||||||
|
name the operating system resolves.
|
||||||
|
|
||||||
|
### Language
|
||||||
|
|
||||||
|
The interface is bilingual and opens in Chinese. The reader switches to
|
||||||
|
English with the control in the top bar; the choice is stored in the browser
|
||||||
|
and applies to every page.
|
||||||
|
|
||||||
|
The backend carries every user facing string in both languages, so the switch
|
||||||
|
takes effect without a request. A concept document is written once in English
|
||||||
|
as `content/concepts/<id>.md` and translated as
|
||||||
|
`content/concepts/<id>.zh.md`. A document without a translation is shown in
|
||||||
|
English to every reader, with a note saying so.
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|
@ -109,8 +132,9 @@ or later. The Rust build does not run it and does not depend on Python.
|
||||||
|
|
||||||
| Path | Contents |
|
| Path | Contents |
|
||||||
|------|----------|
|
|------|----------|
|
||||||
| `content/concepts/chapters.json` | Chapter list and the reading order of documents |
|
| `content/concepts/chapters.json` | Chapter list and the reading order of documents, with bilingual titles |
|
||||||
| `content/concepts/*.md` | Concept documents, with a `key: value` metadata header |
|
| `content/concepts/*.md` | Concept documents, with a `key: value` metadata header |
|
||||||
|
| `content/concepts/*.zh.md` | Chinese translations; the identifier comes from the file name |
|
||||||
| `content/graph/concepts.json` | The concept graph |
|
| `content/graph/concepts.json` | The concept graph |
|
||||||
| `content/flows/*.json` | The algorithm flow graphs |
|
| `content/flows/*.json` | The algorithm flow graphs |
|
||||||
| `content/api.json` | The generated interface catalog; do not edit by hand |
|
| `content/api.json` | The generated interface catalog; do not edit by hand |
|
||||||
|
|
|
||||||
|
|
@ -60,11 +60,12 @@ pub struct Server {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Server {
|
impl Server {
|
||||||
/// Binds a server to `address`.
|
/// Binds a server to a socket address.
|
||||||
///
|
///
|
||||||
/// The address accepts anything that resolves to a socket address, such
|
/// This is the form the command line uses once the host and the port
|
||||||
/// as `"127.0.0.1:8080"` or `"0.0.0.0:80"`.
|
/// have been resolved, so that a name is resolved once rather than left
|
||||||
pub fn bind(address: &str, config: ServerConfig) -> io::Result<Self> {
|
/// to the listener.
|
||||||
|
pub fn bind_address(address: SocketAddr, config: ServerConfig) -> io::Result<Self> {
|
||||||
let listener = TcpListener::bind(address)?;
|
let listener = TcpListener::bind(address)?;
|
||||||
Ok(Self { listener, config })
|
Ok(Self { listener, config })
|
||||||
}
|
}
|
||||||
|
|
@ -198,8 +199,10 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn serves_a_request_over_a_real_socket() {
|
fn serves_a_request_over_a_real_socket() {
|
||||||
let server = Server::bind(
|
let server = Server::bind_address(
|
||||||
"127.0.0.1:0",
|
"127.0.0.1:0"
|
||||||
|
.parse()
|
||||||
|
.expect("the literal address must parse"),
|
||||||
ServerConfig {
|
ServerConfig {
|
||||||
workers: 2,
|
workers: 2,
|
||||||
..ServerConfig::default()
|
..ServerConfig::default()
|
||||||
|
|
|
||||||
161
src/main.rs
161
src/main.rs
|
|
@ -12,16 +12,21 @@
|
||||||
//!
|
//!
|
||||||
//! Because every asset is embedded, the binary is self-contained: it serves
|
//! 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 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 api;
|
||||||
mod docs;
|
mod docs;
|
||||||
mod embedded;
|
mod embedded;
|
||||||
mod graph;
|
mod graph;
|
||||||
mod http;
|
mod http;
|
||||||
|
mod i18n;
|
||||||
mod json;
|
mod json;
|
||||||
mod state;
|
mod state;
|
||||||
mod tools;
|
mod tools;
|
||||||
|
|
||||||
|
use std::net::SocketAddr;
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
|
@ -29,8 +34,12 @@ use crate::api::Router;
|
||||||
use crate::http::{Server, ServerConfig};
|
use crate::http::{Server, ServerConfig};
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
/// Default bind address.
|
/// Default bind host, which accepts connections from the local machine
|
||||||
const DEFAULT_ADDRESS: &str = "127.0.0.1:8080";
|
/// only.
|
||||||
|
const DEFAULT_HOST: &str = "127.0.0.1";
|
||||||
|
|
||||||
|
/// Default bind port.
|
||||||
|
const DEFAULT_PORT: u16 = 8080;
|
||||||
|
|
||||||
/// Entry point.
|
/// Entry point.
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
|
|
@ -49,7 +58,7 @@ fn main() -> ExitCode {
|
||||||
..ServerConfig::default()
|
..ServerConfig::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let server = match Server::bind(&options.address, config) {
|
let server = match Server::bind_address(options.address, config) {
|
||||||
Ok(server) => server,
|
Ok(server) => server,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
eprintln!("apsis-web: cannot bind {}: {error}", options.address);
|
eprintln!("apsis-web: cannot bind {}: {error}", options.address);
|
||||||
|
|
@ -60,7 +69,7 @@ fn main() -> ExitCode {
|
||||||
let bound = server
|
let bound = server
|
||||||
.local_address()
|
.local_address()
|
||||||
.map(|address| address.to_string())
|
.map(|address| address.to_string())
|
||||||
.unwrap_or_else(|_| options.address.clone());
|
.unwrap_or_else(|_| options.address.to_string());
|
||||||
println!("apsis-web listening on http://{bound}");
|
println!("apsis-web listening on http://{bound}");
|
||||||
if !embedded::is_populated() {
|
if !embedded::is_populated() {
|
||||||
println!("apsis-web: no frontend build is embedded; serving the placeholder page");
|
println!("apsis-web: no frontend build is embedded; serving the placeholder page");
|
||||||
|
|
@ -77,8 +86,8 @@ fn main() -> ExitCode {
|
||||||
|
|
||||||
/// Command line options, read from arguments and then from the environment.
|
/// Command line options, read from arguments and then from the environment.
|
||||||
struct Options {
|
struct Options {
|
||||||
/// Address to bind, in `host:port` form.
|
/// Address to bind.
|
||||||
address: String,
|
address: SocketAddr,
|
||||||
/// Number of worker threads.
|
/// Number of worker threads.
|
||||||
workers: usize,
|
workers: usize,
|
||||||
}
|
}
|
||||||
|
|
@ -86,36 +95,67 @@ struct Options {
|
||||||
impl Options {
|
impl Options {
|
||||||
/// Builds the options from the process arguments.
|
/// Builds the options from the process arguments.
|
||||||
///
|
///
|
||||||
/// Arguments override the environment variables `APSIS_WEB_ADDRESS` and
|
/// Arguments override the environment variables `APSIS_WEB_HOST`,
|
||||||
/// `APSIS_WEB_WORKERS`. `--help` and `--version` print and exit.
|
/// `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<Self, String> {
|
fn from_environment() -> Result<Self, String> {
|
||||||
let mut options = Self {
|
let mut host = std::env::var("APSIS_WEB_HOST").unwrap_or_else(|_| DEFAULT_HOST.to_string());
|
||||||
address: std::env::var("APSIS_WEB_ADDRESS")
|
let mut port = match std::env::var("APSIS_WEB_PORT") {
|
||||||
.unwrap_or_else(|_| DEFAULT_ADDRESS.to_string()),
|
Ok(value) => parse_port(&value).map_err(|error| format!("APSIS_WEB_PORT: {error}"))?,
|
||||||
workers: match std::env::var("APSIS_WEB_WORKERS") {
|
Err(_) => DEFAULT_PORT,
|
||||||
Ok(value) => value
|
|
||||||
.parse()
|
|
||||||
.map_err(|_| "APSIS_WEB_WORKERS must be a positive integer".to_string())?,
|
|
||||||
Err(_) => 4,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
if options.workers == 0 {
|
let mut combined: Option<SocketAddr> = 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());
|
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);
|
let mut arguments = std::env::args().skip(1);
|
||||||
while let Some(argument) = arguments.next() {
|
while let Some(argument) = arguments.next() {
|
||||||
match argument.as_str() {
|
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" => {
|
"--address" | "-a" => {
|
||||||
options.address = arguments
|
let value = arguments
|
||||||
.next()
|
.next()
|
||||||
.ok_or_else(|| "--address needs a value".to_string())?;
|
.ok_or_else(|| "--address needs a value".to_string())?;
|
||||||
|
combined =
|
||||||
|
Some(parse_address(&value).map_err(|error| format!("--address: {error}"))?);
|
||||||
}
|
}
|
||||||
"--workers" | "-w" => {
|
"--workers" | "-w" => {
|
||||||
let value = arguments
|
let value = arguments
|
||||||
.next()
|
.next()
|
||||||
.ok_or_else(|| "--workers needs a value".to_string())?;
|
.ok_or_else(|| "--workers needs a value".to_string())?;
|
||||||
options.workers = value
|
workers = value
|
||||||
.parse()
|
.parse()
|
||||||
.map_err(|_| "--workers needs a positive integer".to_string())?;
|
.map_err(|_| "--workers needs a positive integer".to_string())?;
|
||||||
}
|
}
|
||||||
|
|
@ -130,10 +170,75 @@ impl Options {
|
||||||
other => return Err(format!("unknown argument {other:?}; try --help")),
|
other => return Err(format!("unknown argument {other:?}; try --help")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if options.workers == 0 {
|
|
||||||
|
if workers == 0 {
|
||||||
return Err("--workers must be greater than zero".to_string());
|
return Err("--workers must be greater than zero".to_string());
|
||||||
}
|
}
|
||||||
Ok(options)
|
|
||||||
|
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<u16, String> {
|
||||||
|
value
|
||||||
|
.trim()
|
||||||
|
.parse::<u16>()
|
||||||
|
.map_err(|_| format!("{value:?} is not a port number"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses an address written as `host:port`.
|
||||||
|
fn parse_address(value: &str) -> Result<SocketAddr, String> {
|
||||||
|
value
|
||||||
|
.trim()
|
||||||
|
.parse::<SocketAddr>()
|
||||||
|
.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<SocketAddr, String> {
|
||||||
|
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::<std::net::IpAddr>() {
|
||||||
|
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::<SocketAddr>() {
|
||||||
|
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}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,10 +251,18 @@ USAGE:
|
||||||
|
|
||||||
OPTIONS:
|
OPTIONS:
|
||||||
-a, --address <HOST:PORT> address to bind (default 127.0.0.1:8080)
|
-a, --address <HOST:PORT> address to bind (default 127.0.0.1:8080)
|
||||||
|
--host <HOST> interface to bind (default 127.0.0.1)
|
||||||
|
-p, --port <PORT> port to bind (default 8080)
|
||||||
-w, --workers <N> worker threads (default 4)
|
-w, --workers <N> worker threads (default 4)
|
||||||
-h, --help print this help
|
-h, --help print this help
|
||||||
-V, --version print the version
|
-V, --version print the version
|
||||||
|
|
||||||
ENVIRONMENT:
|
ENVIRONMENT:
|
||||||
APSIS_WEB_ADDRESS bind address
|
APSIS_WEB_ADDRESS bind address, as HOST:PORT
|
||||||
APSIS_WEB_WORKERS worker threads";
|
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";
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue