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:
+15
-3
@@ -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))
|
||||
|
||||
@@ -25,12 +25,20 @@ pub async fn run_daemon(session: &str) {
|
||||
let pid_path = socket_dir.join(format!("{}.pid", session));
|
||||
let _ = fs::write(&pid_path, process::id().to_string());
|
||||
|
||||
// On Unix the daemon listens on a Unix domain socket; on Windows it uses
|
||||
// TCP, so there is no .sock file — only a .port file written by the server.
|
||||
let socket_path = socket_dir.join(format!("{}.sock", session));
|
||||
|
||||
#[cfg(unix)]
|
||||
if socket_path.exists() {
|
||||
let _ = fs::remove_file(&socket_path);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let _ = fs::remove_file(socket_dir.join(format!("{}.port", session)));
|
||||
}
|
||||
|
||||
let stream_path = socket_dir.join(format!("{}.stream", session));
|
||||
let _ = fs::remove_file(&stream_path);
|
||||
let _ = fs::remove_file(socket_dir.join(format!("{}.engine", session)));
|
||||
@@ -79,7 +87,14 @@ pub async fn run_daemon(session: &str) {
|
||||
)
|
||||
.await;
|
||||
|
||||
let _ = fs::remove_file(&socket_path);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let _ = fs::remove_file(&socket_path);
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let _ = fs::remove_file(socket_dir.join(format!("{}.port", session)));
|
||||
}
|
||||
let _ = fs::remove_file(&pid_path);
|
||||
let _ = fs::remove_file(&stream_path);
|
||||
let _ = fs::remove_file(socket_dir.join(format!("{}.engine", session)));
|
||||
@@ -206,14 +221,23 @@ async fn run_socket_server(
|
||||
) -> Result<(), String> {
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
let port = get_port_for_session(session);
|
||||
let listener = TcpListener::bind(format!("127.0.0.1:{}", port))
|
||||
.await
|
||||
.map_err(|e| format!("Failed to bind TCP: {}", e))?;
|
||||
let preferred_port = get_port_for_session(session);
|
||||
// Try the hash-derived port first; if it is blocked (e.g. Windows Hyper-V
|
||||
// excluded port range), fall back to an OS-assigned ephemeral port.
|
||||
let listener = match TcpListener::bind(format!("127.0.0.1:{}", preferred_port)).await {
|
||||
Ok(l) => l,
|
||||
Err(_) => TcpListener::bind("127.0.0.1:0")
|
||||
.await
|
||||
.map_err(|e| format!("Failed to bind TCP: {}", e))?,
|
||||
};
|
||||
let actual_port = listener
|
||||
.local_addr()
|
||||
.map_err(|e| format!("Failed to get local address: {}", e))?
|
||||
.port();
|
||||
|
||||
let socket_dir = socket_path.parent().unwrap_or(std::path::Path::new("."));
|
||||
let port_path = socket_dir.join(format!("{}.port", session));
|
||||
let _ = fs::write(&port_path, port.to_string());
|
||||
let _ = fs::write(&port_path, actual_port.to_string());
|
||||
|
||||
let stream_file: Option<PathBuf> = if stream_server.is_some() {
|
||||
Some(socket_dir.join(format!("{}.stream", session)))
|
||||
|
||||
@@ -10,9 +10,9 @@ use tokio::sync::{broadcast, watch, Mutex, Notify, RwLock};
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
|
||||
use super::cdp::client::CdpClient;
|
||||
#[cfg(windows)]
|
||||
use crate::connection::get_port_for_session;
|
||||
use crate::connection::get_socket_dir;
|
||||
#[cfg(windows)]
|
||||
use crate::connection::resolve_port;
|
||||
use crate::install::get_dashboard_dir;
|
||||
|
||||
/// Frame metadata from CDP Page.screencastFrame events.
|
||||
@@ -1213,7 +1213,7 @@ async fn relay_command_to_daemon(session_name: &str, body: &str) -> Result<Strin
|
||||
|
||||
#[cfg(windows)]
|
||||
let stream = {
|
||||
let port = get_port_for_session(session_name);
|
||||
let port = resolve_port(session_name);
|
||||
tokio::net::TcpStream::connect(format!("127.0.0.1:{}", port))
|
||||
.await
|
||||
.map_err(|e| format!("Failed to connect to daemon: {}", e))?
|
||||
|
||||
Reference in New Issue
Block a user