use std::io;
use std::time::Duration;
use prost::Message as _;
use running_process_probe::probe_diag::v1::{
probe_envelope::Body, CaptureReply, CaptureStackRequest, Heartbeat, ProbeEnvelope, ProcessKey,
RegisterProcess, RegistrationStatus, UnregisterProcess,
};
use crate::broker::protocol::framing::{read_frame_with_cap, write_frame, MAX_FRAME_BYTES};
const MAX_REPLY_BYTES: usize = 64 * 1024;
#[derive(Debug, thiserror::Error)]
pub enum ClientError {
#[error("probe daemon unreachable: {0}")]
Unreachable(#[source] io::Error),
#[error("probe wire error: {0}")]
Wire(String),
#[error("probe daemon refused the request: {reason}")]
Refused {
reason: String,
},
#[error("unexpected reply from probe daemon")]
UnexpectedReply,
#[error("probe daemon reply is out of order: expected request {expected}, got {got}")]
Desync {
expected: u64,
got: u64,
},
}
#[derive(Clone, Debug, PartialEq)]
pub enum HeartbeatWork {
Idle,
Capture(CaptureStackRequest),
}
pub trait ProbeClient: Send {
fn register(&mut self, req: &RegisterProcess) -> Result<ProcessKey, ClientError>;
fn heartbeat(&mut self, key: &ProcessKey) -> Result<HeartbeatWork, ClientError>;
fn submit_capture(&mut self, reply: CaptureReply) -> Result<(), ClientError>;
fn unregister(&mut self, key: &ProcessKey) -> Result<(), ClientError>;
}
#[derive(Debug)]
pub struct SocketProbeClient {
stream: crate::platform::ipc::Stream,
request_id: u64,
}
impl SocketProbeClient {
pub fn connect(socket_path: &str, deadline: Duration) -> Result<Self, ClientError> {
let endpoint = crate::platform::ipc::Endpoint::new(socket_path.to_owned())
.map_err(|e| ClientError::Wire(format!("socket name: {e}")))?;
let stream =
crate::platform::ipc::Stream::connect(&endpoint).map_err(ClientError::Unreachable)?;
stream
.set_recv_timeout(Some(deadline))
.map_err(ClientError::Unreachable)?;
Ok(Self {
stream,
request_id: 0,
})
}
fn next_request_id(&mut self) -> u64 {
self.request_id = self.request_id.wrapping_add(1);
self.request_id
}
fn round_trip(&mut self, body: Body) -> Result<ProbeEnvelope, ClientError> {
let request_id = self.next_request_id();
round_trip_on(&mut self.stream, request_id, body)
}
}
fn round_trip_on<S: io::Read + io::Write>(
stream: &mut S,
request_id: u64,
body: Body,
) -> Result<ProbeEnvelope, ClientError> {
let envelope = ProbeEnvelope {
wire_version: 1,
request_id,
deadline_unix_ms: 0,
body: Some(body),
};
write_frame(stream, &envelope.encode_to_vec()).map_err(|e| ClientError::Wire(e.to_string()))?;
let bytes = read_frame_with_cap(stream, MAX_REPLY_BYTES.min(MAX_FRAME_BYTES))
.map_err(|e| ClientError::Wire(e.to_string()))?;
let reply = ProbeEnvelope::decode(bytes.as_slice())
.map_err(|e| ClientError::Wire(format!("decode reply: {e}")))?;
if reply.request_id != request_id {
return Err(ClientError::Desync {
expected: request_id,
got: reply.request_id,
});
}
Ok(reply)
}
impl ProbeClient for SocketProbeClient {
fn register(&mut self, req: &RegisterProcess) -> Result<ProcessKey, ClientError> {
let reply = self.round_trip(Body::Register(req.clone()))?;
match reply.body {
Some(Body::RegistrationStatus(RegistrationStatus { state, detail, .. })) => {
if state == 2 {
req.key.clone().ok_or(ClientError::UnexpectedReply)
} else {
Err(ClientError::Refused { reason: detail })
}
}
_ => Err(ClientError::UnexpectedReply),
}
}
fn heartbeat(&mut self, key: &ProcessKey) -> Result<HeartbeatWork, ClientError> {
let reply = self.round_trip(Body::Heartbeat(Heartbeat {
key: Some(key.clone()),
}))?;
heartbeat_reply(reply)
}
fn submit_capture(&mut self, reply: CaptureReply) -> Result<(), ClientError> {
let reply = self.round_trip(Body::CaptureReply(reply))?;
match reply.body {
Some(Body::RegistrationStatus(RegistrationStatus { error: 0, .. })) => Ok(()),
Some(Body::RegistrationStatus(status)) => Err(ClientError::Refused {
reason: status.detail,
}),
_ => Err(ClientError::UnexpectedReply),
}
}
fn unregister(&mut self, key: &ProcessKey) -> Result<(), ClientError> {
self.round_trip(Body::Unregister(UnregisterProcess {
key: Some(key.clone()),
}))?;
Ok(())
}
}
fn heartbeat_reply(reply: ProbeEnvelope) -> Result<HeartbeatWork, ClientError> {
match reply.body {
Some(Body::CaptureStack(request)) => Ok(HeartbeatWork::Capture(request)),
Some(Body::RegistrationStatus(RegistrationStatus { error: 0, .. })) => {
Ok(HeartbeatWork::Idle)
}
Some(Body::RegistrationStatus(status)) => Err(ClientError::Refused {
reason: status.detail,
}),
_ => Err(ClientError::UnexpectedReply),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Arc, Mutex};
#[derive(Default)]
pub(crate) struct FakeClient {
pub registered: Arc<Mutex<u32>>,
pub heartbeats: Arc<Mutex<u32>>,
pub unregistered: Arc<Mutex<u32>>,
pub fail_register: bool,
}
impl ProbeClient for FakeClient {
fn register(&mut self, req: &RegisterProcess) -> Result<ProcessKey, ClientError> {
if self.fail_register {
return Err(ClientError::Refused {
reason: "test".into(),
});
}
*self.registered.lock().unwrap() += 1;
req.key.clone().ok_or(ClientError::UnexpectedReply)
}
fn heartbeat(&mut self, _key: &ProcessKey) -> Result<HeartbeatWork, ClientError> {
*self.heartbeats.lock().unwrap() += 1;
Ok(HeartbeatWork::Idle)
}
fn submit_capture(&mut self, _reply: CaptureReply) -> Result<(), ClientError> {
Ok(())
}
fn unregister(&mut self, _key: &ProcessKey) -> Result<(), ClientError> {
*self.unregistered.lock().unwrap() += 1;
Ok(())
}
}
#[test]
fn connect_to_a_nonexistent_socket_is_unreachable_not_a_hang() {
let err = SocketProbeClient::connect(
if cfg!(windows) {
r"\\.\pipe\rp-probe-definitely-not-bound-633"
} else {
"/tmp/rp-probe-definitely-not-bound-633.sock"
},
Duration::from_millis(100),
)
.expect_err("must not connect");
assert!(matches!(err, ClientError::Unreachable(_)), "{err:?}");
}
#[test]
fn fake_client_round_trips_for_worker_tests() {
let mut c = FakeClient::default();
let key = ProcessKey {
pid: 1,
start_time: Some(2),
boot_id: Some("b".into()),
};
let req = RegisterProcess {
key: Some(key.clone()),
..Default::default()
};
assert_eq!(c.register(&req).unwrap(), key);
assert_eq!(c.heartbeat(&key).unwrap(), HeartbeatWork::Idle);
c.unregister(&key).unwrap();
assert_eq!(*c.registered.lock().unwrap(), 1);
assert_eq!(*c.heartbeats.lock().unwrap(), 1);
assert_eq!(*c.unregistered.lock().unwrap(), 1);
}
struct Duplex {
incoming: io::Cursor<Vec<u8>>,
written: Vec<u8>,
}
impl io::Read for Duplex {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.incoming.read(buf)
}
}
impl io::Write for Duplex {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.written.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
fn daemon_sending(frames: &[ProbeEnvelope]) -> Duplex {
let mut bytes = Vec::new();
for frame in frames {
write_frame(&mut bytes, &frame.encode_to_vec()).unwrap();
}
Duplex {
incoming: io::Cursor::new(bytes),
written: Vec::new(),
}
}
fn reply_to(request_id: u64) -> ProbeEnvelope {
ProbeEnvelope {
wire_version: 1,
request_id,
deadline_unix_ms: 0,
body: Some(Body::RegistrationStatus(RegistrationStatus::default())),
}
}
fn heartbeat_body() -> Body {
Body::Heartbeat(Heartbeat::default())
}
#[test]
fn a_capture_push_is_returned_to_the_probe_worker() {
let capture = running_process_probe::probe_diag::v1::CaptureStackRequest {
max_depth: 64,
thread_filter: 0,
..Default::default()
};
let reply = ProbeEnvelope {
wire_version: 1,
request_id: 7,
deadline_unix_ms: 0,
body: Some(Body::CaptureStack(capture.clone())),
};
assert_eq!(
heartbeat_reply(reply).expect("capture reply"),
HeartbeatWork::Capture(capture)
);
}
#[test]
fn a_reply_that_answers_the_request_is_accepted() {
let mut stream = daemon_sending(&[reply_to(7)]);
let reply = round_trip_on(&mut stream, 7, heartbeat_body()).expect("matching id");
assert_eq!(reply.request_id, 7);
}
#[test]
fn the_request_carries_the_id_the_reply_is_matched_against() {
let mut stream = daemon_sending(&[reply_to(42)]);
round_trip_on(&mut stream, 42, heartbeat_body()).unwrap();
let mut sent = io::Cursor::new(stream.written);
let frame = read_frame_with_cap(&mut sent, MAX_REPLY_BYTES).expect("a request was written");
let envelope = ProbeEnvelope::decode(frame.as_slice()).unwrap();
assert_eq!(envelope.request_id, 42);
}
#[test]
fn a_reply_answering_a_different_request_is_refused() {
let mut stream = daemon_sending(&[reply_to(2)]);
match round_trip_on(&mut stream, 1, heartbeat_body()) {
Err(ClientError::Desync { expected, got }) => {
assert_eq!((expected, got), (1, 2));
}
other => panic!("expected a desync error, got {other:?}"),
}
}
#[test]
fn an_unsolicited_push_is_not_mistaken_for_the_reply() {
let push = ProbeEnvelope {
wire_version: 1,
request_id: 0,
deadline_unix_ms: 0,
body: Some(Body::Heartbeat(Heartbeat::default())),
};
let mut stream = daemon_sending(&[push, reply_to(1)]);
let outcome = round_trip_on(&mut stream, 1, heartbeat_body());
assert!(
matches!(outcome, Err(ClientError::Desync { .. })),
"a pushed frame must not be consumed as this request's reply, got {outcome:?}"
);
}
#[test]
fn desync_is_not_reported_as_a_wire_error() {
let mut stream = daemon_sending(&[reply_to(9)]);
let err = round_trip_on(&mut stream, 8, heartbeat_body()).unwrap_err();
assert!(!matches!(err, ClientError::Wire(_)), "got {err:?}");
assert!(
err.to_string().contains("out of order"),
"the message should say what went wrong: {err}"
);
}
}