macOS refuses every socket option with EINVAL once the peer has closed (XNU sosetoptlock, bsd/kern/uipc_socket.c), even with unread data still buffered. loopd set a read timeout before each read, so on macOS every frame or response that arrived just before the peer closed was lost: BrokerPort reported the broker unavailable with "os error 22", and the llama client failed the same way. Twelve loopd test binaries failed on macOS; Linux never refuses, so the gate on Talos did not see it. Both places now go through socket::set_read_timeout, which on Apple targets takes that one refusal as success: a socket shut in both directions returns its data or the end at once and cannot block. A zero timeout is still an error. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
22 lines
953 B
Rust
22 lines
953 B
Rust
//! Unix socket options that behave the same on Linux and macOS.
|
|
|
|
use std::io;
|
|
use std::os::unix::net::UnixStream;
|
|
use std::time::Duration;
|
|
|
|
/// `EINVAL` on Darwin (`sys/errno.h`); std does not name it and a crate for one number is too much.
|
|
#[cfg(target_vendor = "apple")]
|
|
const EINVAL: i32 = 22;
|
|
|
|
/// Set the read timeout for the next read. macOS refuses every socket option with `EINVAL` once
|
|
/// the peer has closed (XNU `sosetoptlock`: both `SS_CANTRCVMORE` and `SS_CANTSENDMORE` set), even
|
|
/// with unread data still buffered. A read on such a socket returns that data or the end at once
|
|
/// and cannot block, so there is no timeout left to set and the refusal is not an error.
|
|
pub fn set_read_timeout(stream: &UnixStream, timeout: Duration) -> io::Result<()> {
|
|
match stream.set_read_timeout(Some(timeout)) {
|
|
#[cfg(target_vendor = "apple")]
|
|
Err(e) if e.raw_os_error() == Some(EINVAL) => Ok(()),
|
|
other => other,
|
|
}
|
|
}
|