use std::io::{self, BufRead, BufReader, Write};
use std::net::{Ipv4Addr, TcpListener, TcpStream};
use std::sync::Arc;
use std::thread;
use miette::{IntoDiagnostic, WrapErr};
use super::live::{LiveDiagram, LiveUpdate};
use crate::failure::Failure;
const VIEWER_PAGE: &str = include_str!("page.html");
pub struct ViewServer {
listener: TcpListener,
url: String,
live_diagram: Arc<LiveDiagram>,
}
enum Route {
ViewerPage,
DiagramSvg,
Events,
NotFound,
}
impl ViewServer {
pub fn bind(port: u16, live_diagram: Arc<LiveDiagram>) -> Result<Self, Failure> {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, port))
.into_diagnostic()
.wrap_err_with(|| format!("cannot listen on 127.0.0.1:{port}"))
.map_err(Failure::Io)?;
let local_address = listener
.local_addr()
.into_diagnostic()
.wrap_err_with(|| "cannot read the address the viewer listens on")
.map_err(Failure::Io)?;
let url = format!("http://{local_address}");
Ok(Self {
listener,
url,
live_diagram,
})
}
pub fn serve_in_background(self) {
thread::spawn(move || {
for accepted in self.listener.incoming() {
let Ok(stream) = accepted else {
continue;
};
let live_diagram = Arc::clone(&self.live_diagram);
thread::spawn(move || Self::answer(stream, &live_diagram));
}
});
}
fn answer(mut stream: TcpStream, live_diagram: &LiveDiagram) -> io::Result<()> {
let request_line = Self::read_request_line(&stream)?;
let route = Route::of(&request_line);
match route {
Route::ViewerPage => Self::write_document(
&mut stream,
"200 OK",
"text/html; charset=utf-8",
VIEWER_PAGE,
),
Route::DiagramSvg => {
let svg = live_diagram.svg();
Self::write_document(&mut stream, "200 OK", "image/svg+xml", &svg)
}
Route::Events => Self::stream_events(&mut stream, live_diagram),
Route::NotFound => Self::write_document(
&mut stream,
"404 Not Found",
"text/plain; charset=utf-8",
"not found\n",
),
}
}
fn read_request_line(stream: &TcpStream) -> io::Result<String> {
let mut reader = BufReader::new(stream);
let mut request_line = String::new();
reader.read_line(&mut request_line)?;
let header_lines = reader.lines().map_while(Result::ok);
let _header_count = header_lines
.take_while(|header_line| !header_line.is_empty())
.count();
Ok(request_line)
}
fn write_document(
stream: &mut TcpStream,
status: &str,
content_type: &str,
body: &str,
) -> io::Result<()> {
let body_length = body.len();
let head = format!(
"HTTP/1.1 {status}\r\nContent-Type: {content_type}\r\nContent-Length: {body_length}\r\nCache-Control: no-store\r\nConnection: close\r\n\r\n"
);
stream.write_all(head.as_bytes())?;
stream.write_all(body.as_bytes())
}
fn stream_events(stream: &mut TcpStream, live_diagram: &LiveDiagram) -> io::Result<()> {
let head = "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nCache-Control: no-store\r\nConnection: close\r\n\r\n";
stream.write_all(head.as_bytes())?;
let mut subscription = live_diagram.subscribe();
loop {
let update = subscription.next_update();
let events = Self::server_sent_events_of(update);
stream.write_all(events.as_bytes())?;
}
}
fn server_sent_events_of(update: LiveUpdate) -> String {
let LiveUpdate {
reloaded_generation,
diagnostics,
} = update;
let mut events = String::new();
if let Some(generation) = reloaded_generation {
let reload_event = format!("event: reload\ndata: {generation}\n\n");
events.push_str(&reload_event);
}
if let Some(diagnostics) = diagnostics {
events.push_str("event: diagnostics\n");
for report_line in diagnostics.lines() {
let data_line = format!("data: {report_line}\n");
events.push_str(&data_line);
}
events.push('\n');
}
events
}
pub fn url(&self) -> &str {
&self.url
}
}
impl Route {
fn of(request_line: &str) -> Self {
let mut request_words = request_line.split_whitespace();
let method = request_words.next();
let target = request_words.next().unwrap_or_default();
let (path, _query) = target.split_once('?').unwrap_or((target, ""));
match (method, path) {
(Some("GET"), "/") => Self::ViewerPage,
(Some("GET"), "/diagram.svg") => Self::DiagramSvg,
(Some("GET"), "/events") => Self::Events,
_unknown_route => Self::NotFound,
}
}
}