diff --git a/crates/re_sdk_comms/src/server.rs b/crates/re_sdk_comms/src/server.rs index fdf869df0d37..305c3ee69811 100644 --- a/crates/re_sdk_comms/src/server.rs +++ b/crates/re_sdk_comms/src/server.rs @@ -59,18 +59,19 @@ async fn listen_for_new_clients( /// # use re_sdk_comms::{serve, ServerOptions}; /// #[tokio::main] /// async fn main() { -/// let (sender, receiver) = tokio::sync::broadcast::channel(1); -/// let log_msg_rx = serve(80, ServerOptions::default(), receiver).await.unwrap(); +/// let (shutdown_tx, shutdown_rx) = tokio::sync::broadcast::channel(1); +/// let log_msg_rx = serve("0.0.0.0", 80, ServerOptions::default(), shutdown_rx).await.unwrap(); /// } /// ``` pub async fn serve( + bind_ip: &str, port: u16, options: ServerOptions, shutdown_rx: tokio::sync::broadcast::Receiver<()>, ) -> anyhow::Result> { let (tx, rx) = re_smart_channel::smart_channel(re_smart_channel::Source::TcpServer { port }); - let bind_addr = format!("0.0.0.0:{port}"); + let bind_addr = format!("{bind_ip}:{port}"); let listener = TcpListener::bind(&bind_addr).await.with_context(|| { format!( "Failed to bind TCP address {bind_addr:?}. Another Rerun instance is probably running." diff --git a/crates/re_viewer/src/web.rs b/crates/re_viewer/src/web.rs index 33353a02808a..b073b568bad3 100644 --- a/crates/re_viewer/src/web.rs +++ b/crates/re_viewer/src/web.rs @@ -187,7 +187,12 @@ fn get_url(info: &eframe::IntegrationInfo) -> String { url = param.clone(); } if url.is_empty() { - re_ws_comms::server_url(&info.web_info.location.hostname, Default::default()) + format!( + "{}://{}:{}", + re_ws_comms::PROTOCOL, + &info.web_info.location.hostname, + re_ws_comms::DEFAULT_WS_SERVER_PORT + ) } else { url } diff --git a/crates/re_web_viewer_server/src/lib.rs b/crates/re_web_viewer_server/src/lib.rs index 814e0eed0039..8a984750a444 100644 --- a/crates/re_web_viewer_server/src/lib.rs +++ b/crates/re_web_viewer_server/src/lib.rs @@ -9,6 +9,7 @@ use std::{ fmt::Display, + net::SocketAddr, str::FromStr, task::{Context, Poll}, }; @@ -175,7 +176,7 @@ impl Service for MakeSvc { pub struct WebViewerServerPort(pub u16); impl WebViewerServerPort { - /// Port to use with [`WebViewerServer::port`] when you want the OS to pick a port for you. + /// Port to use with [`WebViewerServer::new`] when you want the OS to pick a port for you. /// /// This is defined as `0`. pub const AUTO: Self = Self(0); @@ -222,7 +223,7 @@ impl WebViewerServer { /// # async fn example() -> Result<(), WebViewerServerError> { /// let (shutdown_tx, shutdown_rx) = tokio::sync::broadcast::channel(1); /// let server = WebViewerServer::new("0.0.0.0", WebViewerServerPort::AUTO)?; - /// let port = server.port(); + /// let server_url = server.server_url(); /// server.serve(shutdown_rx).await?; /// # Ok(()) } /// ``` @@ -247,8 +248,9 @@ impl WebViewerServer { Ok(()) } - pub fn port(&self) -> WebViewerServerPort { - WebViewerServerPort(self.server.local_addr().port()) + /// Includes `http://` prefix + pub fn server_url(&self) -> String { + server_url(&self.server.local_addr()) } } @@ -256,13 +258,13 @@ impl WebViewerServer { /// /// When dropped, the server will be shut down. pub struct WebViewerServerHandle { - port: WebViewerServerPort, + local_addr: std::net::SocketAddr, shutdown_tx: tokio::sync::broadcast::Sender<()>, } impl Drop for WebViewerServerHandle { fn drop(&mut self) { - re_log::info!("Shutting down web server on port {}.", self.port); + re_log::info!("Shutting down web server on {}", self.server_url()); self.shutdown_tx.send(()).ok(); } } @@ -274,22 +276,39 @@ impl WebViewerServerHandle { /// A port of 0 will let the OS choose a free port. /// /// The caller needs to ensure that there is a `tokio` runtime running. - pub fn new(requested_port: WebViewerServerPort) -> Result { + pub fn new( + bind_ip: &str, + requested_port: WebViewerServerPort, + ) -> Result { let (shutdown_tx, shutdown_rx) = tokio::sync::broadcast::channel(1); - let web_server = WebViewerServer::new("0.0.0.0", requested_port)?; + let web_server = WebViewerServer::new(bind_ip, requested_port)?; - let port = web_server.port(); + let local_addr = web_server.server.local_addr(); tokio::spawn(async move { web_server.serve(shutdown_rx).await }); - re_log::info!("Started web server on port {}.", port); + let slf = Self { + local_addr, + shutdown_tx, + }; + + re_log::info!("Started web server on {}", slf.server_url()); - Ok(Self { port, shutdown_tx }) + Ok(slf) } - /// Get the port where the HTTP server is listening - pub fn port(&self) -> WebViewerServerPort { - self.port + /// Includes `http://` prefix + pub fn server_url(&self) -> String { + server_url(&self.local_addr) + } +} + +fn server_url(local_addr: &SocketAddr) -> String { + if local_addr.ip().is_unspecified() { + // "0.0.0.0" + format!("http://localhost:{}", local_addr.port()) + } else { + format!("http://{local_addr}") } } diff --git a/crates/re_web_viewer_server/src/main.rs b/crates/re_web_viewer_server/src/main.rs index 38b8997fc474..818435c8e157 100644 --- a/crates/re_web_viewer_server/src/main.rs +++ b/crates/re_web_viewer_server/src/main.rs @@ -44,8 +44,7 @@ async fn main() { ) .expect("Could not create web server"); - let port = server.port(); - let url = format!("http://{bind_ip}:{port}"); + let url = server.server_url(); eprintln!("Hosting web-viewer on {url}"); if args.open { diff --git a/crates/re_ws_comms/src/lib.rs b/crates/re_ws_comms/src/lib.rs index e38527f04dc4..a2e6af107d29 100644 --- a/crates/re_ws_comms/src/lib.rs +++ b/crates/re_ws_comms/src/lib.rs @@ -76,8 +76,14 @@ impl FromStr for RerunServerPort { } } -pub fn server_url(hostname: &str, port: RerunServerPort) -> String { - format!("{PROTOCOL}://{hostname}:{port}") +/// Add a protocol (`ws://` or `wss://`) to the given address. +pub fn server_url(local_addr: &std::net::SocketAddr) -> String { + if local_addr.ip().is_unspecified() { + // "0.0.0.0" + format!("{PROTOCOL}://localhost:{}", local_addr.port()) + } else { + format!("{PROTOCOL}://{local_addr}") + } } const PREFIX: [u8; 4] = *b"RR00"; diff --git a/crates/re_ws_comms/src/server.rs b/crates/re_ws_comms/src/server.rs index 2412d142a730..49c4a4dbfa75 100644 --- a/crates/re_ws_comms/src/server.rs +++ b/crates/re_ws_comms/src/server.rs @@ -21,29 +21,33 @@ use crate::{server_url, RerunServerError, RerunServerPort}; /// Websocket host for relaying [`LogMsg`]s to a web viewer. pub struct RerunServer { listener: TcpListener, - port: RerunServerPort, + local_addr: std::net::SocketAddr, } impl RerunServer { /// Create new [`RerunServer`] to relay [`LogMsg`]s to a websocket. /// The websocket will be available at `port`. /// + /// A `bind_ip` of `"0.0.0.0"` is a good default. /// A port of 0 will let the OS choose a free port. - pub async fn new(port: RerunServerPort) -> Result { - let bind_addr = format!("0.0.0.0:{port}"); + pub async fn new(bind_ip: String, port: RerunServerPort) -> Result { + let bind_addr = format!("{bind_ip}:{port}"); let listener = TcpListener::bind(&bind_addr) .await .map_err(|err| RerunServerError::BindFailed(port, err))?; - let port = RerunServerPort(listener.local_addr()?.port()); + let slf = Self { + local_addr: listener.local_addr()?, + listener, + }; re_log::info!( "Listening for websocket traffic on {}. Connect with a Rerun Web Viewer.", - listener.local_addr()? + slf.server_url() ); - Ok(Self { listener, port }) + Ok(slf) } /// Accept new connections until we get a message on `shutdown_rx` @@ -74,8 +78,9 @@ impl RerunServer { } } + /// Contains the `ws://` or `wss://` prefix. pub fn server_url(&self) -> String { - server_url("localhost", self.port) + server_url(&self.local_addr) } } @@ -83,13 +88,13 @@ impl RerunServer { /// /// When dropped, the server will be shut down. pub struct RerunServerHandle { - port: RerunServerPort, + local_addr: std::net::SocketAddr, shutdown_tx: tokio::sync::broadcast::Sender<()>, } impl Drop for RerunServerHandle { fn drop(&mut self) { - re_log::info!("Shutting down Rerun server on port {}.", self.port); + re_log::info!("Shutting down Rerun server on {}", self.server_url()); self.shutdown_tx.send(()).ok(); } } @@ -98,11 +103,13 @@ impl RerunServerHandle { /// Create new [`RerunServer`] to relay [`LogMsg`]s to a websocket. /// Returns a [`RerunServerHandle`] that will shutdown the server when dropped. /// + /// A `bind_ip` of `"0.0.0.0"` is a good default. /// A port of 0 will let the OS choose a free port. /// /// The caller needs to ensure that there is a `tokio` runtime running. pub fn new( rerun_rx: Receiver, + bind_ip: String, requested_port: RerunServerPort, ) -> Result { let (shutdown_tx, shutdown_rx) = tokio::sync::broadcast::channel(1); @@ -110,23 +117,22 @@ impl RerunServerHandle { let rt = tokio::runtime::Handle::current(); let ws_server = rt.block_on(tokio::spawn(async move { - RerunServer::new(requested_port).await + RerunServer::new(bind_ip, requested_port).await }))??; - let port = ws_server.port; + let local_addr = ws_server.local_addr; tokio::spawn(async move { ws_server.listen(rerun_rx, shutdown_rx).await }); - Ok(Self { port, shutdown_tx }) - } - - /// Get the port where the websocket server is listening - pub fn port(&self) -> RerunServerPort { - self.port + Ok(Self { + local_addr, + shutdown_tx, + }) } + /// Contains the `ws://` or `wss://` prefix. pub fn server_url(&self) -> String { - server_url("localhost", self.port) + server_url(&self.local_addr) } } diff --git a/crates/rerun/src/clap.rs b/crates/rerun/src/clap.rs index ea883f0a59a4..f18827e778aa 100644 --- a/crates/rerun/src/clap.rs +++ b/crates/rerun/src/clap.rs @@ -66,6 +66,10 @@ pub struct RerunArgs { #[cfg(feature = "web_viewer")] #[clap(long)] serve: bool, + + /// What bind address IP to use. + #[clap(long, default_value = "0.0.0.0")] + bind: String, } impl RerunArgs { @@ -111,6 +115,7 @@ impl RerunArgs { let open_browser = true; crate::web_viewer::new_sink( open_browser, + &self.bind, WebViewerServerPort::default(), RerunServerPort::default(), )? diff --git a/crates/rerun/src/run.rs b/crates/rerun/src/run.rs index 78b6763a883d..6e15a73cb970 100644 --- a/crates/rerun/src/run.rs +++ b/crates/rerun/src/run.rs @@ -114,6 +114,10 @@ struct Args { #[clap(long)] web_viewer: bool, + /// What bind address IP to use. + #[clap(long, default_value = "0.0.0.0")] + bind: String, + /// What port do we listen to for hosting the web viewer over HTTP. /// A port of 0 will pick a random port. #[cfg(feature = "web_viewer")] @@ -339,6 +343,7 @@ async fn run_impl( #[cfg(feature = "web_viewer")] { let web_viewer = host_web_viewer( + args.bind.clone(), args.web_viewer_port, true, rerun_server_ws_url, @@ -411,7 +416,13 @@ async fn run_impl( // `rerun.spawn()` doesn't need to log that a connection has been made quiet: call_source.is_python(), }; - re_sdk_comms::serve(args.port, server_options, shutdown_rx.resubscribe()).await? + re_sdk_comms::serve( + &args.bind, + args.port, + server_options, + shutdown_rx.resubscribe(), + ) + .await? } #[cfg(not(feature = "server"))] @@ -443,12 +454,14 @@ async fn run_impl( let shutdown_web_viewer = shutdown_rx.resubscribe(); // This is the server which the web viewer will talk to: - let ws_server = re_ws_comms::RerunServer::new(args.ws_server_port).await?; + let ws_server = + re_ws_comms::RerunServer::new(args.bind.clone(), args.ws_server_port).await?; let ws_server_url = ws_server.server_url(); let ws_server_handle = tokio::spawn(ws_server.listen(rx, shutdown_ws_server)); // This is the server that serves the Wasm+HTML: let web_server_handle = tokio::spawn(host_web_viewer( + args.bind.clone(), args.web_viewer_port, true, ws_server_url, diff --git a/crates/rerun/src/web_viewer.rs b/crates/rerun/src/web_viewer.rs index eb40eeea6c13..8f5e85508985 100644 --- a/crates/rerun/src/web_viewer.rs +++ b/crates/rerun/src/web_viewer.rs @@ -17,19 +17,21 @@ struct WebViewerSink { } impl WebViewerSink { + /// A `bind_ip` of `"0.0.0.0"` is a good default. pub fn new( open_browser: bool, + bind_ip: &str, web_port: WebViewerServerPort, ws_port: RerunServerPort, ) -> anyhow::Result { let (rerun_tx, rerun_rx) = re_smart_channel::smart_channel(re_smart_channel::Source::Sdk); - let rerun_server = RerunServerHandle::new(rerun_rx, ws_port)?; - let webviewer_server = WebViewerServerHandle::new(web_port)?; + let rerun_server = RerunServerHandle::new(rerun_rx, bind_ip.to_owned(), ws_port)?; + let webviewer_server = WebViewerServerHandle::new(bind_ip, web_port)?; - let web_port = webviewer_server.port(); - let server_url = rerun_server.server_url(); - let viewer_url = format!("http://127.0.0.1:{web_port}?url={server_url}"); + let http_web_viewer_url = webviewer_server.server_url(); + let ws_server_url = rerun_server.server_url(); + let viewer_url = format!("{http_web_viewer_url}?url={ws_server_url}"); re_log::info!("Web server is running - view it at {viewer_url}"); if open_browser { @@ -53,18 +55,20 @@ impl WebViewerSink { /// Note: this does not include the websocket server. #[cfg(feature = "web_viewer")] pub async fn host_web_viewer( + bind_ip: String, web_port: WebViewerServerPort, open_browser: bool, source_url: String, shutdown_rx: tokio::sync::broadcast::Receiver<()>, ) -> anyhow::Result<()> { - let web_server = re_web_viewer_server::WebViewerServer::new("0.0.0.0", web_port)?; - let port = web_server.port(); + let web_server = re_web_viewer_server::WebViewerServer::new(&bind_ip, web_port)?; + let http_web_viewer_url = web_server.server_url(); let web_server_handle = web_server.serve(shutdown_rx); - let viewer_url = format!("http://127.0.0.1:{port}?url={source_url}"); + let viewer_url = format!("{http_web_viewer_url}?url={source_url}"); re_log::info!("Web server is running - view it at {viewer_url}"); + if open_browser { webbrowser::open(&viewer_url).ok(); } @@ -100,11 +104,13 @@ impl crate::sink::LogSink for WebViewerSink { #[must_use = "the sink must be kept around to keep the servers running"] pub fn new_sink( open_browser: bool, + bind_ip: &str, web_port: WebViewerServerPort, ws_port: RerunServerPort, ) -> anyhow::Result> { Ok(Box::new(WebViewerSink::new( open_browser, + bind_ip, web_port, ws_port, )?)) diff --git a/rerun_py/src/python_bridge.rs b/rerun_py/src/python_bridge.rs index a7c0b5047787..505be80c07d5 100644 --- a/rerun_py/src/python_bridge.rs +++ b/rerun_py/src/python_bridge.rs @@ -495,6 +495,7 @@ fn serve( recording.set_sink( rerun::web_viewer::new_sink( open_browser, + "0.0.0.0", web_port.map(WebViewerServerPort).unwrap_or_default(), ws_port.map(RerunServerPort).unwrap_or_default(), ) @@ -1160,7 +1161,7 @@ fn version() -> String { fn get_app_url() -> String { #[cfg(feature = "web_viewer")] if let Some(hosted_assets) = &*global_web_viewer_server() { - return format!("http://localhost:{}", hosted_assets.port()); + return hosted_assets.server_url(); } let build_info = re_build_info::build_info!(); @@ -1179,13 +1180,12 @@ fn start_web_viewer_server(port: u16) -> PyResult<()> { let _guard = enter_tokio_runtime(); *web_handle = Some( - re_web_viewer_server::WebViewerServerHandle::new(WebViewerServerPort(port)).map_err( - |err| { + re_web_viewer_server::WebViewerServerHandle::new("0.0.0.0", WebViewerServerPort(port)) + .map_err(|err| { PyRuntimeError::new_err(format!( "Failed to start web viewer server on port {port}: {err}", )) - }, - )?, + })?, ); Ok(())