use std::io::{Read, Write};
use std::net::{SocketAddr, TcpListener, TcpStream};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{self, Receiver, Sender, SyncSender};
use std::sync::{Arc, Mutex};
use std::thread::{self, JoinHandle};
use serde_json::Value;
use super::record_store::{delivers, RecordStore};
const SSE_CLIENT_QUEUE: usize = 64;
const DEFAULT_HISTORY: usize = 512;
const MAX_HISTORY: usize = crate::monitor::record_store::MAX_RECORDS;
const DASHBOARD_HTML: &str = include_str!("dashboard.html");
pub(crate) enum ServerMsg {
Epoch(String),
SetSvg(String),
SetLabelHash(Option<String>, Option<String>),
SetHardware(String),
SetMetadata(String),
SetGpuInit(String),
Records(Vec<Value>),
Shutdown,
}
struct SseClient {
scope: Option<String>,
tx: SyncSender<String>,
}
pub(crate) struct DashboardServer {
tx: Sender<ServerMsg>,
addr: SocketAddr,
state: Arc<SharedState>,
accept_handle: Option<JoinHandle<()>>,
msg_handle: Option<JoinHandle<()>>,
}
struct SharedState {
epochs: Mutex<Vec<String>>,
svg: Mutex<Option<String>>,
sse_senders: Mutex<Vec<SseClient>>,
records: Arc<Mutex<RecordStore>>,
label: Mutex<Option<String>>,
hash: Mutex<Option<String>>,
hardware: Mutex<Option<String>>,
metadata: Mutex<Option<String>>,
gpu_init: Mutex<Option<String>>,
shutting_down: AtomicBool,
}
fn is_loopback_addr(a: &str) -> bool {
matches!(a, "127.0.0.1" | "::1" | "localhost")
}
pub(crate) fn resolve_dashboard_bind() -> String {
match std::env::var("FLODL_DASHBOARD_BIND") {
Ok(a) if !a.trim().is_empty() => {
let addr = a.trim().to_string();
if !is_loopback_addr(&addr) {
eprintln!(
"flodl: dashboard binding to {addr} — it has NO authentication, \
so anyone who can reach that address can view training metrics. \
Prefer an SSH tunnel: `ssh -L <port>:localhost:<port> <host>`."
);
}
addr
}
_ => "127.0.0.1".to_string(),
}
}
pub(crate) fn dashboard_bind_is_loopback() -> bool {
match std::env::var("FLODL_DASHBOARD_BIND") {
Ok(a) => a.trim().is_empty() || is_loopback_addr(a.trim()),
Err(_) => true,
}
}
impl DashboardServer {
#[cfg(test)]
pub fn start(port: u16) -> std::io::Result<Self> {
Self::start_with_records(port, Arc::new(Mutex::new(RecordStore::new())))
}
pub fn start_with_records(
port: u16,
records: Arc<Mutex<RecordStore>>,
) -> std::io::Result<Self> {
let bind = resolve_dashboard_bind();
let listener = TcpListener::bind((bind.as_str(), port))?;
let addr = listener.local_addr()?;
let (tx, rx) = mpsc::channel::<ServerMsg>();
let state = Arc::new(SharedState {
epochs: Mutex::new(Vec::new()),
svg: Mutex::new(None),
sse_senders: Mutex::new(Vec::new()),
label: Mutex::new(None),
hash: Mutex::new(None),
hardware: Mutex::new(None),
metadata: Mutex::new(None),
gpu_init: Mutex::new(None),
records,
shutting_down: AtomicBool::new(false),
});
let state2 = state.clone();
let msg_handle = thread::spawn(move || {
handle_messages(rx, state2);
});
let state3 = state.clone();
let accept_handle = thread::spawn(move || {
for stream in listener.incoming() {
if state3.shutting_down.load(Ordering::SeqCst) {
break;
}
let Ok(stream) = stream else { continue };
let state = state3.clone();
thread::spawn(move || {
handle_connection(stream, &state);
});
}
});
Ok(Self {
tx,
addr,
state,
accept_handle: Some(accept_handle),
msg_handle: Some(msg_handle),
})
}
pub fn push_epoch(&self, json: String) {
let _ = self.tx.send(ServerMsg::Epoch(json));
}
pub fn set_svg(&self, svg: String) {
let _ = self.tx.send(ServerMsg::SetSvg(svg));
}
pub fn set_label_hash(&self, label: Option<String>, hash: Option<String>) {
let _ = self.tx.send(ServerMsg::SetLabelHash(label, hash));
}
pub fn set_hardware(&self, hw: String) {
let _ = self.tx.send(ServerMsg::SetHardware(hw));
}
pub fn set_metadata(&self, json: String) {
let _ = self.tx.send(ServerMsg::SetMetadata(json));
}
pub fn set_gpu_init(&self, json: String) {
let _ = self.tx.send(ServerMsg::SetGpuInit(json));
}
pub fn push_records(&self, records: Vec<Value>) {
if records.is_empty() {
return;
}
let _ = self.tx.send(ServerMsg::Records(records));
}
pub fn shutdown(&mut self) {
self.state.shutting_down.store(true, Ordering::SeqCst);
let _ = self.tx.send(ServerMsg::Shutdown);
if let Some(h) = self.msg_handle.take() {
let _ = h.join();
}
if let Some(h) = self.accept_handle.take() {
let _ = TcpStream::connect(self.addr);
let _ = h.join();
}
}
}
impl Drop for DashboardServer {
fn drop(&mut self) {
self.shutdown();
}
}
fn handle_messages(rx: Receiver<ServerMsg>, state: Arc<SharedState>) {
for msg in rx {
match msg {
ServerMsg::Epoch(json) => {
let event = format!("event: epoch\ndata: {}\n\n", json);
state.epochs.lock().unwrap().push(json);
let mut senders = state.sse_senders.lock().unwrap();
senders.retain(|c| {
c.scope.is_some() || c.tx.try_send(event.clone()).is_ok()
});
}
ServerMsg::Records(records) => {
{
let mut store = state.records.lock().unwrap();
store.insert_all(&records);
if store.take_path_cap_hit() {
crate::msg!(
" warning: monitor record plane hit its {}-path cap; \
new paths are not indexed (a producer emitting \
unbounded paths?)",
crate::monitor::record_store::MAX_PATHS,
);
}
}
let mut senders = state.sse_senders.lock().unwrap();
senders.retain(|c| {
let Some(scope) = c.scope.as_deref() else {
return true; };
for rec in &records {
if !delivers(scope, rec) {
continue;
}
let event = format!("event: record\ndata: {rec}\n\n");
if c.tx.try_send(event).is_err() {
return false;
}
}
true
});
}
ServerMsg::SetSvg(svg) => {
*state.svg.lock().unwrap() = Some(svg);
}
ServerMsg::SetLabelHash(label, hash) => {
*state.label.lock().unwrap() = label;
*state.hash.lock().unwrap() = hash;
}
ServerMsg::SetHardware(hw) => {
*state.hardware.lock().unwrap() = Some(hw);
}
ServerMsg::SetMetadata(json) => {
*state.metadata.lock().unwrap() = Some(json);
}
ServerMsg::SetGpuInit(json) => {
*state.gpu_init.lock().unwrap() = Some(json);
}
ServerMsg::Shutdown => {
let event = "event: complete\ndata: {}\n\n".to_string();
let mut senders = state.sse_senders.lock().unwrap();
for c in senders.iter() {
let _ = c.tx.try_send(event.clone());
}
senders.clear();
break;
}
}
}
}
fn handle_connection(mut stream: TcpStream, state: &SharedState) {
let mut buf = [0u8; 2048];
let n = stream.read(&mut buf).unwrap_or(0);
if n == 0 {
return;
}
let request = String::from_utf8_lossy(&buf[..n]);
let target = parse_path(&request);
let (path, query) = target.split_once('?').unwrap_or((target, ""));
match path {
"/" => serve_html(&mut stream, state),
"/events" => serve_sse(stream, state, None),
"/graph.svg" => serve_svg(&mut stream, state),
"/api/history" => serve_history(&mut stream, state),
"/node" => serve_node(&mut stream, state, query),
"/history" => serve_record_history(&mut stream, state, query),
"/paths" => serve_paths(&mut stream, state),
"/stream" => {
let scope = query_param(query, "path").unwrap_or_else(|| "root".to_string());
serve_sse(stream, state, Some(scope));
}
_ => {
let _ = stream.write_all(b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n");
}
}
}
fn parse_path(request: &str) -> &str {
request
.lines()
.next()
.and_then(|line| line.split_whitespace().nth(1))
.unwrap_or("/")
}
fn query_param(query: &str, key: &str) -> Option<String> {
query
.split('&')
.filter_map(|kv| kv.split_once('='))
.find(|(k, _)| *k == key)
.map(|(_, v)| percent_decode(v))
}
fn percent_decode(s: &str) -> String {
let bytes = s.as_bytes();
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'%' if i + 2 < bytes.len() => {
let hex = std::str::from_utf8(&bytes[i + 1..i + 3]).unwrap_or("");
match u8::from_str_radix(hex, 16) {
Ok(b) => {
out.push(b);
i += 3;
}
Err(_) => {
out.push(b'%');
i += 1;
}
}
}
b'+' => {
out.push(b' ');
i += 1;
}
b => {
out.push(b);
i += 1;
}
}
}
String::from_utf8_lossy(&out).into_owned()
}
fn write_json(stream: &mut TcpStream, body: &str) {
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\
Access-Control-Allow-Origin: *\r\nContent-Length: {}\r\n\r\n{}",
body.len(),
body,
);
let _ = stream.write_all(response.as_bytes());
}
fn serve_node(stream: &mut TcpStream, state: &SharedState, query: &str) {
let path = query_param(query, "path").unwrap_or_else(|| "root".to_string());
let snap = state.records.lock().unwrap().snapshot(&path);
write_json(stream, &snap.to_string());
}
fn serve_record_history(stream: &mut TcpStream, state: &SharedState, query: &str) {
let path = query_param(query, "path").unwrap_or_else(|| "root".to_string());
let n = query_param(query, "n")
.and_then(|v| v.parse::<usize>().ok())
.unwrap_or(DEFAULT_HISTORY)
.min(MAX_HISTORY);
let body = {
let store = state.records.lock().unwrap();
let lines: Vec<String> =
store.history(&path, n).iter().map(|r| r.to_string()).collect();
format!("[{}]", lines.join(","))
};
write_json(stream, &body);
}
fn serve_paths(stream: &mut TcpStream, state: &SharedState) {
let body = {
let store = state.records.lock().unwrap();
serde_json::to_string(&store.paths()).unwrap_or_else(|_| "[]".to_string())
};
write_json(stream, &body);
}
fn serve_html(stream: &mut TcpStream, state: &SharedState) {
let label = state.label.lock().unwrap().clone();
let hash = state.hash.lock().unwrap().clone();
let hardware = state.hardware.lock().unwrap().clone();
let metadata = state.metadata.lock().unwrap().clone();
let gpu_init = state.gpu_init.lock().unwrap().clone();
let has_inject = label.is_some() || hash.is_some() || hardware.is_some()
|| metadata.is_some() || gpu_init.is_some();
let body = if has_inject {
let label_js = match &label {
Some(l) => format!("\"{}\"", l.replace('\\', "\\\\").replace('"', "\\\"")),
None => "null".to_string(),
};
let hash_js = match &hash {
Some(h) => format!("\"{}\"", h),
None => "null".to_string(),
};
let hw_js = match &hardware {
Some(h) => format!("\"{}\"", h.replace('\\', "\\\\").replace('"', "\\\"")),
None => "null".to_string(),
};
let meta_js = match &metadata {
Some(m) => m.clone(),
None => "null".to_string(),
};
let gpu_init_js = match &gpu_init {
Some(j) => j.clone(),
None => "null".to_string(),
};
let consts = super::neutralize_script_close(&format!(
"const LIVE_LABEL={};const LIVE_HASH={};const LIVE_HARDWARE={};const LIVE_META={};const LIVE_GPU_INIT={};",
label_js, hash_js, hw_js, meta_js, gpu_init_js,
));
let inject = format!("<script>{}</script>\n", consts);
DASHBOARD_HTML.replacen("<script>", &format!("{}<script>", inject), 1)
} else {
DASHBOARD_HTML.to_string()
};
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: {}\r\n\r\n{}",
body.len(),
body,
);
let _ = stream.write_all(response.as_bytes());
}
fn serve_sse(mut stream: TcpStream, state: &SharedState, scope: Option<String>) {
let headers = "HTTP/1.1 200 OK\r\n\
Content-Type: text/event-stream\r\n\
Cache-Control: no-cache\r\n\
Connection: keep-alive\r\n\
Access-Control-Allow-Origin: *\r\n\r\n";
if stream.write_all(headers.as_bytes()).is_err() {
return;
}
let (tx, rx) = mpsc::sync_channel::<String>(SSE_CLIENT_QUEUE);
{
let mut senders = state.sse_senders.lock().unwrap();
if state.shutting_down.load(Ordering::SeqCst) {
return;
}
senders.push(SseClient {
scope: scope.clone(),
tx,
});
}
let preamble: Vec<String> = match scope.as_deref() {
None => state
.epochs
.lock()
.unwrap()
.iter()
.map(|json| format!("event: epoch\ndata: {json}\n\n"))
.collect(),
Some(path) => {
let store = state.records.lock().unwrap();
store
.meta()
.into_iter()
.chain(store.history(path, DEFAULT_HISTORY))
.map(|r| format!("event: record\ndata: {r}\n\n"))
.collect()
}
};
for event in preamble {
if stream.write_all(event.as_bytes()).is_err() {
return;
}
}
let _ = stream.flush();
for event in rx {
if stream.write_all(event.as_bytes()).is_err() {
break;
}
let _ = stream.flush();
}
}
fn serve_svg(stream: &mut TcpStream, state: &SharedState) {
let svg = state.svg.lock().unwrap().clone();
if let Some(s) = svg {
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: image/svg+xml\r\nContent-Length: {}\r\n\r\n{}",
s.len(),
s,
);
let _ = stream.write_all(response.as_bytes());
} else {
let _ = stream.write_all(b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n");
}
}
fn serve_history(stream: &mut TcpStream, state: &SharedState) {
let body = {
let epochs = state.epochs.lock().unwrap();
format!("[{}]", epochs.join(","))
};
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
body.len(),
body,
);
let _ = stream.write_all(response.as_bytes());
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_loopback_addr_classifies_bind_targets() {
assert!(is_loopback_addr("127.0.0.1"));
assert!(is_loopback_addr("::1"));
assert!(is_loopback_addr("localhost"));
assert!(!is_loopback_addr("0.0.0.0"));
assert!(!is_loopback_addr("192.168.1.5"));
assert!(!is_loopback_addr("::"));
}
#[test]
fn query_param_decodes_and_misses_cleanly() {
assert_eq!(query_param("path=root", "path").as_deref(), Some("root"));
assert_eq!(
query_param("path=root%2Fexa%2Frank0", "path").as_deref(),
Some("root/exa/rank0"),
);
assert_eq!(query_param("path=root/exa", "path").as_deref(), Some("root/exa"));
assert_eq!(query_param("n=10&path=root", "path").as_deref(), Some("root"));
assert_eq!(query_param("n=10", "path"), None);
assert_eq!(query_param("", "path"), None);
assert_eq!(query_param("path=a%zz", "path").as_deref(), Some("a%zz"));
assert_eq!(query_param("path=a%", "path").as_deref(), Some("a%"));
}
#[test]
fn injected_constants_land_ahead_of_the_first_reader() {
let first = DASHBOARD_HTML.find("<script>").expect("no script block");
let first_body = &DASHBOARD_HTML[first..];
let first_body = &first_body[..first_body.find("</script>").unwrap()];
assert!(
first_body.contains("ARCHIVE_THEME"),
"the first script block must be the theme init (it reads ARCHIVE_THEME)",
);
assert!(
first_body.contains("data-theme"),
"the theme init must set data-theme before the body paints",
);
assert_eq!(
DASHBOARD_HTML.matches("<script>").count(),
DASHBOARD_HTML.matches("</script>").count(),
);
}
#[test]
fn the_page_reads_the_record_plane_and_the_epoch_feed() {
for needle in [
"'/stream?path='",
"'/history?path='",
"EventSource('/events')",
"ARCHIVE_DATA",
"LIVE_GPU_INIT",
] {
assert!(DASHBOARD_HTML.contains(needle), "page lost {needle}");
}
}
#[test]
fn the_page_only_reaches_for_elements_and_handlers_it_has() {
let (markup, js) = {
let mut markup = String::new();
let mut js = String::new();
let mut rest = DASHBOARD_HTML;
while let Some(open) = rest.find("<script>") {
markup.push_str(&rest[..open]);
let after = &rest[open + "<script>".len()..];
let close = after.find("</script>").expect("unclosed <script>");
js.push_str(&after[..close]);
rest = &after[close + "</script>".len()..];
}
markup.push_str(rest);
(markup, js)
};
let js = js.as_str();
fn ids(hay: &str, needle: &str, term: char) -> Vec<String> {
hay.match_indices(needle)
.filter_map(|(i, _)| {
let rest = &hay[i + needle.len()..];
rest.find(term).map(|e| rest[..e].to_string())
})
.collect()
}
let declared = ids(&markup, "id=\"", '"');
for want in ids(js, "getElementById('", '\'') {
assert!(
declared.contains(&want),
"the page calls getElementById('{want}') but declares no such id",
);
}
for handler in ids(&markup, "onclick=\"", '(')
.into_iter()
.chain(ids(&markup, "onchange=\"", '('))
{
assert!(
js.contains(&format!("function {handler}(")),
"inline handler {handler}() has no function in the page script",
);
}
}
#[test]
fn serve_html_injects_constants_once_ahead_of_the_page() {
let mut srv = DashboardServer::start(0).expect("bind");
let addr = srv.addr;
srv.set_hardware("2x GPU test rig".to_string());
let body = get_until(addr, "/", "LIVE_HARDWARE");
assert_eq!(body.matches("const LIVE_HARDWARE=").count(), 1);
let consts = body.find("const LIVE_HARDWARE=").unwrap();
let boot = body.find("floDl monitoring portal").unwrap();
assert!(consts < boot, "constants injected after the page body");
srv.shutdown();
}
fn get(addr: SocketAddr, target: &str) -> String {
let mut s = TcpStream::connect(addr).unwrap();
s.set_read_timeout(Some(std::time::Duration::from_secs(10))).unwrap();
s.write_all(format!("GET {target} HTTP/1.1\r\nHost: x\r\n\r\n").as_bytes())
.unwrap();
let mut raw = String::new();
let _ = s.read_to_string(&mut raw);
raw.split_once("\r\n\r\n").map(|(_, b)| b.to_string()).unwrap_or(raw)
}
fn get_until(addr: SocketAddr, target: &str, needle: &str) -> String {
for _ in 0..500 {
let body = get(addr, target);
if body.contains(needle) {
return body;
}
thread::sleep(std::time::Duration::from_millis(20));
}
panic!("{target} never contained {needle}");
}
fn open_sse(addr: SocketAddr, target: &str) -> TcpStream {
let mut s = TcpStream::connect(addr).unwrap();
s.set_read_timeout(Some(std::time::Duration::from_millis(600))).unwrap();
s.write_all(format!("GET {target} HTTP/1.1\r\nHost: x\r\n\r\n").as_bytes())
.unwrap();
s
}
fn drain(s: &mut TcpStream) -> String {
let mut out = String::new();
let mut buf = [0u8; 8192];
loop {
match s.read(&mut buf) {
Ok(0) => break,
Ok(n) => out.push_str(&String::from_utf8_lossy(&buf[..n])),
Err(_) => break, }
}
out
}
fn node_rec(path: &str, tick: u64) -> Value {
serde_json::json!({ "v": 1, "kind": "node", "path": path, "tick": tick,
"metrics": { "loss": 0.5 }, "work": 10.0 })
}
fn tree(tick: u64) -> Vec<Value> {
vec![
node_rec("root", tick),
node_rec("root/exa", tick),
node_rec("root/exa/rank0", tick),
node_rec("root/pascal", tick),
node_rec("root/pascal/rank1", tick),
]
}
#[test]
fn node_endpoint_serves_one_level_not_the_cluster() {
let mut srv = DashboardServer::start(0).expect("bind");
let addr = srv.addr;
srv.push_records(tree(1));
let body = get_until(addr, "/node?path=root", "\"root\"");
let v: Value = serde_json::from_str(&body).unwrap();
assert_eq!(v["node"]["path"], "root");
let kids: Vec<&str> = v["children"]
.as_array()
.unwrap()
.iter()
.map(|c| c["path"].as_str().unwrap())
.collect();
assert_eq!(kids, vec!["root/exa", "root/pascal"]);
let body = get(addr, "/node?path=root/exa");
let v: Value = serde_json::from_str(&body).unwrap();
assert_eq!(v["node"]["path"], "root/exa");
assert_eq!(v["children"].as_array().unwrap().len(), 1);
assert_eq!(v["children"][0]["path"], "root/exa/rank0");
let enc = get(addr, "/node?path=root%2Fexa");
assert_eq!(enc, body);
srv.shutdown();
}
#[test]
fn history_endpoint_honors_scope_and_n() {
let mut srv = DashboardServer::start(0).expect("bind");
let addr = srv.addr;
for t in 1..=5 {
srv.push_records(tree(t));
}
let body = get_until(addr, "/history?path=root&n=100", "\"tick\":5");
let v: Vec<Value> = serde_json::from_str(&body).unwrap();
assert_eq!(v.len(), 15, "{body}");
assert!(v.iter().all(|r| r["path"] != "root/exa/rank0"));
assert_eq!(v.last().unwrap()["tick"], 5);
let v: Vec<Value> =
serde_json::from_str(&get(addr, "/history?path=root&n=2")).unwrap();
assert_eq!(v.len(), 2);
assert_eq!(v.last().unwrap()["tick"], 5);
srv.shutdown();
}
#[test]
fn paths_endpoint_is_the_navigation_index() {
let mut srv = DashboardServer::start(0).expect("bind");
let addr = srv.addr;
srv.push_records(tree(1));
let body = get_until(addr, "/paths", "root/pascal/rank1");
let v: Vec<String> = serde_json::from_str(&body).unwrap();
assert_eq!(
v,
vec![
"root",
"root/exa",
"root/exa/rank0",
"root/pascal",
"root/pascal/rank1",
],
);
srv.shutdown();
}
#[test]
fn stream_is_path_scoped_per_subscriber() {
let mut srv = DashboardServer::start(0).expect("bind");
let addr = srv.addr;
let mut at_root = open_sse(addr, "/stream?path=root");
let mut at_exa = open_sse(addr, "/stream?path=root%2Fexa");
let _ = drain(&mut at_root);
let _ = drain(&mut at_exa);
srv.push_records(tree(1));
srv.push_records(vec![serde_json::json!({
"v": 1, "kind": "event", "path": "root/pascal/rank1",
"class": "rank_lost", "sev": "critical", "detail": "died", "count": 1,
})]);
let root_feed = drain(&mut at_root);
let exa_feed = drain(&mut at_exa);
assert!(root_feed.contains("\"path\":\"root\""), "{root_feed}");
assert!(root_feed.contains("\"path\":\"root/exa\""));
assert!(!root_feed.contains("\"path\":\"root/exa/rank0\""));
assert!(root_feed.contains("rank_lost"), "{root_feed}");
assert!(exa_feed.contains("\"path\":\"root/exa/rank0\""), "{exa_feed}");
assert!(!exa_feed.contains("\"path\":\"root/pascal\""));
assert!(!exa_feed.contains("rank_lost"));
srv.shutdown();
}
#[test]
fn stream_preamble_replays_meta_then_history() {
let mut srv = DashboardServer::start(0).expect("bind");
let addr = srv.addr;
srv.push_records(vec![serde_json::json!({
"v": 1, "kind": "meta", "reductions": { "accuracy": "mean" },
})]);
srv.push_records(tree(1));
srv.push_records(tree(2));
get_until(addr, "/history?path=root&n=100", "\"tick\":2");
let mut late = open_sse(addr, "/stream?path=root");
let feed = drain(&mut late);
let meta_at = feed.find("\"kind\":\"meta\"").expect("meta replayed");
let first_node = feed.find("\"kind\":\"node\"").expect("history replayed");
assert!(meta_at < first_node, "meta must precede any record");
assert!(feed.contains("\"tick\":1") && feed.contains("\"tick\":2"));
srv.shutdown();
}
#[test]
fn the_epoch_feed_is_untouched_by_the_record_plane() {
let mut srv = DashboardServer::start(0).expect("bind");
let addr = srv.addr;
let mut epochs = open_sse(addr, "/events");
let mut records = open_sse(addr, "/stream?path=root");
let _ = drain(&mut epochs);
let _ = drain(&mut records);
srv.push_epoch("{\"epoch\":1}".to_string());
srv.push_records(tree(1));
let epoch_feed = drain(&mut epochs);
let record_feed = drain(&mut records);
assert!(epoch_feed.contains("event: epoch"), "{epoch_feed}");
assert!(!epoch_feed.contains("event: record"), "{epoch_feed}");
assert!(record_feed.contains("event: record"), "{record_feed}");
assert!(!record_feed.contains("event: epoch"), "{record_feed}");
srv.shutdown();
}
#[test]
fn shutdown_closes_a_scoped_subscriber_too() {
let mut srv = DashboardServer::start(0).expect("bind");
let addr = srv.addr;
let mut sub = open_sse(addr, "/stream?path=root");
let _ = drain(&mut sub);
thread::sleep(std::time::Duration::from_millis(100));
srv.push_records(tree(1));
srv.shutdown();
sub.set_read_timeout(Some(std::time::Duration::from_secs(10))).unwrap();
let mut buf = [0u8; 512];
loop {
match sub.read(&mut buf) {
Ok(0) => break,
Ok(_) => continue,
Err(e) => panic!("scoped SSE socket still open after shutdown: {e}"),
}
}
std::net::TcpListener::bind(addr).expect("port free after shutdown");
}
#[test]
fn shutdown_closes_sse_and_frees_port() {
let mut srv = DashboardServer::start(0).expect("bind ephemeral port");
let addr = srv.addr;
let mut sse = TcpStream::connect(addr).unwrap();
sse.set_read_timeout(Some(std::time::Duration::from_secs(10)))
.unwrap();
sse.write_all(b"GET /events HTTP/1.1\r\nHost: x\r\n\r\n")
.unwrap();
let mut buf = [0u8; 512];
let n = sse.read(&mut buf).unwrap();
assert!(n > 0, "SSE headers expected");
std::thread::sleep(std::time::Duration::from_millis(100));
srv.push_epoch("{\"epoch\":1}".to_string());
srv.shutdown();
loop {
match sse.read(&mut buf) {
Ok(0) => break,
Ok(_) => continue,
Err(e) => panic!("SSE socket still open after shutdown: {e}"),
}
}
std::net::TcpListener::bind(addr).expect("port must be free after shutdown");
}
}