use crate::broker::protocol::framing::MAX_FRAME_BYTES;
use crate::client::paths;
use interprocess::local_socket::Stream;
use std::io::{self, Read};
use std::sync::mpsc;
use std::thread;
use std::time::{Duration, Instant};
const NONBLOCKING_POLL_INTERVAL: Duration = Duration::from_millis(5);
const DEFAULT_RPC_TIMEOUT: Duration = Duration::from_secs(30);
const RPC_TIMEOUT_ENV: &str = "RUNNING_PROCESS_CLIENT_RPC_TIMEOUT_MS";
pub(crate) fn rpc_read_deadline() -> Instant {
let timeout = std::env::var(RPC_TIMEOUT_ENV)
.ok()
.and_then(|raw| raw.trim().parse::<u64>().ok())
.filter(|&ms| ms > 0)
.map(Duration::from_millis)
.unwrap_or(DEFAULT_RPC_TIMEOUT);
Instant::now() + timeout
}
fn wait_for_io(deadline: Instant) -> io::Result<()> {
let now = Instant::now();
if now >= deadline {
return Err(io::Error::new(
io::ErrorKind::TimedOut,
"daemon response read timed out; the daemon accepted the \
connection but never sent a complete reply",
));
}
thread::sleep((deadline - now).min(NONBLOCKING_POLL_INTERVAL));
Ok(())
}
pub(crate) fn read_exact_with_deadline<R: Read>(
reader: &mut R,
mut buf: &mut [u8],
deadline: Instant,
) -> io::Result<()> {
while !buf.is_empty() {
match reader.read(buf) {
Ok(0) => wait_for_io(deadline)?,
Ok(read) => {
let tmp = buf;
buf = &mut tmp[read..];
}
Err(err) if err.kind() == io::ErrorKind::WouldBlock => wait_for_io(deadline)?,
Err(err) if err.kind() == io::ErrorKind::Interrupted => continue,
Err(err) => return Err(err),
}
}
Ok(())
}
pub(crate) fn write_all_with_deadline<W: io::Write>(
writer: &mut W,
mut buf: &[u8],
deadline: Instant,
) -> io::Result<()> {
while !buf.is_empty() {
match writer.write(buf) {
Ok(0) => wait_for_io(deadline)?,
Ok(written) => buf = &buf[written..],
Err(err) if err.kind() == io::ErrorKind::WouldBlock => wait_for_io(deadline)?,
Err(err) if err.kind() == io::ErrorKind::Interrupted => continue,
Err(err) => return Err(err),
}
}
loop {
match writer.flush() {
Ok(()) => return Ok(()),
Err(err) if err.kind() == io::ErrorKind::WouldBlock => wait_for_io(deadline)?,
Err(err) if err.kind() == io::ErrorKind::Interrupted => continue,
Err(err) => return Err(err),
}
}
}
pub(crate) fn read_frame_with_deadline<R: Read>(
reader: &mut R,
deadline: Instant,
) -> io::Result<Vec<u8>> {
let mut len_buf = [0u8; 4];
read_exact_with_deadline(reader, &mut len_buf, deadline)?;
let len = u32::from_be_bytes(len_buf) as usize;
check_frame_len(len)?;
let mut buf = vec![0u8; len];
if len > 0 {
read_exact_with_deadline(reader, &mut buf, deadline)?;
}
Ok(buf)
}
const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
const CONNECT_TIMEOUT_ENV: &str = "RUNNING_PROCESS_CLIENT_CONNECT_TIMEOUT_MS";
fn connect_timeout() -> Duration {
std::env::var(CONNECT_TIMEOUT_ENV)
.ok()
.and_then(|raw| raw.trim().parse::<u64>().ok())
.map(Duration::from_millis)
.unwrap_or(DEFAULT_CONNECT_TIMEOUT)
}
pub(crate) fn connect_with_timeout(socket_path: &str) -> io::Result<Stream> {
let path = socket_path.to_string();
let (tx, rx) = mpsc::channel();
thread::Builder::new()
.name("rp-client-connect".to_string())
.spawn(move || {
use interprocess::local_socket::traits::Stream as _;
let result = match paths::make_socket_name(&path) {
Ok(name) => Stream::connect(name),
Err(err) => Err(err),
};
let _ = tx.send(result);
})?;
match rx.recv_timeout(connect_timeout()) {
Ok(result) => result,
Err(_) => Err(io::Error::new(
io::ErrorKind::TimedOut,
format!(
"daemon socket connect timed out after {:?}: the socket \
exists but the server never accepted the connection \
(path {socket_path})",
connect_timeout()
),
)),
}
}
pub(crate) fn check_frame_len(len: usize) -> io::Result<()> {
if len > MAX_FRAME_BYTES {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("daemon frame length {len} exceeds cap {MAX_FRAME_BYTES}"),
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn check_frame_len_accepts_within_cap() {
assert!(check_frame_len(0).is_ok());
assert!(check_frame_len(MAX_FRAME_BYTES).is_ok());
}
#[test]
fn check_frame_len_rejects_over_cap() {
let err = check_frame_len(MAX_FRAME_BYTES + 1).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
}
#[test]
fn check_frame_len_rejects_bogus_u32_max() {
assert!(check_frame_len(u32::MAX as usize).is_err());
}
#[test]
fn connect_with_timeout_errors_on_missing_socket() {
let bogus = if cfg!(windows) {
r"\\.\pipe\running-process-nonexistent-test-endpoint"
} else {
"/tmp/running-process-nonexistent-test-endpoint.sock"
};
assert!(connect_with_timeout(bogus).is_err());
}
struct NeverReady;
impl Read for NeverReady {
fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
Err(io::Error::new(io::ErrorKind::WouldBlock, "never ready"))
}
}
#[test]
fn read_exact_with_deadline_times_out_instead_of_hanging() {
let mut reader = NeverReady;
let mut buf = [0u8; 4];
let start = Instant::now();
let err = read_exact_with_deadline(
&mut reader,
&mut buf,
Instant::now() + Duration::from_millis(100),
)
.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::TimedOut);
assert!(start.elapsed() < Duration::from_secs(2));
}
#[test]
fn read_frame_with_deadline_times_out_on_silent_peer() {
let mut reader = NeverReady;
let start = Instant::now();
let err =
read_frame_with_deadline(&mut reader, Instant::now() + Duration::from_millis(100))
.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::TimedOut);
assert!(start.elapsed() < Duration::from_secs(2));
}
#[test]
fn read_frame_with_deadline_reads_a_complete_frame() {
let payload = b"hello-daemon";
let mut framed = Vec::new();
framed.extend_from_slice(&(payload.len() as u32).to_be_bytes());
framed.extend_from_slice(payload);
let mut cursor = io::Cursor::new(framed);
let got =
read_frame_with_deadline(&mut cursor, Instant::now() + Duration::from_secs(1)).unwrap();
assert_eq!(got, payload);
}
#[test]
fn read_frame_with_deadline_rejects_oversized_length_prefix() {
let mut framed = vec![0xFF_u8; 4];
framed.extend_from_slice(b"body");
let mut cursor = io::Cursor::new(framed);
let err = read_frame_with_deadline(&mut cursor, Instant::now() + Duration::from_secs(1))
.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
}
}