Skip to main content

newgit_core/
ports.rs

1use std::collections::BTreeSet;
2use std::net::TcpListener;
3
4use crate::branch::BranchInstance;
5use crate::error::{NewgitError, Result};
6
7/// Ports already promised to instances. Binding records are the single
8/// source of truth: removing an instance frees its ports with no ledger.
9pub fn used_ports(branches: &[BranchInstance]) -> BTreeSet<u16> {
10    branches
11        .iter()
12        .flat_map(|branch| branch.resources.values())
13        .flat_map(|binding| binding.resolved_ports.values().copied())
14        .collect()
15}
16
17/// First port scanning up from `start` that is neither promised to another
18/// instance nor OS-unbindable right now. The chosen port is added to `used`
19/// so one allocation pass stays self-consistent.
20pub fn allocate(start: u16, used: &mut BTreeSet<u16>) -> Result<u16> {
21    let mut candidate = start;
22    loop {
23        if !used.contains(&candidate) && bindable(candidate) {
24            used.insert(candidate);
25            return Ok(candidate);
26        }
27        candidate = candidate.checked_add(1).ok_or_else(|| {
28            NewgitError::Unsupported(format!("no free port found scanning up from {start}"))
29        })?;
30        if candidate - start > 1000 {
31            return Err(NewgitError::Unsupported(format!(
32                "no free port found within 1000 of {start}"
33            )));
34        }
35    }
36}
37
38fn bindable(port: u16) -> bool {
39    TcpListener::bind(("127.0.0.1", port)).is_ok()
40}