#![doc = document_features::document_features!()]
#![forbid(unsafe_code)]
#![warn(clippy::all, rust_2018_idioms)]
use std::fmt::Display;
use std::str::FromStr;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
pub const DEFAULT_WEB_VIEWER_SERVER_PORT: u16 = 9090;
#[cfg(all(not(disable_web_viewer_server), not(trailing_web_viewer)))]
mod data {
#![expect(clippy::large_include_file)]
#[inline]
pub fn index_html() -> &'static [u8] {
include_bytes!("../web_viewer/index.html")
}
#[inline]
pub fn favicon() -> &'static [u8] {
include_bytes!("../web_viewer/favicon.svg")
}
#[inline]
pub fn sw_js() -> &'static [u8] {
include_bytes!("../web_viewer/sw.js")
}
#[inline]
pub fn viewer_js() -> &'static [u8] {
include_bytes!("../web_viewer/re_viewer.js")
}
#[inline]
pub fn viewer_wasm() -> &'static [u8] {
include_bytes!("../web_viewer/re_viewer_bg.wasm")
}
#[inline]
pub fn signed_in_html() -> &'static [u8] {
include_bytes!("../web_viewer/signed-in.html")
}
#[inline]
pub fn signed_out_html() -> &'static [u8] {
include_bytes!("../web_viewer/signed-out.html")
}
}
#[cfg(all(not(disable_web_viewer_server), trailing_web_viewer))]
mod trailing_data;
#[cfg(all(not(disable_web_viewer_server), trailing_web_viewer))]
use trailing_data as data;
#[derive(thiserror::Error, Debug)]
pub enum WebViewerServerError {
#[error("Could not parse address: {0}")]
AddrParseFailed(#[from] std::net::AddrParseError),
#[error("Failed to create server: {source}: ({address})")]
CreateServerFailed {
source: Box<dyn std::error::Error + Send + Sync + 'static>,
address: String,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct WebViewerServerPort(pub u16);
impl From<u16> for WebViewerServerPort {
#[inline]
fn from(port: u16) -> Self {
Self(port)
}
}
impl WebViewerServerPort {
pub const AUTO: Self = Self(0);
}
impl Default for WebViewerServerPort {
fn default() -> Self {
Self(DEFAULT_WEB_VIEWER_SERVER_PORT)
}
}
impl Display for WebViewerServerPort {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl FromStr for WebViewerServerPort {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.parse::<u16>() {
Ok(port) => Ok(Self(port)),
Err(err) => Err(format!("Failed to parse port: {err}")),
}
}
}
#[must_use = "Dropping this means stopping the server"]
pub struct WebViewerServer {
inner: Arc<WebViewerServerInner>,
thread_handle: Option<std::thread::JoinHandle<()>>,
}
struct WebViewerServerInner {
server: tiny_http::Server,
shutdown: AtomicBool,
num_wasm_served: AtomicU64,
}
impl WebViewerServer {
pub fn new(bind_ip: &str, port: WebViewerServerPort) -> Result<Self, WebViewerServerError> {
let bind_addr = std::net::SocketAddr::new(bind_ip.parse()?, port.0);
let server = tiny_http::Server::http(bind_addr).map_err(|err| {
WebViewerServerError::CreateServerFailed {
address: bind_addr.to_string(),
source: err,
}
})?;
let shutdown = AtomicBool::new(false);
let inner = Arc::new(WebViewerServerInner {
server,
shutdown,
num_wasm_served: Default::default(),
});
let inner_copy = inner.clone();
let thread_handle = std::thread::Builder::new()
.name("re_web_viewer_server".to_owned())
.spawn(move || inner_copy.serve())
.ok();
Ok(Self {
inner,
thread_handle,
})
}
pub fn server_url(&self) -> String {
let local_addr = self.inner.server.server_addr();
if let Some(local_addr) = local_addr.clone().to_ip()
&& local_addr.ip().is_unspecified()
{
return format!("http://127.0.0.1:{}", local_addr.port());
}
format!("http://{local_addr}")
}
pub fn block(mut self) {
if let Some(thread_handle) = self.thread_handle.take() {
thread_handle.join().ok();
}
}
pub fn detach(mut self) {
if let Some(thread_handle) = self.thread_handle.take() {
drop(thread_handle);
}
}
}
impl Drop for WebViewerServer {
fn drop(&mut self) {
if let Some(thread_handle) = self.thread_handle.take() {
let num_wasm_served = self.inner.num_wasm_served.load(Ordering::Relaxed);
re_log::debug!(
"Shutting down web server after serving the Wasm {num_wasm_served} time(s)"
);
self.inner.shutdown.store(true, Ordering::Release);
self.inner.server.unblock();
thread_handle.join().ok();
}
}
}
impl WebViewerServerInner {
fn serve(&self) {
loop {
let request = self.server.recv();
if self.shutdown.load(Ordering::Acquire) {
return;
}
let request = match request {
Ok(request) => request,
Err(err) => {
re_log::error!("Failed to receive http request: {err}");
continue;
}
};
if let Err(err) = self.send_response(request) {
re_log::error!("Failed to send http response: {err}");
}
}
}
fn on_serve_wasm(&self) {
self.num_wasm_served.fetch_add(1, Ordering::Relaxed);
#[cfg(feature = "analytics")]
re_analytics::record(|| re_analytics::event::ServeWasm);
}
#[cfg(disable_web_viewer_server)]
fn send_response(&self, _request: tiny_http::Request) -> Result<(), std::io::Error> {
if false {
self.on_serve_wasm(); }
panic!(
"re_web_viewer_server compiled without .wasm, because of '__disable_server' feature, `--all-features`, or 'RERUN_DISABLE_WEB_VIEWER_SERVER=1'. DON'T DO THAT! It's only meant for tests and docs!"
);
}
#[cfg(not(disable_web_viewer_server))]
fn send_response(&self, request: tiny_http::Request) -> Result<(), std::io::Error> {
let url = request.url();
let path = url.split('?').next().unwrap_or(url);
let (mime, bytes): (&str, &[u8]) = match path {
"/" | "/index.html" => ("text/html", data::index_html()),
"/favicon.svg" => ("image/svg+xml", data::favicon()),
"/favicon.ico" => ("image/x-icon", data::favicon()),
"/sw.js" => ("text/javascript", data::sw_js()),
"/re_viewer.js" => ("text/javascript", data::viewer_js()),
"/re_viewer_bg.wasm" => {
self.on_serve_wasm();
("application/wasm", data::viewer_wasm())
}
"/signed-in" => ("text/html", data::signed_in_html()),
"/signed-out" => ("text/html", data::signed_out_html()),
_ => {
re_log::warn!("404 path: {}", path);
return request.respond(tiny_http::Response::empty(404));
}
};
let mut response = tiny_http::Response::from_data(bytes).with_header(
tiny_http::Header::from_str(&format!("Content-Type: {mime}"))
.expect("Invalid http header"),
);
if let Ok(header) =
tiny_http::Header::from_str(&format!("rerun-final-length: {}", bytes.len()))
{
response.add_header(header);
}
request.respond(response)
}
}