#![cfg(feature = "web")]
use std::{
net::{TcpListener, TcpStream},
sync::{Arc, Mutex},
thread,
};
use once_cell::sync::Lazy;
use tiny_http::{Response, Server};
use tungstenite::{accept, Message, WebSocket};
use crate::state::{DiffEvent, INSPECTOR_STATE, Snapshot};
type WsClients = Arc<Mutex<Vec<WebSocket<TcpStream>>>>;
static WS_CLIENTS: Lazy<WsClients> = Lazy::new(|| Arc::new(Mutex::new(Vec::new())));
pub static INSPECTOR_SERVER: Lazy<InspectorServer> = Lazy::new(InspectorServer::start);
pub fn broadcast_snapshot(snap: &Snapshot) {
let json = serde_json::to_string(&WsEvent::Snapshot { data: snap }).unwrap();
broadcast(&json);
}
pub fn broadcast_diff(diff: &DiffEvent) {
let json = serde_json::to_string(&WsEvent::Diff { data: diff }).unwrap();
broadcast(&json);
}
fn broadcast(json: &str) {
let msg = Message::Text(json.to_owned().into());
let mut clients = WS_CLIENTS.lock().unwrap();
clients.retain_mut(|ws| ws.send(msg.clone()).is_ok());
}
pub struct InspectorServer;
impl InspectorServer {
fn start() -> Self {
let http_port = std::env::var("LUPA_PORT")
.ok()
.and_then(|p| p.parse::<u16>().ok())
.unwrap_or(7777);
let ws_port = http_port + 1;
thread::Builder::new()
.name("lupa-http".into())
.spawn(move || {
let addr = format!("0.0.0.0:{http_port}");
let server = Server::http(&addr)
.unwrap_or_else(|e| panic!("lupa: HTTP bind failed on {addr}: {e}"));
for req in server.incoming_requests() {
let url = req.url().to_string();
let path = url.split('?').next().unwrap_or("/");
let resp = match path {
"/" | "/index.html" => Response::from_string(HTML_UI)
.with_header(hdr("Content-Type: text/html; charset=utf-8")),
"/api/snapshots" => {
let snaps = INSPECTOR_STATE.snapshots();
let json = serde_json::to_string(&snaps).unwrap_or_default();
Response::from_string(json)
.with_header(hdr("Content-Type: application/json"))
}
"/api/diffs" => {
let diffs = INSPECTOR_STATE.diffs();
let json = serde_json::to_string(&diffs).unwrap_or_default();
Response::from_string(json)
.with_header(hdr("Content-Type: application/json"))
}
_ => Response::from_string("404 Not Found").with_status_code(404),
};
let _ = req.respond(resp);
}
})
.expect("lupa: failed to spawn HTTP thread");
let clients_ws = WS_CLIENTS.clone();
thread::Builder::new()
.name("lupa-ws".into())
.spawn(move || {
let addr = format!("0.0.0.0:{ws_port}");
let listener = TcpListener::bind(&addr)
.unwrap_or_else(|e| panic!("lupa: WS bind failed on {addr}: {e}"));
for stream in listener.incoming().flatten() {
let clients_inner = clients_ws.clone();
thread::spawn(move || match accept(stream) {
Ok(ws) => clients_inner.lock().unwrap().push(ws),
Err(e) => eprintln!("lupa: WS handshake error: {e}"),
});
}
})
.expect("lupa: failed to spawn WS thread");
InspectorServer
}
}
#[derive(serde::Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
enum WsEvent<'a> {
Snapshot { data: &'a Snapshot },
Diff { data: &'a DiffEvent },
}
fn hdr(s: &str) -> tiny_http::Header {
s.parse().expect("static header is valid")
}
const HTML_UI: &str = include_str!("ui.html");