diff --git a/src/client/mod.rs b/src/client/mod.rs index a895dae0..8236bb44 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -448,6 +448,33 @@ fn requested_keybindings() -> ClientKeybindings { } } +#[cfg(windows)] +fn set_handshake_recv_timeout( + stream: &LocalStream, + timeout: Option, + context: &'static str, +) -> Result<(), ClientError> { + match stream.set_recv_timeout(timeout) { + Ok(()) => Ok(()), + Err(err) if err.kind() == io::ErrorKind::Unsupported => { + debug!(err = %err, context, "client socket receive timeout unavailable"); + Ok(()) + } + Err(err) => Err(ClientError::ConnectionFailed(err)), + } +} + +#[cfg(not(windows))] +fn set_handshake_recv_timeout( + stream: &LocalStream, + timeout: Option, + _context: &'static str, +) -> Result<(), ClientError> { + stream + .set_recv_timeout(timeout) + .map_err(ClientError::ConnectionFailed) +} + /// Performs the client→server handshake. /// /// Sends Hello with the terminal size and protocol version, reads the Welcome @@ -484,13 +511,17 @@ fn do_handshake( .map_err(|e| ClientError::ConnectionFailed(io::Error::other(e.to_string())))?; // Read Welcome. - if let Err(err) = stream.set_recv_timeout(Some(Duration::from_secs(5))) { - debug!(err = %err, "client handshake read timeout unavailable"); - } + set_handshake_recv_timeout( + stream, + Some(Duration::from_secs(5)), + "client handshake read timeout unavailable", + )?; let welcome: ServerMessage = protocol::read_message(stream, MAX_FRAME_SIZE)?; - if let Err(err) = stream.set_recv_timeout(None) { - debug!(err = %err, "failed to clear client handshake read timeout"); - } + set_handshake_recv_timeout( + stream, + None, + "failed to clear client handshake read timeout", + )?; match welcome { ServerMessage::Welcome { diff --git a/src/ipc.rs b/src/ipc.rs index 5c1fe1b7..4c92fec8 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -79,14 +79,7 @@ pub(crate) fn prepare_socket_path( Ok(_) => { return Err(io::Error::new(io::ErrorKind::AddrInUse, busy_message(path))); } - Err(err) - if matches!( - err.kind(), - io::ErrorKind::ConnectionRefused - | io::ErrorKind::NotFound - | io::ErrorKind::TimedOut - | io::ErrorKind::WouldBlock - ) => {} + Err(err) if stale_socket_connect_error(err.kind()) => {} Err(err) => return Err(err), } @@ -99,6 +92,13 @@ pub(crate) fn prepare_socket_path( Ok(()) } +fn stale_socket_connect_error(kind: io::ErrorKind) -> bool { + matches!( + kind, + io::ErrorKind::ConnectionRefused | io::ErrorKind::NotFound | io::ErrorKind::TimedOut + ) || (cfg!(windows) && kind == io::ErrorKind::WouldBlock) +} + pub(crate) fn socket_file_identity(path: &Path) -> io::Result { #[cfg(windows)] { @@ -159,11 +159,24 @@ pub(crate) fn restrict_socket_permissions(_path: &Path, _mode: u32) -> io::Resul Ok(()) } -#[cfg(all(test, windows))] +#[cfg(test)] mod tests { use super::*; + #[cfg(windows)] use std::path::PathBuf; + #[test] + fn stale_socket_connect_errors_keep_unix_would_block_strict() { + assert!(stale_socket_connect_error(io::ErrorKind::ConnectionRefused)); + assert!(stale_socket_connect_error(io::ErrorKind::NotFound)); + assert!(stale_socket_connect_error(io::ErrorKind::TimedOut)); + assert_eq!( + stale_socket_connect_error(io::ErrorKind::WouldBlock), + cfg!(windows) + ); + } + + #[cfg(windows)] #[test] fn remove_socket_file_if_owned_compares_windows_marker_contents() { let path = temp_socket_marker_path("same-len-marker"); @@ -180,6 +193,7 @@ mod tests { let _ = fs::remove_file(&path); } + #[cfg(windows)] fn temp_socket_marker_path(name: &str) -> PathBuf { std::env::temp_dir().join(format!("herdr-{name}-{}.sock", std::process::id())) } diff --git a/src/server/client_transport.rs b/src/server/client_transport.rs index 4bbf7bbf..b7e19bb3 100644 --- a/src/server/client_transport.rs +++ b/src/server/client_transport.rs @@ -153,6 +153,33 @@ fn input_events_within_limits(events: &[ClientInputEvent]) -> bool { true } +#[cfg(windows)] +fn set_client_recv_timeout( + stream: &LocalStream, + timeout: Option, + context: &'static str, + client_id: u64, +) -> io::Result<()> { + match stream.set_recv_timeout(timeout) { + Ok(()) => Ok(()), + Err(err) if err.kind() == io::ErrorKind::Unsupported => { + debug!(client_id, err = %err, context, "client socket receive timeout unavailable"); + Ok(()) + } + Err(err) => Err(err), + } +} + +#[cfg(not(windows))] +fn set_client_recv_timeout( + stream: &LocalStream, + timeout: Option, + _context: &'static str, + _client_id: u64, +) -> io::Result<()> { + stream.set_recv_timeout(timeout) +} + /// Handles the client handshake on a blocking thread. /// /// Reads the `Hello` message, validates the version, sends `Welcome`, @@ -167,9 +194,12 @@ pub(crate) fn handle_client_handshake( // the handshake thread needs blocking I/O for read_message/write_message. stream.set_nonblocking(false)?; - if let Err(err) = stream.set_recv_timeout(Some(HANDSHAKE_TIMEOUT)) { - debug!(client_id, err = %err, "client handshake read timeout unavailable"); - } + set_client_recv_timeout( + &stream, + Some(HANDSHAKE_TIMEOUT), + "client handshake read timeout unavailable", + client_id, + )?; // Read the Hello message. let hello: ClientMessage = match protocol::read_message(&mut stream, MAX_FRAME_SIZE) { @@ -268,9 +298,12 @@ pub(crate) fn handle_client_handshake( }; protocol::write_message(&mut stream, &welcome).map_err(|e| io::Error::other(e.to_string()))?; - if let Err(err) = stream.set_recv_timeout(None) { - debug!(client_id, err = %err, "failed to clear client handshake read timeout"); - } + set_client_recv_timeout( + &stream, + None, + "failed to clear client handshake read timeout", + client_id, + )?; // Create separate channels for reliable control messages and droppable renders. let (control_tx, control_rx) = std::sync::mpsc::channel::>();