fix: preserve unix socket error semantics

This commit is contained in:
Ogulcan Celik
2026-06-07 16:29:45 +03:00
parent 52e57507e1
commit f61ed48eff
3 changed files with 99 additions and 21 deletions
+37 -6
View File
@@ -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 {
+23 -9
View File
@@ -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,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()))
}
+39 -6
View File
@@ -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>>();