aboutsummaryrefslogtreecommitdiff
path: root/src/api/client.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/api/client.rs')
-rw-r--r--src/api/client.rs97
1 files changed, 90 insertions, 7 deletions
diff --git a/src/api/client.rs b/src/api/client.rs
index 326ebc7d..15d3a884 100644
--- a/src/api/client.rs
+++ b/src/api/client.rs
@@ -1,7 +1,7 @@
use std::fmt;
-use std::io::{self, BufRead, BufReader, Write};
+use std::io::{self, BufRead, BufReader, Read, Write};
use std::path::PathBuf;
-use std::time::Duration;
+use std::time::{Duration, Instant};
use interprocess::local_socket::traits::Stream as _;
use serde::de::DeserializeOwned;
@@ -75,10 +75,37 @@ impl ApiClient {
}
pub fn status(&self) -> Result<crate::api::RuntimeStatus, ApiClientError> {
- let response = self.request(Request {
+ self.read_status(None)
+ }
+
+ pub(crate) fn status_with_timeout(
+ &self,
+ timeout: Duration,
+ ) -> Result<crate::api::RuntimeStatus, ApiClientError> {
+ self.read_status(Some(timeout))
+ }
+
+ fn read_status(
+ &self,
+ timeout: Option<Duration>,
+ ) -> Result<crate::api::RuntimeStatus, ApiClientError> {
+ let request = Request {
id: "api-client:status".into(),
method: Method::Ping(PingParams::default()),
- })?;
+ };
+ let response = match timeout {
+ Some(timeout) => {
+ let mut stream = self.connect()?;
+ write_request(&mut stream, &request)?;
+ crate::ipc::set_local_stream_polling(&mut stream, true)?;
+ let mut reader = BufReader::new(DeadlineReader {
+ stream: &mut stream,
+ deadline: Instant::now() + timeout,
+ });
+ parse_response_value(read_json_line(&mut reader)?)?
+ }
+ None => self.request(request)?,
+ };
match response.result {
ResponseResult::Pong {
version,
@@ -162,9 +189,37 @@ fn write_request(stream: &mut LocalStream, request: &Request) -> Result<(), ApiC
Ok(())
}
-fn read_json_line<T: DeserializeOwned>(
- reader: &mut BufReader<LocalStream>,
-) -> Result<T, ApiClientError> {
+struct DeadlineReader<'a> {
+ stream: &'a mut LocalStream,
+ deadline: Instant,
+}
+
+impl Read for DeadlineReader<'_> {
+ fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
+ if buffer.is_empty() {
+ return Ok(0);
+ }
+ loop {
+ if Instant::now() >= self.deadline {
+ return Err(io::Error::new(
+ io::ErrorKind::TimedOut,
+ "server status probe timed out",
+ ));
+ }
+ // Windows named pipes have no read timeout; peek-before-read keeps
+ // both idle and partial responses subject to the same deadline.
+ match crate::ipc::poll_local_stream_read_count(self.stream, buffer)? {
+ crate::ipc::LocalStreamReadCount::Data(count) => return Ok(count),
+ crate::ipc::LocalStreamReadCount::Closed => return Ok(0),
+ crate::ipc::LocalStreamReadCount::Pending => {
+ std::thread::sleep(Duration::from_millis(2))
+ }
+ }
+ }
+ }
+}
+
+fn read_json_line<T: DeserializeOwned>(reader: &mut impl BufRead) -> Result<T, ApiClientError> {
let mut line = String::new();
let read = reader.read_line(&mut line)?;
if read == 0 || line.trim().is_empty() {
@@ -200,6 +255,34 @@ mod tests {
}
#[test]
+ fn status_timeout_closes_a_stalled_probe() {
+ use interprocess::local_socket::traits::Listener as _;
+ let path =
+ std::env::temp_dir().join(format!("herdr-status-timeout-{}.sock", std::process::id()));
+ let listener = crate::ipc::bind_private_local_listener(&path).unwrap();
+ let server = std::thread::spawn(move || {
+ let stream = listener.accept().unwrap();
+ let mut reader = BufReader::new(stream);
+ let mut line = String::new();
+ reader.read_line(&mut line).unwrap();
+ assert_eq!(
+ serde_json::from_str::<serde_json::Value>(&line).unwrap()["method"],
+ "ping"
+ );
+ std::thread::sleep(Duration::from_millis(300));
+ });
+ let client = ApiClient::for_target(ConnectionTarget::SocketPath(path.clone()));
+ let error = client
+ .status_with_timeout(Duration::from_millis(100))
+ .unwrap_err();
+ assert!(
+ matches!(error, ApiClientError::Io(error) if matches!(error.kind(), io::ErrorKind::TimedOut | io::ErrorKind::WouldBlock))
+ );
+ server.join().unwrap();
+ std::fs::remove_file(path).unwrap();
+ }
+
+ #[test]
fn socket_path_target_uses_explicit_path() {
let path = PathBuf::from("/tmp/herdr-test.sock");
let client = ApiClient::for_target(ConnectionTarget::SocketPath(path.clone()));