web: bind with --host and --port
This commit is contained in:
parent
25f9b28e9e
commit
0415a5fe6f
30
README.md
30
README.md
|
|
@ -56,7 +56,9 @@ Environment variables:
|
|||
|
||||
| Variable | Effect |
|
||||
|----------|--------|
|
||||
| `APSIS_BOOK_ADDRESS` | Bind address, default `127.0.0.1:8080` |
|
||||
| `APSIS_BOOK_HOST` | Interface to bind, default `127.0.0.1` |
|
||||
| `APSIS_BOOK_PORT` | Port to bind, default `8080` |
|
||||
| `APSIS_BOOK_ADDRESS` | Bind address as `HOST:PORT`, overriding the two above |
|
||||
| `APSIS_BOOK_WORKERS` | Worker threads, default 4 |
|
||||
| `APSIS_BOOK_SKIP_FRONTEND` | Set to `1` to skip the frontend build in a release build |
|
||||
| `APSIS_BOOK_FRONTEND_DIR` | Frontend directory, default `frontend` |
|
||||
|
|
@ -64,9 +66,30 @@ Environment variables:
|
|||
Command line options override the environment:
|
||||
|
||||
```sh
|
||||
apsis-book --address 0.0.0.0:8080 --workers 8
|
||||
# Bind every interface on port 8080.
|
||||
apsis-book --host 0.0.0.0 --port 8080
|
||||
|
||||
# The combined form is equivalent.
|
||||
apsis-book --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
|
||||
|
||||
```sh
|
||||
|
|
@ -109,8 +132,9 @@ or later. The Rust build does not run it and does not depend on Python.
|
|||
|
||||
| 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/*.zh.md` | Chinese translations; the identifier comes from the file name |
|
||||
| `content/graph/concepts.json` | The concept graph |
|
||||
| `content/flows/*.json` | The algorithm flow graphs |
|
||||
| `content/api.json` | The generated interface catalog; do not edit by hand |
|
||||
|
|
|
|||
|
|
@ -60,11 +60,12 @@ pub struct 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
|
||||
/// as `"127.0.0.1:8080"` or `"0.0.0.0:80"`.
|
||||
pub fn bind(address: &str, config: ServerConfig) -> io::Result<Self> {
|
||||
/// This is the form the command line uses once the host and the port
|
||||
/// have been resolved, so that a name is resolved once rather than left
|
||||
/// to the listener.
|
||||
pub fn bind_address(address: SocketAddr, config: ServerConfig) -> io::Result<Self> {
|
||||
let listener = TcpListener::bind(address)?;
|
||||
Ok(Self { listener, config })
|
||||
}
|
||||
|
|
@ -198,8 +199,10 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn serves_a_request_over_a_real_socket() {
|
||||
let server = Server::bind(
|
||||
"127.0.0.1:0",
|
||||
let server = Server::bind_address(
|
||||
"127.0.0.1:0"
|
||||
.parse()
|
||||
.expect("the literal address must parse"),
|
||||
ServerConfig {
|
||||
workers: 2,
|
||||
..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
|
||||
//! 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;
|
||||
|
||||
|
|
@ -29,8 +34,12 @@ use crate::api::Router;
|
|||
use crate::http::{Server, ServerConfig};
|
||||
use crate::state::AppState;
|
||||
|
||||
/// Default bind address.
|
||||
const DEFAULT_ADDRESS: &str = "127.0.0.1:8080";
|
||||
/// 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 {
|
||||
|
|
@ -49,7 +58,7 @@ fn main() -> ExitCode {
|
|||
..ServerConfig::default()
|
||||
};
|
||||
|
||||
let server = match Server::bind(&options.address, config) {
|
||||
let server = match Server::bind_address(options.address, config) {
|
||||
Ok(server) => server,
|
||||
Err(error) => {
|
||||
eprintln!("apsis-book: cannot bind {}: {error}", options.address);
|
||||
|
|
@ -60,7 +69,7 @@ fn main() -> ExitCode {
|
|||
let bound = server
|
||||
.local_address()
|
||||
.map(|address| address.to_string())
|
||||
.unwrap_or_else(|_| options.address.clone());
|
||||
.unwrap_or_else(|_| options.address.to_string());
|
||||
println!("apsis-book listening on http://{bound}");
|
||||
if !embedded::is_populated() {
|
||||
println!("apsis-book: 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.
|
||||
struct Options {
|
||||
/// Address to bind, in `host:port` form.
|
||||
address: String,
|
||||
/// Address to bind.
|
||||
address: SocketAddr,
|
||||
/// Number of worker threads.
|
||||
workers: usize,
|
||||
}
|
||||
|
|
@ -86,36 +95,67 @@ struct Options {
|
|||
impl Options {
|
||||
/// Builds the options from the process arguments.
|
||||
///
|
||||
/// Arguments override the environment variables `APSIS_BOOK_ADDRESS` and
|
||||
/// `APSIS_BOOK_WORKERS`. `--help` and `--version` print and exit.
|
||||
/// Arguments override the environment variables `APSIS_BOOK_HOST`,
|
||||
/// `APSIS_BOOK_PORT`, `APSIS_BOOK_ADDRESS`, and `APSIS_BOOK_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> {
|
||||
let mut options = Self {
|
||||
address: std::env::var("APSIS_BOOK_ADDRESS")
|
||||
.unwrap_or_else(|_| DEFAULT_ADDRESS.to_string()),
|
||||
workers: match std::env::var("APSIS_BOOK_WORKERS") {
|
||||
Ok(value) => value
|
||||
.parse()
|
||||
.map_err(|_| "APSIS_BOOK_WORKERS must be a positive integer".to_string())?,
|
||||
Err(_) => 4,
|
||||
},
|
||||
let mut host = std::env::var("APSIS_BOOK_HOST").unwrap_or_else(|_| DEFAULT_HOST.to_string());
|
||||
let mut port = match std::env::var("APSIS_BOOK_PORT") {
|
||||
Ok(value) => parse_port(&value).map_err(|error| format!("APSIS_BOOK_PORT: {error}"))?,
|
||||
Err(_) => DEFAULT_PORT,
|
||||
};
|
||||
if options.workers == 0 {
|
||||
let mut combined: Option<SocketAddr> = match std::env::var("APSIS_BOOK_ADDRESS") {
|
||||
Ok(value) => {
|
||||
Some(parse_address(&value).map_err(|error| format!("APSIS_BOOK_ADDRESS: {error}"))?)
|
||||
}
|
||||
Err(_) => None,
|
||||
};
|
||||
let mut workers = match std::env::var("APSIS_BOOK_WORKERS") {
|
||||
Ok(value) => value
|
||||
.parse()
|
||||
.map_err(|_| "APSIS_BOOK_WORKERS must be a positive integer".to_string())?,
|
||||
Err(_) => 4,
|
||||
};
|
||||
if workers == 0 {
|
||||
return Err("APSIS_BOOK_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" => {
|
||||
options.address = arguments
|
||||
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())?;
|
||||
options.workers = value
|
||||
workers = value
|
||||
.parse()
|
||||
.map_err(|_| "--workers needs a positive integer".to_string())?;
|
||||
}
|
||||
|
|
@ -130,10 +170,75 @@ impl Options {
|
|||
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());
|
||||
}
|
||||
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_BOOK_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:
|
||||
-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)
|
||||
-h, --help print this help
|
||||
-V, --version print the version
|
||||
|
||||
ENVIRONMENT:
|
||||
APSIS_BOOK_ADDRESS bind address
|
||||
APSIS_BOOK_WORKERS worker threads";
|
||||
APSIS_BOOK_ADDRESS bind address, as HOST:PORT
|
||||
APSIS_BOOK_HOST interface to bind
|
||||
APSIS_BOOK_PORT port to bind
|
||||
APSIS_BOOK_WORKERS worker threads
|
||||
|
||||
EXAMPLES:
|
||||
apsis-book --host 0.0.0.0 --port 8080
|
||||
apsis-book --address 127.0.0.1:9000";
|
||||
|
|
|
|||
Loading…
Reference in New Issue