huskies: merge 1129 story find_free_port fallback returns unbindable port silently when range is exhausted
This commit is contained in:
@@ -431,8 +431,13 @@ async fn handle_adopt_project(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Allocate ports and launch container ──────────────────────────────────
|
// ── Allocate ports and launch container ──────────────────────────────────
|
||||||
let port = find_free_port(3100);
|
let Some(port) = find_free_port(3100) else {
|
||||||
let ssh_port = find_free_port(2200);
|
return "No free port in range 3100–3200. Stop unused containers and retry.".to_string();
|
||||||
|
};
|
||||||
|
let Some(ssh_port) = find_free_port(2200) else {
|
||||||
|
return "No free SSH port in range 2200–2300. Stop unused containers and retry."
|
||||||
|
.to_string();
|
||||||
|
};
|
||||||
let container_url = format!("http://127.0.0.1:{port}");
|
let container_url = format!("http://127.0.0.1:{port}");
|
||||||
let container_name = format!("huskies-{name}");
|
let container_name = format!("huskies-{name}");
|
||||||
|
|
||||||
@@ -772,8 +777,17 @@ pub async fn handle_new_project(
|
|||||||
|
|
||||||
// ── Allocate ports and launch container ──────────────────────────────────
|
// ── Allocate ports and launch container ──────────────────────────────────
|
||||||
|
|
||||||
let port = find_free_port(3100);
|
let Some(port) = find_free_port(3100) else {
|
||||||
let ssh_port = find_free_port(2200);
|
let _ = tokio::fs::remove_dir_all(&host_path).await;
|
||||||
|
let _ = tokio::fs::remove_dir_all(&ssh_key_dir).await;
|
||||||
|
return "No free port in range 3100–3200. Stop unused containers and retry.".to_string();
|
||||||
|
};
|
||||||
|
let Some(ssh_port) = find_free_port(2200) else {
|
||||||
|
let _ = tokio::fs::remove_dir_all(&host_path).await;
|
||||||
|
let _ = tokio::fs::remove_dir_all(&ssh_key_dir).await;
|
||||||
|
return "No free SSH port in range 2200–2300. Stop unused containers and retry."
|
||||||
|
.to_string();
|
||||||
|
};
|
||||||
let container_url = format!("http://127.0.0.1:{port}");
|
let container_url = format!("http://127.0.0.1:{port}");
|
||||||
let container_name = format!("huskies-{name}");
|
let container_name = format!("huskies-{name}");
|
||||||
|
|
||||||
@@ -924,17 +938,21 @@ fn interpret_docker_run_error(stderr: &str, image: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Scan `start..start+range` for a bindable TCP port on 127.0.0.1.
|
||||||
|
///
|
||||||
|
/// Returns `Some(port)` for the first port that can be bound, or `None` if all
|
||||||
|
/// ports in the range are occupied.
|
||||||
|
fn find_free_port_in_range(start: u16, range: u16) -> Option<u16> {
|
||||||
|
(start..start.saturating_add(range))
|
||||||
|
.find(|&port| std::net::TcpListener::bind(("127.0.0.1", port)).is_ok())
|
||||||
|
}
|
||||||
|
|
||||||
/// Find a free TCP port by attempting to bind starting from `start`.
|
/// Find a free TCP port by attempting to bind starting from `start`.
|
||||||
///
|
///
|
||||||
/// Scans up to 100 ports above `start` and returns the first available one.
|
/// Scans up to 100 ports above `start` and returns `Some(port)` for the first
|
||||||
/// Falls back to `start` if none are found (unlikely in practice).
|
/// bindable one, or `None` when the entire range `start..start+100` is exhausted.
|
||||||
fn find_free_port(start: u16) -> u16 {
|
fn find_free_port(start: u16) -> Option<u16> {
|
||||||
for port in start..start + 100 {
|
find_free_port_in_range(start, 100)
|
||||||
if std::net::TcpListener::bind(("127.0.0.1", port)).is_ok() {
|
|
||||||
return port;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
start
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -1148,13 +1166,21 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn find_free_port_returns_bindable_port() {
|
fn find_free_port_returns_bindable_port() {
|
||||||
let port = find_free_port(2200);
|
let port = find_free_port(2200).expect("expected Some(port) in range 2200..2300");
|
||||||
// The returned port must be in range and actually bindable.
|
|
||||||
assert!((2200..2300).contains(&port));
|
assert!((2200..2300).contains(&port));
|
||||||
let listener = std::net::TcpListener::bind(("127.0.0.1", port));
|
let listener = std::net::TcpListener::bind(("127.0.0.1", port));
|
||||||
assert!(listener.is_ok(), "returned port {port} is not bindable");
|
assert!(listener.is_ok(), "returned port {port} is not bindable");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn find_free_port_exhausted_range_returns_none() {
|
||||||
|
// Bind the single port in a 1-wide scan window, then verify None is returned.
|
||||||
|
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
|
||||||
|
let port = listener.local_addr().unwrap().port();
|
||||||
|
// The range [port, port+1) contains only `port`, which is already bound.
|
||||||
|
assert_eq!(find_free_port_in_range(port, 1), None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn detect_stack_go_mod() {
|
fn detect_stack_go_mod() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user