use std::net::TcpStream;
use std::time::Duration;
use super::{
AnyError, DEFAULT_LOCAL_PORT, ResolvedServer, configured_source, load_managed, verify_health,
};
fn local_candidate_ports() -> Vec<u16> {
let mut ports = Vec::new();
let mut push = |port: u16| {
if port != 0 && !ports.contains(&port) {
ports.push(port);
}
};
if let Some(port) = std::env::var("ROUTER_PORT")
.ok()
.and_then(|value| value.trim().parse::<u16>().ok())
{
push(port);
}
push(DEFAULT_LOCAL_PORT);
if let Ok(Some(state)) = load_managed() {
push(state.port);
}
for port in published_container_ports() {
push(port);
}
ports
}
fn published_container_ports() -> Vec<u16> {
let Ok(output) = std::process::Command::new("docker")
.args(["ps", "--format", "{{.Ports}}"])
.stderr(std::process::Stdio::null())
.output()
else {
return Vec::new();
};
if !output.status.success() {
return Vec::new();
}
parse_published_ports(&String::from_utf8_lossy(&output.stdout))
}
fn parse_published_ports(listing: &str) -> Vec<u16> {
let mut ports = Vec::new();
for mapping in listing
.split([',', '\n'])
.map(str::trim)
.filter(|mapping| !mapping.is_empty())
{
let Some((host, _)) = mapping.split_once("->") else {
continue;
};
let Some((address, port)) = host.rsplit_once(':') else {
continue;
};
if !address.is_empty()
&& !address.contains("127.0.0.1")
&& !address.contains("0.0.0.0")
&& !address.contains("[::]")
{
continue;
}
if let Ok(port) = port.trim().parse::<u16>()
&& port != 0
&& !ports.contains(&port)
{
ports.push(port);
}
}
ports
}
pub(super) async fn discover_local_router(force_managed: bool) -> Option<String> {
if force_managed {
return None;
}
if matches!(load_managed(), Ok(Some(_))) {
return None;
}
for port in local_candidate_ports() {
if !port_accepts(port) {
continue;
}
let base_url = format!("http://127.0.0.1:{port}");
if verify_health(&base_url).await.is_ok() {
return Some(base_url);
}
}
None
}
fn port_accepts(port: u16) -> bool {
let address = std::net::SocketAddr::from(([127, 0, 0, 1], port));
TcpStream::connect_timeout(&address, Duration::from_millis(150)).is_ok()
}
pub async fn discovered_local_router() -> Option<ResolvedServer> {
let base_url = discover_local_router(false).await?;
let token = std::env::var("LINK_ASSISTANT_ROUTER_TOKEN")
.or_else(|_| std::env::var("LINK_ASSISTANT_TOKEN"))
.ok();
Some(ResolvedServer::at(
base_url,
token,
"already-running local server",
))
}
pub async fn effective_source() -> Result<String, AnyError> {
let configured = configured_source()?;
if configured != "managed local container" {
return Ok(configured);
}
Ok(discover_local_router(false)
.await
.map_or(configured, |url| {
format!("already-running local server: {url}")
}))
}
#[cfg(test)]
#[path = "discovery_tests.rs"]
mod tests;