use std::net::SocketAddr;
use super::ServerRunConfig;
pub(super) fn listener_url(
scheme: &str,
listener: &tokio::net::TcpListener,
run_cfg: &ServerRunConfig<'_>,
) -> Option<String> {
if !run_cfg.open {
return None;
}
match listener.local_addr() {
Ok(addr) => Some(browser_url(scheme, addr, run_cfg.open_path)),
Err(err) => {
tracing::warn!(%err, "failed to determine listener address for browser URL");
None
}
}
}
pub(super) fn open(url: String) {
let spawn_result = std::thread::Builder::new()
.name("sws-browser".to_owned())
.spawn(move || match open::that_detached(&url) {
Ok(()) => {
tracing::info!(%url, "opened server URL in web browser");
}
Err(err) => {
tracing::warn!(%url, %err, "failed to open server URL in web browser");
}
});
if let Err(err) = spawn_result {
tracing::warn!(%err, "failed to spawn browser launcher thread");
}
}
#[must_use]
fn browser_url(scheme: &str, addr: SocketAddr, path: Option<&str>) -> String {
let addr = if addr.ip().is_unspecified() {
format!("localhost:{}", addr.port())
} else {
addr.to_string()
};
match path {
Some(path) if path.starts_with('/') => format!("{scheme}://{addr}{path}"),
Some(path) => format!("{scheme}://{addr}/{path}"),
None => format!("{scheme}://{addr}/"),
}
}
#[cfg(test)]
mod tests {
use super::{ServerRunConfig, browser_url, listener_url};
#[tokio::test]
async fn disabled_browser_launch_has_no_url() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let run_cfg = ServerRunConfig {
addr_str: "127.0.0.1:0",
threads: 1,
open: false,
open_path: Some("/docs"),
conn: Default::default(),
};
assert_eq!(listener_url("http", &listener, &run_cfg), None);
}
#[tokio::test]
async fn enabled_browser_launch_uses_configured_path() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
let run_cfg = ServerRunConfig {
addr_str: "127.0.0.1:0",
threads: 1,
open: true,
open_path: Some("/docs"),
conn: Default::default(),
};
assert_eq!(
listener_url("http", &listener, &run_cfg),
Some(format!("http://127.0.0.1:{port}/docs"))
);
}
#[test]
fn ipv4_unspecified_address_uses_localhost() {
let addr = "0.0.0.0:8080".parse().unwrap();
assert_eq!(browser_url("http", addr, None), "http://localhost:8080/");
}
#[test]
fn ipv6_unspecified_address_uses_localhost() {
let addr = "[::]:8080".parse().unwrap();
assert_eq!(browser_url("http", addr, None), "http://localhost:8080/");
}
#[test]
fn https_unspecified_address_uses_localhost() {
let addr = "[::]:8443".parse().unwrap();
assert_eq!(browser_url("https", addr, None), "https://localhost:8443/");
}
#[test]
fn https_url_preserves_bound_port() {
let addr = "127.0.0.1:8443".parse().unwrap();
assert_eq!(browser_url("https", addr, None), "https://127.0.0.1:8443/");
}
#[test]
fn configured_path_is_appended_to_url() {
let addr = "127.0.0.1:8080".parse().unwrap();
assert_eq!(
browser_url("http", addr, Some("/docs")),
"http://127.0.0.1:8080/docs"
);
}
#[test]
fn configured_path_without_leading_slash_is_normalized() {
let addr = "127.0.0.1:8080".parse().unwrap();
assert_eq!(
browser_url("http", addr, Some("docs")),
"http://127.0.0.1:8080/docs"
);
}
}