use std::thread;
use tiny_http::{Request, Response};
use super::api;
use super::assembly;
use super::bind::bind;
use super::bodies;
use super::bom;
use super::desktop::{announce_ready, exit_on_stdin_eof};
use super::firmware;
use super::git_api;
use super::guard::{is_allowed_host, is_same_origin};
use super::hub::stream_events;
use super::kicad_watch::start_kicad_watchers;
use super::limiter::handlers;
use super::refresh::{load_bundle, rebuild_trigger, refresh, warn_if_bundle_missing, watch_dir};
use super::run_api;
use super::serve::serve_static;
use super::state::{Slot, State};
use super::watcher::start_file_watcher;
use crate::commands::target::BoardTarget;
pub(super) fn query_param(url: &str, key: &str) -> Option<String> {
let (_, query) = url.split_once('?')?;
let value = query
.split('&')
.find_map(|pair| pair.strip_prefix(key)?.strip_prefix('='))
.filter(|value| !value.is_empty())?;
percent_decode(value)
}
pub(super) fn percent_decode(value: &str) -> Option<String> {
let bytes = value.as_bytes();
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
let hex = bytes
.get(i + 1..i + 3)
.and_then(|h| std::str::from_utf8(h).ok());
match (bytes[i], hex.and_then(|h| u8::from_str_radix(h, 16).ok())) {
(b'%', Some(byte)) => (out.push(byte), i += 3),
(b'+', _) => (out.push(b' '), i += 1),
(byte, _) => (out.push(byte), i += 1),
};
}
String::from_utf8(out).ok()
}
pub(super) fn route_path(url: &str) -> &str {
url.split_once('?').map_or(url, |(path, _)| path)
}
pub(super) fn handle(request: Request, state: &State, port: u16) {
let host = request.headers().iter().find(|h| h.field.equiv("Host"));
if !is_allowed_host(host.map(|h| h.value.as_str()), port) {
let _ = request.respond(Response::from_string("Forbidden").with_status_code(403));
return;
}
let path = route_path(request.url()).to_string();
let guarded = path.starts_with("/api/") || path == "/events";
if guarded && !is_same_origin(&request, port) {
let _ = request.respond(Response::from_string("Forbidden").with_status_code(403));
return;
}
match path.as_str() {
"/api/mechanical" => api::body_preview(request, state, false),
"/api/mechanical.glb" => api::body_preview(request, state, true),
"/api/circuit" => api::circuit(request, state),
"/api/project" => api::project(request, state),
"/api/bom" => bom::serve(request, state, port),
"/api/runs" => api::runs(request, state),
p if assembly::owns(p) => assembly::serve(request, p),
p if p.starts_with("/api/bodies/") => bodies::serve(request, p),
"/api/kicad" => api::kicad(request, state),
"/api/firmware" => firmware::serve(request, state),
"/api/run" => run_api::start_run(request, state, port),
p if git_api::owns(p) => git_api::serve(request, state, port),
"/events" => subscribe(request, state),
_ => serve_static(request, state, &path),
}
}
fn subscribe(request: Request, state: &State) {
let ready = || {
state
.slots
.iter()
.map(Slot::payload)
.filter(|p| !p.is_empty())
};
let rx = state.hub.subscribe(|| ready().collect());
thread::spawn(move || stream_events(request, rx));
}
fn dispatch(request: Request, state: &State, port: u16) {
let Some(permit) = handlers().try_acquire() else {
let _ = request.respond(Response::from_string("Busy").with_status_code(503));
return;
};
let state = state.clone();
thread::spawn(move || {
handle(request, &state, port);
drop(permit);
});
}
fn refresh_all(state: &State) {
for slot in state.slots.iter() {
let (slot, state) = (slot.clone(), state.clone());
thread::spawn(move || refresh(&slot, &state));
}
}
pub fn run_dev_server(
boards: Vec<BoardTarget>,
asked: u16,
desktop: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let (server, port) = bind(asked)?;
if port != asked {
println!("ℹ Port {asked} is in use, using {port} instead.");
}
let mut state = State::new(&boards);
state.desktop = desktop;
if desktop {
announce_ready(port);
exit_on_stdin_eof();
}
load_bundle(&state);
warn_if_bundle_missing(&state);
refresh_all(&state);
println!("⚡ Fusible Dev Server running at http://localhost:{port} (press Ctrl+C to quit)");
let _watchers = state
.slots
.iter()
.map(|slot| {
let trigger = rebuild_trigger(slot.clone(), state.clone());
start_file_watcher(&watch_dir(&slot.entry), trigger)
})
.collect::<Result<Vec<_>, _>>()?;
let _kicad = start_kicad_watchers(&state);
for request in server.incoming_requests() {
dispatch(request, &state, port);
}
Ok(())
}