fix(windows): fall back to OS-assigned port when Hyper-V blocks daemon TCP bind (#1041)

On Windows the daemon derives a TCP port from the session name via a
djb2 hash (e.g. "default" → 50838). On many machines this port falls
inside Hyper-V's excluded port range (winnat), causing EACCES on bind
and preventing the daemon from starting.

Changes:
- daemon: try the hash-derived port first; on failure, bind to port 0
  (OS-assigned) and write the actual port to the .port file
- client (connection.rs, stream.rs): read the .port file to discover the
  daemon's actual port, falling back to the hash if the file is absent
- run_daemon: guard .sock file operations with #[cfg(unix)] and add
  .port file cleanup for #[cfg(windows)]

Fixes #390

Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
This commit is contained in:
Chris Tate
2026-03-26 21:01:06 -07:00
committed by GitHub
co-authored by ctate
parent 995a47fdb0
commit a95bc0f75a
3 changed files with 48 additions and 12 deletions
+15 -3
View File
@@ -154,6 +154,18 @@ pub fn get_port_for_session(session: &str) -> u16 {
49152 + ((hash.unsigned_abs() as u32 % 16383) as u16)
}
/// Read the actual daemon port from the `.port` file written by the daemon.
/// Falls back to the hash-derived port if the file does not exist or is
/// unreadable (e.g. daemon has not started yet).
#[cfg(windows)]
pub fn resolve_port(session: &str) -> u16 {
let port_path = get_port_path(session);
fs::read_to_string(&port_path)
.ok()
.and_then(|s| s.trim().parse::<u16>().ok())
.unwrap_or_else(|| get_port_for_session(session))
}
pub fn daemon_ready(session: &str) -> bool {
#[cfg(unix)]
{
@@ -162,7 +174,7 @@ pub fn daemon_ready(session: &str) -> bool {
}
#[cfg(windows)]
{
let port = get_port_for_session(session);
let port = resolve_port(session);
TcpStream::connect_timeout(
&format!("127.0.0.1:{}", port).parse().unwrap(),
Duration::from_millis(50),
@@ -442,7 +454,7 @@ pub fn ensure_daemon(session: &str, opts: &DaemonOptions) -> Result<DaemonResult
get_socket_dir().join(format!("{}.sock", session)).display()
);
#[cfg(windows)]
let endpoint_info = format!("port: 127.0.0.1:{}", get_port_for_session(session));
let endpoint_info = format!("port: 127.0.0.1:{}", resolve_port(session));
Err(format!("Daemon failed to start ({})", endpoint_info))
}
@@ -457,7 +469,7 @@ fn connect(session: &str) -> Result<Connection, String> {
}
#[cfg(windows)]
{
let port = get_port_for_session(session);
let port = resolve_port(session);
TcpStream::connect(format!("127.0.0.1:{}", port))
.map(Connection::Tcp)
.map_err(|e| format!("Failed to connect: {}", e))