diff --git a/cli/src/connection.rs b/cli/src/connection.rs index d734f9d..f423164 100644 --- a/cli/src/connection.rs +++ b/cli/src/connection.rs @@ -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::().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 Result { } #[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)) diff --git a/cli/src/native/daemon.rs b/cli/src/native/daemon.rs index d1699d3..99e1f97 100644 --- a/cli/src/native/daemon.rs +++ b/cli/src/native/daemon.rs @@ -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 = if stream_server.is_some() { Some(socket_dir.join(format!("{}.stream", session))) diff --git a/cli/src/native/stream.rs b/cli/src/native/stream.rs index 31cd98f..21e556f 100644 --- a/cli/src/native/stream.rs +++ b/cli/src/native/stream.rs @@ -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