aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-07 02:03:39 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-07 16:29:45 +0300
commitf61ed48eff622de4f970adff220603ea50292f07 (patch)
tree1d7028b60a137e6f2e67c3c5435788a11b974344
parent52e57507e10ee7cb2869156fb1f00e280e614736 (diff)
fix: preserve unix socket error semantics
-rw-r--r--src/client/mod.rs43
-rw-r--r--src/ipc.rs32
-rw-r--r--src/server/client_transport.rs45
3 files changed, 99 insertions, 21 deletions
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<Duration>,
+ 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<Duration>,
+ _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<SocketFileIdentity> {
#[cfg(windows)]
{
@@ -159,12 +159,25 @@ 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");
let _ = fs::remove_file(&path);
@@ -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<Duration>,
+ 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<Duration>,
+ _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::<Vec<u8>>();