diff --git a/packages/cli/src/serve/output.rs b/packages/cli/src/serve/output.rs index 6a0637a11..76b15e364 100644 --- a/packages/cli/src/serve/output.rs +++ b/packages/cli/src/serve/output.rs @@ -31,7 +31,7 @@ use tracing::Level; use super::AppServer; const TICK_RATE_MS: u64 = 100; -const VIEWPORT_MAX_WIDTH: u16 = 90; +const VIEWPORT_MAX_WIDTH: u16 = 92; const VIEWPORT_HEIGHT_SMALL: u16 = 5; const VIEWPORT_HEIGHT_BIG: u16 = 14; @@ -740,14 +740,7 @@ impl Output { }; frame.render_widget_ref( - Paragraph::new(Line::from(vec![ - if client.build.bundle == BundleFormat::Web { - "Serving at: ".gray() - } else { - "Server at: ".gray() - }, - address, - ])), + Paragraph::new(Line::from(vec!["Address: ".gray(), address])), serve_address, ); } @@ -823,14 +816,8 @@ impl Output { meta_list[3], ); - let server_address = match state.server.server_address() { - Some(address) => format!("http://{address}").yellow(), - None => "no address".dark_gray(), - }; - frame.render_widget( - Paragraph::new(Line::from(vec!["Network: ".gray(), server_address])), - meta_list[4], - ); + // space + frame.render_widget(Paragraph::new(Line::from(vec![" ".gray()])), meta_list[4]); let links_list: [_; 2] = Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(bottom); diff --git a/packages/cli/src/serve/server.rs b/packages/cli/src/serve/server.rs index 838ec9129..5ed32e06f 100644 --- a/packages/cli/src/serve/server.rs +++ b/packages/cli/src/serve/server.rs @@ -31,7 +31,7 @@ use serde::{Deserialize, Serialize}; use std::{ convert::Infallible, fs, io, - net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener}, + net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener, UdpSocket}, path::Path, sync::{Arc, RwLock}, time::Duration, @@ -54,7 +54,6 @@ use super::AppServer; /// and better tooling on the pages that we serve. pub(crate) struct WebServer { devserver_exposed_ip: IpAddr, - devserver_bind_ip: IpAddr, devserver_port: u16, proxied_port: Option, hot_reload_sockets: Vec, @@ -121,7 +120,6 @@ impl WebServer { Ok(Self { build_status, proxied_port, - devserver_bind_ip, devserver_exposed_ip, devserver_port, hot_reload_sockets: Default::default(), @@ -389,23 +387,40 @@ impl WebServer { } } - /// Get the address the server is running - showing 127.0.0.1 if the devserver is bound to 0.0.0.0 - /// This is designed this way to not confuse users who expect the devserver to be bound to localhost - /// ... which it is, but they don't know that 0.0.0.0 also serves localhost. + /// Get the address to display to the user as a clickable link. + /// + /// When bound to 0.0.0.0, resolves the machine's primary network address so the user + /// sees a real clickable URL instead of the unroutable 0.0.0.0. pub fn displayed_address(&self) -> Option { let mut address = self.server_address()?; // Set the port to the devserver port since that's usually what people expect address.set_port(self.devserver_port); - if self.devserver_bind_ip == IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)) { - address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), address.port()); + if address.ip().is_unspecified() { + match resolve_local_ip() { + Some(ip) => address.set_ip(ip), + None => { + tracing::debug!("Could not resolve local network IP; displaying localhost"); + address.set_ip(IpAddr::V4(Ipv4Addr::LOCALHOST)); + } + } } Some(address) } } +/// Resolve the machine's primary local IP by asking the OS which interface routes to the internet. +/// +/// Connects a UDP socket to a public address (no traffic is sent) and reads back the local address +/// the OS selected. Returns `None` if the lookup fails (e.g. no network connectivity). +fn resolve_local_ip() -> Option { + let socket = UdpSocket::bind("0.0.0.0:0").ok()?; + socket.connect("8.8.8.8:80").ok()?; + Some(socket.local_addr().ok()?.ip()) +} + async fn devserver_mainloop( https_cfg: WebHttpsConfig, listener: TcpListener,