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 ──────────────────────────────────
|
||||
let port = find_free_port(3100);
|
||||
let ssh_port = find_free_port(2200);
|
||||
let Some(port) = find_free_port(3100) else {
|
||||
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_name = format!("huskies-{name}");
|
||||
|
||||
@@ -772,8 +777,17 @@ pub async fn handle_new_project(
|
||||
|
||||
// ── Allocate ports and launch container ──────────────────────────────────
|
||||
|
||||
let port = find_free_port(3100);
|
||||
let ssh_port = find_free_port(2200);
|
||||
let Some(port) = find_free_port(3100) else {
|
||||
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_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`.
|
||||
///
|
||||
/// Scans up to 100 ports above `start` and returns the first available one.
|
||||
/// Falls back to `start` if none are found (unlikely in practice).
|
||||
fn find_free_port(start: u16) -> u16 {
|
||||
for port in start..start + 100 {
|
||||
if std::net::TcpListener::bind(("127.0.0.1", port)).is_ok() {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
start
|
||||
/// Scans up to 100 ports above `start` and returns `Some(port)` for the first
|
||||
/// bindable one, or `None` when the entire range `start..start+100` is exhausted.
|
||||
fn find_free_port(start: u16) -> Option<u16> {
|
||||
find_free_port_in_range(start, 100)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1148,13 +1166,21 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn find_free_port_returns_bindable_port() {
|
||||
let port = find_free_port(2200);
|
||||
// The returned port must be in range and actually bindable.
|
||||
let port = find_free_port(2200).expect("expected Some(port) in range 2200..2300");
|
||||
assert!((2200..2300).contains(&port));
|
||||
let listener = std::net::TcpListener::bind(("127.0.0.1", port));
|
||||
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]
|
||||
fn detect_stack_go_mod() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user