98 lines
3.3 KiB
Rust
98 lines
3.3 KiB
Rust
//! Serve files from `/state/www` and expose a control endpoint at `/self/control`.
|
|
//! Start this process detached or as a service so it outlives its caller.
|
|
//! Clients read `/proc/<pid>/control`. A fresh core call cannot inspect this process's counters.
|
|
use sausage_plugin::{Ctx, Result, background, listen, pid, rpc, schema, state_dir};
|
|
use std::{cell::Cell, rc::Rc};
|
|
use wstd::{
|
|
io::{AsyncRead, AsyncWrite},
|
|
iter::AsyncIterator,
|
|
net::TcpListener,
|
|
};
|
|
/// The HTTP plugin starts a server and returns its process ID.
|
|
struct Www;
|
|
/// The server configuration.
|
|
#[schema]
|
|
struct StartParams {
|
|
/// The HTTP port on all network interfaces.
|
|
port: u16,
|
|
}
|
|
#[rpc(endpoints(control=Control),grace_seconds=5)]
|
|
impl Www {
|
|
/// Start the HTTP server and return its process ID.
|
|
async fn start(&self, p: StartParams) -> Result<String> {
|
|
let tcp = TcpListener::bind(&format!("0.0.0.0:{}", p.port)).await?;
|
|
let control = Rc::new(Control { hits: Cell::new(0) });
|
|
listen("/self/control", control.clone()).await?;
|
|
background(serve(tcp, control));
|
|
Ok(pid())
|
|
}
|
|
}
|
|
/// Shared process-local state for the HTTP task and its endpoint.
|
|
struct Control {
|
|
/// The number of answered requests.
|
|
hits: Cell<u64>,
|
|
}
|
|
#[rpc]
|
|
impl Control {
|
|
/// Count the HTTP requests this server answered.
|
|
fn stats(&self) -> Result<u64> {
|
|
Ok(self.hits.get())
|
|
}
|
|
/// End this process after the response reaches the client.
|
|
fn stop(&self, ctx: &Ctx) -> Result<()> {
|
|
ctx.exit_after_response();
|
|
Ok(())
|
|
}
|
|
}
|
|
/// Answer HTTP requests with files and count all responses, including missing files.
|
|
async fn serve(tcp: TcpListener, control: Rc<Control>) {
|
|
let mut incoming = tcp.incoming();
|
|
while let Some(Ok(mut stream)) = incoming.next().await {
|
|
let mut request = [0; 8192];
|
|
let Ok(size) = stream.read(&mut request).await else {
|
|
continue;
|
|
};
|
|
let line = String::from_utf8_lossy(&request[..size]);
|
|
let path = line
|
|
.split_whitespace()
|
|
.nth(1)
|
|
.unwrap_or("/")
|
|
.trim_start_matches('/');
|
|
let (status, body) = match std::fs::read(state_dir().join(if path.is_empty() {
|
|
"index.html"
|
|
} else {
|
|
path
|
|
})) {
|
|
Ok(body) => ("200 OK", body),
|
|
Err(_) => ("404 Not Found", b"Not found".to_vec()),
|
|
};
|
|
control.hits.set(control.hits.get() + 1);
|
|
let header = format!(
|
|
"HTTP/1.1 {status}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
|
body.len()
|
|
);
|
|
let _ = stream.write_all(header.as_bytes()).await;
|
|
let _ = stream.write_all(&body).await;
|
|
let _ = stream.flush().await;
|
|
}
|
|
}
|
|
/// Start the SDK transport for the HTTP plugin.
|
|
fn main() {
|
|
sausage_plugin::run(Www)
|
|
}
|
|
/// Tests check local counters and the service entry point.
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
/// Control reports its shared counter without a core round trip.
|
|
#[test]
|
|
fn stats_reads_counter() {
|
|
assert_eq!(Control { hits: Cell::new(3) }.stats().unwrap(), 3);
|
|
}
|
|
/// The listener binds an ephemeral port and publishes its control endpoint.
|
|
#[sausage_plugin::test]
|
|
async fn starts_on_available_port() {
|
|
assert_eq!(Www.start(StartParams { port: 0 }).await.unwrap(), pid());
|
|
}
|
|
}
|