1use std::io::{Read, Write};
17use std::time::Duration;
18
19use interprocess::local_socket::traits::Stream as _;
20use interprocess::local_socket::Stream;
21use prost::Message;
22
23pub const DEFAULT_HELLO_DEADLINE: Duration = Duration::from_secs(3);
32
33use crate::broker::adopt::{IntoBackendIoError, OwnedBackendIo};
34use crate::broker::client::connect_local_socket;
35use crate::broker::connect_watchdog::{capture_connect_dump, ConnectWatchdog, WATCHDOG_GRACE};
36use crate::broker::lifecycle::names::PipePathError;
37use crate::broker::lifecycle::names_v2::{
38 broker_path_scope_hash, v2_program_pipe, BrokerPathIdentityError,
39};
40use crate::broker::lifecycle::sid::{user_sid_hash, SidError};
41use crate::broker::protocol::{
42 hello_reply, read_frame, validate_frame_envelope, write_frame, Frame, FrameKind,
43 FrameValidationError, FramingError, Hello, HelloReply, Negotiated, PayloadEncoding, Refused,
44 CONTROL_PAYLOAD_PROTOCOL, ENVELOPE_VERSION, PROTOCOL_VERSION,
45};
46
47#[derive(Debug, thiserror::Error)]
49pub enum BrokerV2Error {
50 #[error(transparent)]
52 Sid(#[from] SidError),
53
54 #[error(transparent)]
56 PipeName(#[from] PipePathError),
57
58 #[error("dial v2 broker pipe at {socket_path:?}: {source}")]
60 Dial {
61 socket_path: String,
63 #[source]
65 source: std::io::Error,
66 },
67
68 #[error(transparent)]
71 Framing(#[from] FramingError),
72
73 #[error("Hello round-trip io: {0}")]
75 Io(#[from] std::io::Error),
76
77 #[error("HelloReply decode: {0}")]
79 Decode(#[from] prost::DecodeError),
80
81 #[error("HelloReply.result missing")]
83 MissingResult,
84
85 #[error("broker refused Hello: {reason}")]
94 Refused {
95 reason: String,
97 retry_after_ms: u64,
100 details: Box<Refused>,
102 },
103
104 #[error("Hello encode: {0}")]
106 Encode(#[from] prost::EncodeError),
107}
108
109#[derive(Debug, thiserror::Error)]
115pub enum BrokerPathConnectError {
116 #[error(transparent)]
118 Identity(#[from] BrokerPathIdentityError),
119
120 #[error(transparent)]
122 Connect(#[from] BrokerV2Error),
123}
124
125#[derive(Debug, thiserror::Error)]
129pub(crate) enum ExplicitHelloError {
130 #[error(transparent)]
132 Broker(#[from] BrokerV2Error),
133
134 #[error("response Frame decode: {0}")]
136 DecodeFrame(prost::DecodeError),
137
138 #[error("unexpected broker response frame: {0}")]
141 UnexpectedResponseFrame(&'static str),
142}
143
144impl ExplicitHelloError {
145 fn into_broker_v2(self) -> BrokerV2Error {
146 match self {
147 Self::Broker(error) => error,
148 Self::DecodeFrame(error) => BrokerV2Error::Decode(error),
149 Self::UnexpectedResponseFrame(reason) => {
150 BrokerV2Error::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, reason))
151 }
152 }
153 }
154}
155
156#[cfg(feature = "client-async")]
171#[derive(Debug)]
172pub struct AsyncClientSession {
173 inner: ClientSession,
174}
175
176#[cfg(feature = "client-async")]
177impl AsyncClientSession {
178 pub async fn connect(program: &str, version_hint: &str) -> Result<Self, AsyncConnectError> {
183 Self::connect_with_deadline(program, version_hint, DEFAULT_HELLO_DEADLINE).await
184 }
185
186 pub async fn connect_with_deadline(
188 program: &str,
189 version_hint: &str,
190 deadline: Duration,
191 ) -> Result<Self, AsyncConnectError> {
192 let program = program.to_owned();
193 let version_hint = version_hint.to_owned();
194 let joined = tokio::task::spawn_blocking(move || {
195 super::client_v2::connect_with_deadline(&program, &version_hint, deadline)
196 })
197 .await
198 .map_err(|err| AsyncConnectError::Join(err.to_string()))?;
199 Ok(Self { inner: joined? })
200 }
201
202 pub fn negotiated(&self) -> &Negotiated {
204 self.inner.negotiated()
205 }
206
207 pub async fn connect_backend(self) -> Result<Stream, AsyncConnectError> {
214 let inner = self.inner;
215 tokio::task::spawn_blocking(move || inner.connect_backend())
216 .await
217 .map_err(|err| AsyncConnectError::Join(err.to_string()))?
218 .map_err(AsyncConnectError::Dial)
219 }
220
221 pub async fn into_backend_io(self) -> Result<OwnedBackendIo, AsyncConnectError> {
224 let inner = self.inner;
225 tokio::task::spawn_blocking(move || inner.into_backend_io())
226 .await
227 .map_err(|err| AsyncConnectError::Join(err.to_string()))?
228 .map_err(AsyncConnectError::Dial)
229 }
230
231 pub fn into_blocking(self) -> ClientSession {
233 self.inner
234 }
235}
236
237#[cfg(feature = "client-async")]
244#[derive(Debug, thiserror::Error)]
245pub enum AsyncConnectError {
246 #[error(transparent)]
248 Broker(#[from] BrokerV2Error),
249
250 #[error(transparent)]
252 Dial(#[from] BackendDialError),
253
254 #[error("the blocking worker did not complete: {0}")]
258 Join(String),
259}
260
261#[derive(Debug)]
268pub struct ClientSession {
269 stream: Stream,
270 negotiated: Negotiated,
271}
272
273impl ClientSession {
274 pub fn negotiated(&self) -> &Negotiated {
276 &self.negotiated
277 }
278
279 pub fn into_inner(self) -> (Stream, Negotiated) {
284 (self.stream, self.negotiated)
285 }
286
287 pub fn connect_backend(self) -> Result<Stream, BackendDialError> {
304 if self.negotiated.backend_pipe.is_empty() {
305 return Err(BackendDialError::EmptyBackendPipe);
306 }
307 connect_local_socket(&self.negotiated.backend_pipe).map_err(BackendDialError::Connect)
308 }
309
310 pub fn into_backend_io(self) -> Result<OwnedBackendIo, BackendDialError> {
327 let stream = self.connect_backend()?;
328 OwnedBackendIo::from_local_socket_stream(stream).map_err(BackendDialError::IntoBackendIo)
329 }
330}
331
332#[derive(Debug, thiserror::Error)]
339pub enum BackendDialError {
340 #[error("broker negotiated but named no backend pipe")]
347 EmptyBackendPipe,
348
349 #[error("could not connect to the negotiated backend: {0}")]
351 Connect(#[source] std::io::Error),
352
353 #[error("could not take ownership of the backend socket: {0}")]
356 IntoBackendIo(#[source] IntoBackendIoError),
357}
358
359pub fn connect(program: &str, version_hint: &str) -> Result<ClientSession, BrokerV2Error> {
373 connect_service(program, program, version_hint)
374}
375
376pub fn connect_service(
383 program: &str,
384 service_name: &str,
385 version_hint: &str,
386) -> Result<ClientSession, BrokerV2Error> {
387 connect_service_with_deadline(program, service_name, version_hint, DEFAULT_HELLO_DEADLINE)
388}
389
390pub fn connect_or_die(program: &str, version_hint: &str, deadline: Duration) -> ClientSession {
406 let watchdog = ConnectWatchdog::arm(deadline + WATCHDOG_GRACE);
410
411 match connect_with_deadline(program, version_hint, deadline) {
412 Ok(session) => {
413 drop(watchdog);
414 session
415 }
416 Err(err) => {
417 let error = err.to_string();
418 eprintln!(
419 "running-process: v2 broker for '{program}' unreachable within \
420 {deadline:?}: {error} — capturing a stack dump and exiting (no retry)"
421 );
422 if let Some(path) = capture_connect_dump(program, deadline, &error) {
423 eprintln!(
424 "running-process: all-thread stack dump written to {}",
425 path.display()
426 );
427 }
428 std::process::exit(1);
429 }
430 }
431}
432
433pub fn connect_with_deadline(
442 program: &str,
443 version_hint: &str,
444 deadline: Duration,
445) -> Result<ClientSession, BrokerV2Error> {
446 connect_service_with_deadline(program, program, version_hint, deadline)
447}
448
449pub fn connect_service_with_deadline(
451 program: &str,
452 service_name: &str,
453 version_hint: &str,
454 deadline: Duration,
455) -> Result<ClientSession, BrokerV2Error> {
456 let scope_hash = user_sid_hash()?;
457 connect_service_with_scope_hash_and_deadline(
458 program,
459 &scope_hash,
460 service_name,
461 version_hint,
462 deadline,
463 )
464}
465
466pub fn connect_service_for_broker_path_with_deadline(
472 program: &str,
473 broker_path: impl AsRef<std::path::Path>,
474 service_name: &str,
475 version_hint: &str,
476 deadline: Duration,
477) -> Result<ClientSession, BrokerPathConnectError> {
478 let scope_hash = broker_path_scope_hash(broker_path)?;
479 let pipe_name = v2_program_pipe(program, &scope_hash, 0).map_err(BrokerV2Error::from)?;
480 let socket_path =
481 crate::broker::server::singleton_bind::resolve_path_scoped_socket_path(&pipe_name)
482 .map_err(|err| {
483 BrokerV2Error::Io(std::io::Error::new(std::io::ErrorKind::InvalidInput, err))
484 })?;
485 Ok(connect_service_at_socket_with_deadline(
486 program,
487 socket_path,
488 service_name,
489 version_hint,
490 deadline,
491 )?)
492}
493
494pub fn connect_service_with_scope_hash_and_deadline(
501 program: &str,
502 scope_hash: &str,
503 service_name: &str,
504 version_hint: &str,
505 deadline: Duration,
506) -> Result<ClientSession, BrokerV2Error> {
507 let pipe_name = v2_program_pipe(program, scope_hash, 0)?;
508 let socket_path = resolve_socket_path(&pipe_name);
509 connect_service_at_socket_with_deadline(
510 program,
511 socket_path,
512 service_name,
513 version_hint,
514 deadline,
515 )
516}
517
518#[cfg(feature = "client-async")]
525pub(crate) fn connect_hello_at_endpoint_with_deadline(
526 broker_endpoint: impl Into<String>,
527 hello: Hello,
528 deadline: Duration,
529) -> Result<ClientSession, ExplicitHelloError> {
530 let socket_path = broker_endpoint.into();
531 let (tx, rx) = std::sync::mpsc::channel();
532 std::thread::spawn(move || {
533 let _ = tx.send(connect_unbounded_with_hello(&socket_path, hello));
534 });
535 match rx.recv_timeout(deadline) {
536 Ok(result) => result,
537 Err(_) => Err(BrokerV2Error::Io(std::io::Error::new(
538 std::io::ErrorKind::TimedOut,
539 format!("v2 broker Hello did not complete within {deadline:?}"),
540 ))
541 .into()),
542 }
543}
544
545fn connect_service_at_socket_with_deadline(
546 program: &str,
547 socket_path: String,
548 service_name: &str,
549 version_hint: &str,
550 deadline: Duration,
551) -> Result<ClientSession, BrokerV2Error> {
552 let program = program.to_owned();
553 let service_name = service_name.to_owned();
554 let version_hint = version_hint.to_owned();
555 let (tx, rx) = std::sync::mpsc::channel();
556 std::thread::spawn(move || {
557 let _ = tx.send(connect_unbounded(
558 &program,
559 &socket_path,
560 &service_name,
561 &version_hint,
562 ));
563 });
564 match rx.recv_timeout(deadline) {
565 Ok(result) => result,
566 Err(_) => Err(BrokerV2Error::Io(std::io::Error::new(
567 std::io::ErrorKind::TimedOut,
568 format!("v2 broker Hello did not complete within {deadline:?}"),
569 ))),
570 }
571}
572
573fn connect_unbounded(
576 program: &str,
577 socket_path: &str,
578 service_name: &str,
579 version_hint: &str,
580) -> Result<ClientSession, BrokerV2Error> {
581 let name = wrap_socket_name(socket_path).map_err(|err| BrokerV2Error::Dial {
582 socket_path: socket_path.to_string(),
583 source: std::io::Error::new(std::io::ErrorKind::InvalidInput, err),
584 })?;
585 let mut stream = Stream::connect(name).map_err(|source| BrokerV2Error::Dial {
586 socket_path: socket_path.to_string(),
587 source,
588 })?;
589 let hello = default_hello(program, service_name, version_hint);
590 let negotiated =
591 hello_round_trip(&mut stream, hello).map_err(ExplicitHelloError::into_broker_v2)?;
592 Ok(ClientSession { stream, negotiated })
593}
594
595#[cfg(feature = "client-async")]
596fn connect_unbounded_with_hello(
597 socket_path: &str,
598 hello: Hello,
599) -> Result<ClientSession, ExplicitHelloError> {
600 let name = wrap_socket_name(socket_path).map_err(|err| BrokerV2Error::Dial {
601 socket_path: socket_path.to_string(),
602 source: std::io::Error::new(std::io::ErrorKind::InvalidInput, err),
603 })?;
604 let mut stream = Stream::connect(name).map_err(|source| BrokerV2Error::Dial {
605 socket_path: socket_path.to_string(),
606 source,
607 })?;
608 let negotiated = hello_round_trip(&mut stream, hello)?;
609 Ok(ClientSession { stream, negotiated })
610}
611
612fn default_hello(program: &str, service_name: &str, version_hint: &str) -> Hello {
613 Hello {
614 client_min_protocol: ENVELOPE_VERSION as u32,
615 client_max_protocol: ENVELOPE_VERSION as u32,
616 service_name: service_name.to_string(),
617 wanted_version: version_hint.to_string(),
618 client_version: env!("CARGO_PKG_VERSION").to_string(),
619 client_capabilities: 0,
620 auth_token: Vec::new(),
621 request_id: format!("client_v2-{program}-{}", std::process::id()),
622 connection_id: 0,
623 peer_pid: std::process::id(),
624 client_lib_name: "running-process broker::client_v2".to_string(),
625 client_lib_version: env!("CARGO_PKG_VERSION").to_string(),
626 peer_attestation_nonce: Vec::new(),
627 capability_token: Vec::new(),
628 client_keepalive_secs: 0,
629 }
630}
631
632fn hello_round_trip<S: Read + Write>(
633 stream: &mut S,
634 hello: Hello,
635) -> Result<Negotiated, ExplicitHelloError> {
636 let hello_bytes = hello.encode_to_vec();
652 let request_frame = Frame {
653 envelope_version: PROTOCOL_VERSION,
654 kind: FrameKind::Request as i32,
655 payload_protocol: CONTROL_PAYLOAD_PROTOCOL,
656 payload: hello_bytes,
657 request_id: 1,
658 payload_encoding: PayloadEncoding::None as i32,
659 deadline_unix_ms: 0,
660 traceparent: String::new(),
661 tracestate: String::new(),
662 };
663 let body = request_frame.encode_to_vec();
664 write_frame(stream, &body).map_err(BrokerV2Error::from)?;
665
666 let reply_frame_bytes = read_frame(stream).map_err(BrokerV2Error::from)?;
667 let reply_frame =
668 Frame::decode(reply_frame_bytes.as_slice()).map_err(ExplicitHelloError::DecodeFrame)?;
669 validate_frame_envelope(&reply_frame, FrameKind::Response, CONTROL_PAYLOAD_PROTOCOL)
670 .map_err(map_response_frame_validation)?;
671 if reply_frame.request_id != request_frame.request_id {
672 return Err(ExplicitHelloError::UnexpectedResponseFrame(
673 "request_id does not match the Hello request",
674 ));
675 }
676 let reply =
677 HelloReply::decode(reply_frame.payload.as_slice()).map_err(BrokerV2Error::Decode)?;
678 match reply.result {
679 Some(hello_reply::Result::Negotiated(n)) => Ok(n),
680 Some(hello_reply::Result::Refused(r)) => Err(BrokerV2Error::Refused {
681 reason: r.reason.clone(),
682 retry_after_ms: r.retry_after_ms,
683 details: Box::new(r),
684 }
685 .into()),
686 None => Err(BrokerV2Error::MissingResult.into()),
687 }
688}
689
690fn map_response_frame_validation(error: FrameValidationError) -> ExplicitHelloError {
691 ExplicitHelloError::UnexpectedResponseFrame(match error {
692 FrameValidationError::EnvelopeVersion { .. } => "envelope_version is not v1",
693 FrameValidationError::Kind { .. } => "kind is not RESPONSE",
694 FrameValidationError::PayloadProtocol { .. } => "payload_protocol is not control-plane",
695 FrameValidationError::PayloadEncoding { .. } => "payload is compressed",
696 })
697}
698
699fn resolve_socket_path(bare_name: &str) -> String {
700 #[cfg(windows)]
701 {
702 format!(r"\\.\pipe\{bare_name}")
703 }
704 #[cfg(unix)]
705 {
706 use std::path::PathBuf;
707 let dir: PathBuf = {
708 #[cfg(target_os = "macos")]
709 {
710 let uid = unsafe { libc::getuid() };
711 let tmp = std::env::var_os("TMPDIR")
712 .map(PathBuf::from)
713 .unwrap_or_else(|| PathBuf::from("/tmp"));
714 tmp.join(format!(".rp-{uid}-broker-v2"))
715 }
716 #[cfg(not(target_os = "macos"))]
717 {
718 if let Some(d) = std::env::var_os("XDG_RUNTIME_DIR") {
719 PathBuf::from(d).join("running-process").join("broker-v2")
720 } else {
721 let uid = unsafe { libc::getuid() };
722 PathBuf::from(format!("/tmp/running-process-{uid}/broker-v2"))
723 }
724 }
725 };
726 let leaf = if cfg!(target_os = "macos") {
727 let mut hash = blake3::Hasher::new();
728 hash.update(bare_name.as_bytes());
729 let bytes = hash.finalize();
730 let mut hex = String::with_capacity(16);
731 for b in bytes.as_bytes().iter().take(8) {
732 use std::fmt::Write as _;
733 let _ = write!(hex, "{b:02x}");
734 }
735 format!("{hex}.sock")
736 } else {
737 format!("{bare_name}.sock")
738 };
739 dir.join(leaf).to_string_lossy().into_owned()
740 }
741}
742
743fn wrap_socket_name(socket_path: &str) -> Result<interprocess::local_socket::Name<'_>, String> {
744 crate::broker::server::singleton_bind::wrap_socket_name(socket_path)
745}
746
747#[cfg(test)]
748mod tests {
749 use super::*;
750 use interprocess::local_socket::traits::Listener as _;
751 use interprocess::local_socket::ListenerOptions;
752 use std::sync::mpsc;
753 use std::thread;
754 use std::time::{Duration, Instant};
755
756 struct ScriptedHelloIo {
757 response: std::io::Cursor<Vec<u8>>,
758 request: Vec<u8>,
759 }
760
761 impl Read for ScriptedHelloIo {
762 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
763 self.response.read(buf)
764 }
765 }
766
767 impl Write for ScriptedHelloIo {
768 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
769 self.request.extend_from_slice(buf);
770 Ok(buf.len())
771 }
772
773 fn flush(&mut self) -> std::io::Result<()> {
774 Ok(())
775 }
776 }
777
778 fn scripted_response(body: &[u8]) -> ScriptedHelloIo {
779 let mut response = Vec::new();
780 write_frame(&mut response, body).expect("frame scripted response");
781 ScriptedHelloIo {
782 response: std::io::Cursor::new(response),
783 request: Vec::new(),
784 }
785 }
786
787 fn valid_negotiated_response() -> Frame {
788 let reply = HelloReply {
789 result: Some(hello_reply::Result::Negotiated(Negotiated {
790 backend_pipe: "backend".into(),
791 ..Default::default()
792 })),
793 };
794 Frame {
795 envelope_version: PROTOCOL_VERSION,
796 kind: FrameKind::Response as i32,
797 payload_protocol: CONTROL_PAYLOAD_PROTOCOL,
798 payload: reply.encode_to_vec(),
799 request_id: 1,
800 payload_encoding: PayloadEncoding::None as i32,
801 deadline_unix_ms: 0,
802 traceparent: String::new(),
803 tracestate: String::new(),
804 }
805 }
806
807 #[test]
808 fn hello_rejects_invalid_response_envelopes_and_correlation() {
809 let mut invalid = Vec::new();
810 let mut frame = valid_negotiated_response();
811 frame.envelope_version += 1;
812 invalid.push(frame);
813 let mut frame = valid_negotiated_response();
814 frame.kind = FrameKind::Event as i32;
815 invalid.push(frame);
816 let mut frame = valid_negotiated_response();
817 frame.payload_protocol += 1;
818 invalid.push(frame);
819 let mut frame = valid_negotiated_response();
820 frame.payload_encoding = PayloadEncoding::Zstd as i32;
821 invalid.push(frame);
822 let mut frame = valid_negotiated_response();
823 frame.request_id = 0;
824 invalid.push(frame);
825
826 for frame in invalid {
827 let mut io = scripted_response(&frame.encode_to_vec());
828 assert!(matches!(
829 hello_round_trip(&mut io, default_hello("test", "service", "1")),
830 Err(ExplicitHelloError::UnexpectedResponseFrame(_))
831 ));
832 }
833 }
834
835 #[test]
836 fn hello_distinguishes_outer_frame_and_inner_reply_decode_errors() {
837 let mut bad_frame = scripted_response(&[0xff, 0xff, 0xff]);
838 assert!(matches!(
839 hello_round_trip(&mut bad_frame, default_hello("test", "service", "1")),
840 Err(ExplicitHelloError::DecodeFrame(_))
841 ));
842
843 let mut frame = valid_negotiated_response();
844 frame.payload = vec![0xff, 0xff, 0xff];
845 let mut bad_reply = scripted_response(&frame.encode_to_vec());
846 assert!(matches!(
847 hello_round_trip(&mut bad_reply, default_hello("test", "service", "1")),
848 Err(ExplicitHelloError::Broker(BrokerV2Error::Decode(_)))
849 ));
850 }
851
852 fn read_hello_frame(stream: &mut impl Read) -> (Hello, u64) {
860 let bytes = read_frame(stream).expect("read Hello frame");
861 let frame = Frame::decode(bytes.as_slice()).expect("decode Frame");
862 (
863 Hello::decode(frame.payload.as_slice()).expect("decode Hello"),
864 frame.request_id,
865 )
866 }
867
868 fn write_hello_reply_frame(stream: &mut impl Write, request_id: u64, reply: &HelloReply) {
871 let reply_frame = Frame {
872 envelope_version: PROTOCOL_VERSION,
873 kind: FrameKind::Response as i32,
874 payload_protocol: CONTROL_PAYLOAD_PROTOCOL,
875 payload: reply.encode_to_vec(),
876 request_id,
877 payload_encoding: PayloadEncoding::None as i32,
878 deadline_unix_ms: 0,
879 traceparent: String::new(),
880 tracestate: String::new(),
881 };
882 write_frame(stream, &reply_frame.encode_to_vec()).expect("write HelloReply frame");
883 }
884
885 #[cfg(unix)]
895 struct SocketCleanup(std::path::PathBuf);
896
897 #[cfg(unix)]
898 impl Drop for SocketCleanup {
899 fn drop(&mut self) {
900 let _ = std::fs::remove_file(&self.0);
901 }
902 }
903
904 fn spawn_stub_broker(socket_path: String) -> mpsc::Receiver<()> {
909 let (tx, rx) = mpsc::channel();
910 thread::spawn(move || {
911 let name = wrap_socket_name(&socket_path).expect("wrap_socket_name");
912 #[cfg(unix)]
913 let _cleanup = {
914 let _ =
915 std::fs::create_dir_all(std::path::Path::new(&socket_path).parent().unwrap());
916 let _ = std::fs::remove_file(&socket_path);
917 SocketCleanup(std::path::PathBuf::from(&socket_path))
918 };
919 let listener = ListenerOptions::new()
920 .name(name)
921 .create_sync()
922 .expect("ListenerOptions create_sync");
923 tx.send(()).expect("send listener-ready signal");
924 let mut stream = listener.accept().expect("accept");
925 let (hello, request_id) = read_hello_frame(&mut stream);
926 let reply = HelloReply {
927 result: Some(hello_reply::Result::Negotiated(Negotiated {
928 negotiated_protocol: ENVELOPE_VERSION as u32,
929 daemon_version: "stub-1.2.3".to_string(),
930 backend_pipe: String::new(),
931 warnings: Vec::new(),
932 server_capabilities: 0,
933 keepalive_interval_secs: 0,
934 handle_passed_token: Vec::new(),
935 connection_id: 0x00C0_FFEE,
936 })),
937 };
938 write_hello_reply_frame(&mut stream, request_id, &reply);
939 let _ = hello.service_name;
942 });
943 rx
944 }
945
946 #[test]
947 fn connect_completes_hello_round_trip_against_stub_broker() {
948 let program = "client-v2-stub";
950 let sid = user_sid_hash().expect("user_sid_hash");
951 let pipe_name = v2_program_pipe(program, &sid, 0).expect("pipe name");
952 let socket_path = resolve_socket_path(&pipe_name);
953
954 let ready = spawn_stub_broker(socket_path.clone());
955 ready
956 .recv_timeout(Duration::from_secs(2))
957 .expect("stub broker listening");
958
959 let start = Instant::now();
963 let session = loop {
964 match connect(program, "0.0.0") {
965 Ok(s) => break s,
966 Err(err) if start.elapsed() < Duration::from_secs(2) => {
967 eprintln!("connect retry after error: {err}");
968 std::thread::sleep(Duration::from_millis(50));
969 continue;
970 }
971 Err(err) => panic!("connect failed after retries: {err}"),
972 }
973 };
974
975 let neg = session.negotiated();
976 assert_eq!(neg.negotiated_protocol, ENVELOPE_VERSION as u32);
977 assert_eq!(neg.connection_id, 0x00C0_FFEE);
978 assert_eq!(neg.daemon_version, "stub-1.2.3");
979 }
980
981 #[test]
982 fn connect_service_dials_broker_program_but_routes_named_service() {
983 let program = "client-v2-router";
984 let service_name = "soldr-daemon-root-version-hash";
985 let sid = user_sid_hash().expect("user_sid_hash");
986 let pipe_name = v2_program_pipe(program, &sid, 0).expect("pipe name");
987 let socket_path = resolve_socket_path(&pipe_name);
988
989 let (ready_tx, ready_rx) = mpsc::channel();
990 let (hello_tx, hello_rx) = mpsc::channel();
991 thread::spawn(move || {
992 let name = wrap_socket_name(&socket_path).expect("wrap_socket_name");
993 #[cfg(unix)]
994 let _cleanup = {
995 let _ =
996 std::fs::create_dir_all(std::path::Path::new(&socket_path).parent().unwrap());
997 let _ = std::fs::remove_file(&socket_path);
998 SocketCleanup(std::path::PathBuf::from(&socket_path))
999 };
1000 let listener = ListenerOptions::new()
1001 .name(name)
1002 .create_sync()
1003 .expect("ListenerOptions create_sync");
1004 ready_tx.send(()).expect("ready");
1005 let mut stream = listener.accept().expect("accept");
1006 let (hello, request_id) = read_hello_frame(&mut stream);
1007 hello_tx
1008 .send(hello.service_name.clone())
1009 .expect("observed service name");
1010 write_hello_reply_frame(
1011 &mut stream,
1012 request_id,
1013 &HelloReply {
1014 result: Some(hello_reply::Result::Negotiated(Negotiated {
1015 backend_pipe: "route-endpoint".into(),
1016 ..Default::default()
1017 })),
1018 },
1019 );
1020 });
1021
1022 ready_rx
1023 .recv_timeout(Duration::from_secs(2))
1024 .expect("stub broker listening");
1025 let session = connect_service(program, service_name, "0.8.0")
1026 .expect("independent service route connects");
1027 assert_eq!(session.negotiated().backend_pipe, "route-endpoint");
1028 assert_eq!(
1029 hello_rx.recv_timeout(Duration::from_secs(2)).unwrap(),
1030 service_name
1031 );
1032 }
1033
1034 #[test]
1035 fn connect_with_no_broker_returns_dial_error() {
1036 let err =
1037 connect("client-v2-no-broker-ever", "0.0.0").expect_err("no broker => Dial error");
1038 match err {
1039 BrokerV2Error::Dial { .. } => {}
1040 other => panic!("expected Dial, got: {other:?}"),
1041 }
1042 }
1043
1044 fn spawn_stall_broker(socket_path: String) -> mpsc::Receiver<()> {
1048 let (tx, rx) = mpsc::channel();
1049 thread::spawn(move || {
1050 let name = wrap_socket_name(&socket_path).expect("wrap_socket_name");
1051 #[cfg(unix)]
1052 let _cleanup = {
1053 let _ =
1054 std::fs::create_dir_all(std::path::Path::new(&socket_path).parent().unwrap());
1055 let _ = std::fs::remove_file(&socket_path);
1056 SocketCleanup(std::path::PathBuf::from(&socket_path))
1057 };
1058 let listener = ListenerOptions::new()
1059 .name(name)
1060 .create_sync()
1061 .expect("ListenerOptions create_sync");
1062 tx.send(()).expect("send listener-ready signal");
1063 let _stream = listener.accept().expect("accept");
1064 thread::sleep(Duration::from_secs(60));
1067 });
1068 rx
1069 }
1070
1071 #[test]
1074 fn connect_with_deadline_fires_on_stalling_broker() {
1075 let program = "client-v2-stall-deadline";
1076 let sid = user_sid_hash().expect("user_sid_hash");
1077 let pipe_name = v2_program_pipe(program, &sid, 0).expect("pipe name");
1078 let socket_path = resolve_socket_path(&pipe_name);
1079 let ready = spawn_stall_broker(socket_path);
1080 ready
1081 .recv_timeout(Duration::from_secs(2))
1082 .expect("stall broker listening");
1083 let start = Instant::now();
1084 let err = connect_with_deadline(program, "0.0.0", Duration::from_millis(200))
1085 .expect_err("stall broker => deadline TimedOut");
1086 let elapsed = start.elapsed();
1087 match err {
1088 BrokerV2Error::Io(io) => assert_eq!(io.kind(), std::io::ErrorKind::TimedOut),
1089 other => panic!("expected Io(TimedOut), got: {other:?}"),
1090 }
1091 assert!(
1092 elapsed < Duration::from_secs(2),
1093 "deadline should fire within budget; took {elapsed:?}"
1094 );
1095 }
1096
1097 fn spawn_refusing_broker(socket_path: String, retry_after_ms: u64) -> mpsc::Receiver<()> {
1102 let (tx, rx) = mpsc::channel();
1103 thread::spawn(move || {
1104 let name = wrap_socket_name(&socket_path).expect("wrap_socket_name");
1105 #[cfg(unix)]
1106 let _cleanup = {
1107 let _ =
1108 std::fs::create_dir_all(std::path::Path::new(&socket_path).parent().unwrap());
1109 let _ = std::fs::remove_file(&socket_path);
1110 SocketCleanup(std::path::PathBuf::from(&socket_path))
1111 };
1112 let listener = ListenerOptions::new()
1113 .name(name)
1114 .create_sync()
1115 .expect("ListenerOptions create_sync");
1116 tx.send(()).expect("send listener-ready signal");
1117 let mut stream = listener.accept().expect("accept");
1118 let (_hello, request_id) = read_hello_frame(&mut stream);
1119 let reply = HelloReply {
1120 result: Some(hello_reply::Result::Refused(Refused {
1121 code: 0,
1122 reason: "stub refusal".to_string(),
1123 retry_after_ms,
1124 ..Refused::default()
1125 })),
1126 };
1127 write_hello_reply_frame(&mut stream, request_id, &reply);
1128 });
1129 rx
1130 }
1131
1132 fn spawn_multi_accept_stub_broker(socket_path: String, count: usize) -> mpsc::Receiver<()> {
1137 let (tx, rx) = mpsc::channel();
1138 thread::spawn(move || {
1139 let name = wrap_socket_name(&socket_path).expect("wrap_socket_name");
1140 #[cfg(unix)]
1141 let _cleanup = {
1142 let _ =
1143 std::fs::create_dir_all(std::path::Path::new(&socket_path).parent().unwrap());
1144 let _ = std::fs::remove_file(&socket_path);
1145 SocketCleanup(std::path::PathBuf::from(&socket_path))
1146 };
1147 let listener = ListenerOptions::new()
1148 .name(name)
1149 .create_sync()
1150 .expect("ListenerOptions create_sync");
1151 tx.send(()).expect("send listener-ready signal");
1152 for _ in 0..count {
1153 let mut stream = match listener.accept() {
1154 Ok(s) => s,
1155 Err(_) => break,
1156 };
1157 let (_hello, request_id) = read_hello_frame(&mut stream);
1158 let reply = HelloReply {
1159 result: Some(hello_reply::Result::Negotiated(Negotiated {
1160 negotiated_protocol: ENVELOPE_VERSION as u32,
1161 daemon_version: "stub-multi-1".to_string(),
1162 backend_pipe: String::new(),
1163 warnings: Vec::new(),
1164 server_capabilities: 0,
1165 keepalive_interval_secs: 0,
1166 handle_passed_token: Vec::new(),
1167 connection_id: 0x0FFF_F1EE,
1168 })),
1169 };
1170 write_hello_reply_frame(&mut stream, request_id, &reply);
1171 }
1172 });
1173 rx
1174 }
1175
1176 #[test]
1182 fn concurrent_connects_against_multi_accept_broker() {
1183 let program = "client-v2-concurrent-multi";
1184 let sid = user_sid_hash().expect("user_sid_hash");
1185 let pipe_name = v2_program_pipe(program, &sid, 0).expect("pipe name");
1186 let socket_path = resolve_socket_path(&pipe_name);
1187 const N: usize = 8;
1188 let ready = spawn_multi_accept_stub_broker(socket_path, N);
1189 ready
1190 .recv_timeout(Duration::from_secs(2))
1191 .expect("multi-accept broker listening");
1192
1193 let start = Instant::now();
1194 let handles: Vec<_> = (0..N)
1195 .map(|_| {
1196 let p = program.to_string();
1197 thread::spawn(move || connect_with_deadline(&p, "0.0.0", Duration::from_secs(2)))
1198 })
1199 .collect();
1200 let results: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
1201 let elapsed = start.elapsed();
1202
1203 let ok = results.iter().filter(|r| r.is_ok()).count();
1204 assert_eq!(
1205 ok, N,
1206 "all {N} concurrent connects must succeed; got {ok} ok, full results: {results:?}"
1207 );
1208 assert!(
1209 elapsed < Duration::from_secs(5),
1210 "concurrent connect took {elapsed:?}; expected < 5s"
1211 );
1212 for session in results.iter().flatten() {
1213 assert_eq!(session.negotiated().connection_id, 0x0FFF_F1EE);
1214 assert_eq!(session.negotiated().daemon_version, "stub-multi-1");
1215 }
1216 }
1217
1218 fn spawn_missing_result_broker(socket_path: String) -> mpsc::Receiver<()> {
1223 let (tx, rx) = mpsc::channel();
1224 thread::spawn(move || {
1225 let name = wrap_socket_name(&socket_path).expect("wrap_socket_name");
1226 #[cfg(unix)]
1227 let _cleanup = {
1228 let _ =
1229 std::fs::create_dir_all(std::path::Path::new(&socket_path).parent().unwrap());
1230 let _ = std::fs::remove_file(&socket_path);
1231 SocketCleanup(std::path::PathBuf::from(&socket_path))
1232 };
1233 let listener = ListenerOptions::new()
1234 .name(name)
1235 .create_sync()
1236 .expect("ListenerOptions create_sync");
1237 tx.send(()).expect("send listener-ready signal");
1238 let mut stream = listener.accept().expect("accept");
1239 let (_hello, request_id) = read_hello_frame(&mut stream);
1240 let reply = HelloReply { result: None };
1241 write_hello_reply_frame(&mut stream, request_id, &reply);
1242 });
1243 rx
1244 }
1245
1246 #[test]
1247 fn connect_rejects_hello_reply_with_missing_result_oneof() {
1248 let program = "client-v2-missing-result";
1249 let sid = user_sid_hash().expect("user_sid_hash");
1250 let pipe_name = v2_program_pipe(program, &sid, 0).expect("pipe name");
1251 let socket_path = resolve_socket_path(&pipe_name);
1252 let ready = spawn_missing_result_broker(socket_path);
1253 ready
1254 .recv_timeout(Duration::from_secs(2))
1255 .expect("missing-result broker listening");
1256 let start = Instant::now();
1257 let err = loop {
1258 match connect(program, "0.0.0") {
1259 Err(e) => break e,
1260 Ok(_) if start.elapsed() < Duration::from_secs(2) => {
1261 thread::sleep(Duration::from_millis(50));
1262 continue;
1263 }
1264 Ok(_) => panic!("expected MissingResult, got Ok"),
1265 }
1266 };
1267 assert!(
1268 matches!(err, BrokerV2Error::MissingResult),
1269 "expected MissingResult, got: {err:?}"
1270 );
1271 }
1272
1273 fn spawn_drop_on_accept_broker(socket_path: String) -> mpsc::Receiver<()> {
1278 let (tx, rx) = mpsc::channel();
1279 thread::spawn(move || {
1280 let name = wrap_socket_name(&socket_path).expect("wrap_socket_name");
1281 #[cfg(unix)]
1282 let _cleanup = {
1283 let _ =
1284 std::fs::create_dir_all(std::path::Path::new(&socket_path).parent().unwrap());
1285 let _ = std::fs::remove_file(&socket_path);
1286 SocketCleanup(std::path::PathBuf::from(&socket_path))
1287 };
1288 let listener = ListenerOptions::new()
1289 .name(name)
1290 .create_sync()
1291 .expect("ListenerOptions create_sync");
1292 tx.send(()).expect("send listener-ready signal");
1293 let stream = listener.accept().expect("accept");
1294 drop(stream); });
1296 rx
1297 }
1298
1299 #[test]
1300 fn connect_returns_err_on_premature_disconnect() {
1301 let program = "client-v2-prem-disconnect";
1302 let sid = user_sid_hash().expect("user_sid_hash");
1303 let pipe_name = v2_program_pipe(program, &sid, 0).expect("pipe name");
1304 let socket_path = resolve_socket_path(&pipe_name);
1305 let ready = spawn_drop_on_accept_broker(socket_path);
1306 ready
1307 .recv_timeout(Duration::from_secs(2))
1308 .expect("drop-on-accept broker listening");
1309 let start = Instant::now();
1310 let err = loop {
1311 match connect_with_deadline(program, "0.0.0", Duration::from_millis(500)) {
1312 Err(e) => break e,
1313 Ok(_) if start.elapsed() < Duration::from_secs(2) => {
1314 thread::sleep(Duration::from_millis(50));
1315 continue;
1316 }
1317 Ok(_) => panic!("expected transport error, got Ok"),
1318 }
1319 };
1320 match err {
1324 BrokerV2Error::Framing(_) | BrokerV2Error::Io(_) | BrokerV2Error::Dial { .. } => {}
1325 other => panic!("expected transport variant, got: {other:?}"),
1326 }
1327 assert!(
1328 start.elapsed() < Duration::from_secs(2),
1329 "must not hang past deadline; took {:?}",
1330 start.elapsed()
1331 );
1332 }
1333
1334 #[test]
1341 fn connect_rejects_invalid_program_names_before_dial() {
1342 let too_long = "a".repeat(65);
1343 for bad in [
1344 "zccache\0evil",
1345 "../etc/passwd",
1346 r"a\b",
1347 "Zccache",
1348 "a b",
1349 too_long.as_str(),
1350 "",
1351 ] {
1352 let err = connect(bad, "0.0.0")
1353 .expect_err(&format!("invalid program name {bad:?} must be rejected"));
1354 assert!(
1355 matches!(err, BrokerV2Error::PipeName(_)),
1356 "expected PipeName for {bad:?}, got: {err:?}"
1357 );
1358 }
1359 }
1360
1361 #[test]
1365 fn refused_with_u64_max_retry_after_ms_round_trips() {
1366 let program = "client-v2-refused-u64-max";
1367 let sid = user_sid_hash().expect("user_sid_hash");
1368 let pipe_name = v2_program_pipe(program, &sid, 0).expect("pipe name");
1369 let socket_path = resolve_socket_path(&pipe_name);
1370 let ready = spawn_refusing_broker(socket_path, u64::MAX);
1371 ready
1372 .recv_timeout(Duration::from_secs(2))
1373 .expect("refusing broker listening");
1374 let start = Instant::now();
1375 let err = loop {
1376 match connect(program, "0.0.0") {
1377 Err(e) => break e,
1378 Ok(_) if start.elapsed() < Duration::from_secs(2) => {
1379 thread::sleep(Duration::from_millis(50));
1380 continue;
1381 }
1382 Ok(_) => panic!("expected Refused, got Ok"),
1383 }
1384 };
1385 match err {
1386 BrokerV2Error::Refused {
1387 retry_after_ms,
1388 details,
1389 ..
1390 } => {
1391 assert_eq!(retry_after_ms, u64::MAX);
1392 assert_eq!(details.retry_after_ms, u64::MAX);
1393 let _safe_duration = Duration::from_millis(retry_after_ms);
1395 }
1396 other => panic!("expected Refused, got: {other:?}"),
1397 }
1398 }
1399
1400 #[test]
1401 fn refused_exposes_retry_after_ms_top_level() {
1402 let program = "client-v2-refused-retry";
1403 let sid = user_sid_hash().expect("user_sid_hash");
1404 let pipe_name = v2_program_pipe(program, &sid, 0).expect("pipe name");
1405 let socket_path = resolve_socket_path(&pipe_name);
1406 let ready = spawn_refusing_broker(socket_path, 1234);
1407 ready
1408 .recv_timeout(Duration::from_secs(2))
1409 .expect("refusing broker listening");
1410 let start = Instant::now();
1411 let err = loop {
1412 match connect(program, "0.0.0") {
1413 Err(e) => break e,
1414 Ok(_) if start.elapsed() < Duration::from_secs(2) => {
1415 thread::sleep(Duration::from_millis(50));
1416 continue;
1417 }
1418 Ok(_) => panic!("expected Refused"),
1419 }
1420 };
1421 match err {
1422 BrokerV2Error::Refused {
1423 retry_after_ms,
1424 reason,
1425 details,
1426 } => {
1427 assert_eq!(
1428 retry_after_ms, 1234,
1429 "retry hint must surface top-level (was: {retry_after_ms})"
1430 );
1431 assert_eq!(reason, "stub refusal");
1432 assert_eq!(
1433 details.retry_after_ms, 1234,
1434 "details payload still carries the field for full diagnostics"
1435 );
1436 }
1437 other => panic!("expected Refused, got: {other:?}"),
1438 }
1439 }
1440
1441 #[cfg(feature = "client-async")]
1454 #[test]
1455 fn the_hello_does_not_occupy_the_runtime_worker() {
1456 use std::sync::atomic::{AtomicBool, Ordering};
1457 use std::sync::Arc;
1458
1459 let program = "client-v2-async-nonblocking";
1460 let sid = user_sid_hash().expect("user_sid_hash");
1461 let pipe_name = v2_program_pipe(program, &sid, 0).expect("pipe name");
1462 let socket_path = resolve_socket_path(&pipe_name);
1463 let ready = spawn_stall_broker(socket_path);
1464 ready
1465 .recv_timeout(Duration::from_secs(2))
1466 .expect("stall broker listening");
1467
1468 let rt = tokio::runtime::Builder::new_current_thread()
1469 .build()
1470 .expect("current-thread runtime");
1471 rt.block_on(async {
1472 let progressed = Arc::new(AtomicBool::new(false));
1473 let flag = Arc::clone(&progressed);
1474 let other = tokio::spawn(async move {
1475 flag.store(true, Ordering::SeqCst);
1476 });
1477
1478 let _ = AsyncClientSession::connect_with_deadline(
1479 program,
1480 "0.0.0",
1481 Duration::from_millis(200),
1482 )
1483 .await;
1484
1485 assert!(
1486 progressed.load(Ordering::SeqCst),
1487 "the runtime made no progress during the Hello — it ran on the worker"
1488 );
1489 let _ = other.await;
1490 });
1491 }
1492}
1493
1494#[cfg(test)]
1501mod backend_dial_tests {
1502 use super::*;
1503 use interprocess::local_socket::traits::Listener as _;
1507 use interprocess::local_socket::{ListenerOptions, Stream};
1508
1509 fn session_with(stream: Stream, backend_pipe: &str) -> ClientSession {
1516 ClientSession {
1517 stream,
1518 negotiated: Negotiated {
1519 backend_pipe: backend_pipe.to_string(),
1520 ..Default::default()
1521 },
1522 }
1523 }
1524
1525 fn socket_name(path: &str) -> interprocess::local_socket::Name<'_> {
1531 crate::broker::server::connection::local_socket_name(path).expect("socket name")
1532 }
1533
1534 fn temp_endpoint(tag: &str) -> (tempfile::TempDir, String) {
1535 let dir = tempfile::tempdir().expect("tempdir");
1536 let path = if cfg!(windows) {
1537 format!(r"\.\pipe\rp-v2-dial-{tag}-{}", std::process::id())
1538 } else {
1539 dir.path().join(format!("{tag}.sock")).display().to_string()
1540 };
1541 (dir, path)
1542 }
1543
1544 #[test]
1552 fn a_negotiated_reply_with_no_backend_pipe_is_its_own_error() {
1553 let (_dir, path) = temp_endpoint("empty");
1554 let listener = ListenerOptions::new()
1555 .name(socket_name(&path))
1556 .create_sync()
1557 .expect("bind");
1558 let broker_side = Stream::connect(socket_name(&path)).expect("dial");
1559 let _accepted = listener.accept().expect("accept");
1560
1561 let err = session_with(broker_side, "")
1562 .connect_backend()
1563 .expect_err("an empty backend pipe must not be dialed");
1564 assert!(
1565 matches!(err, BackendDialError::EmptyBackendPipe),
1566 "expected EmptyBackendPipe, got {err:?}"
1567 );
1568 }
1569
1570 #[test]
1577 fn the_dial_connects_to_the_backend_and_carries_traffic() {
1578 let (_bdir, broker_path) = temp_endpoint("broker");
1579 let broker_listener = ListenerOptions::new()
1580 .name(socket_name(&broker_path))
1581 .create_sync()
1582 .expect("bind broker");
1583 let broker_side = Stream::connect(socket_name(&broker_path)).expect("dial broker");
1584 let _broker_accepted = broker_listener.accept().expect("accept broker");
1585
1586 let (_kdir, backend_path) = temp_endpoint("backend");
1587 let backend_listener = ListenerOptions::new()
1588 .name(socket_name(&backend_path))
1589 .create_sync()
1590 .expect("bind backend");
1591
1592 let (accepted_tx, accepted_rx) = std::sync::mpsc::channel();
1599 std::thread::spawn(move || {
1600 let _ = accepted_tx.send(backend_listener.accept());
1601 });
1602
1603 let mut data = session_with(broker_side, &backend_path)
1604 .connect_backend()
1605 .expect("dial the negotiated backend");
1606
1607 let mut served = accepted_rx
1609 .recv_timeout(std::time::Duration::from_secs(10))
1610 .expect("nothing connected to the backend within 10s")
1611 .expect("backend accept");
1612 data.write_all(b"ping").expect("write to backend");
1613 data.flush().expect("flush");
1614 let mut got = [0u8; 4];
1615 served.read_exact(&mut got).expect("backend read");
1616 assert_eq!(&got, b"ping", "bytes did not reach the backend");
1617 }
1618
1619 #[test]
1621 fn a_backend_that_is_not_listening_reports_a_connect_error() {
1622 let (_bdir, broker_path) = temp_endpoint("broker2");
1623 let broker_listener = ListenerOptions::new()
1624 .name(socket_name(&broker_path))
1625 .create_sync()
1626 .expect("bind broker");
1627 let broker_side = Stream::connect(socket_name(&broker_path)).expect("dial broker");
1628 let _broker_accepted = broker_listener.accept().expect("accept broker");
1629
1630 let (_kdir, dead_path) = temp_endpoint("nobody-home");
1631 let err = session_with(broker_side, &dead_path)
1632 .connect_backend()
1633 .expect_err("nothing is listening there");
1634 assert!(
1635 matches!(err, BackendDialError::Connect(_)),
1636 "expected Connect, got {err:?}"
1637 );
1638 }
1639
1640 #[cfg(feature = "client-async")]
1644 fn runtime() -> tokio::runtime::Runtime {
1645 tokio::runtime::Builder::new_current_thread()
1646 .build()
1647 .expect("current-thread runtime")
1648 }
1649
1650 #[cfg(feature = "client-async")]
1657 #[test]
1658 fn the_async_dial_reaches_the_backend() {
1659 let (_bdir, broker_path) = temp_endpoint("abroker");
1660 let broker_listener = ListenerOptions::new()
1661 .name(socket_name(&broker_path))
1662 .create_sync()
1663 .expect("bind broker");
1664 let broker_side = Stream::connect(socket_name(&broker_path)).expect("dial broker");
1665 let _broker_accepted = broker_listener.accept().expect("accept broker");
1666
1667 let (_kdir, backend_path) = temp_endpoint("abackend");
1668 let backend_listener = ListenerOptions::new()
1669 .name(socket_name(&backend_path))
1670 .create_sync()
1671 .expect("bind backend");
1672
1673 let (accepted_tx, accepted_rx) = std::sync::mpsc::channel();
1674 std::thread::spawn(move || {
1675 let _ = accepted_tx.send(backend_listener.accept());
1676 });
1677
1678 let session = AsyncClientSession {
1679 inner: session_with(broker_side, &backend_path),
1680 };
1681 let mut data = runtime()
1682 .block_on(session.connect_backend())
1683 .expect("async dial");
1684
1685 let mut served = accepted_rx
1686 .recv_timeout(std::time::Duration::from_secs(10))
1687 .expect("nothing connected to the backend within 10s")
1688 .expect("backend accept");
1689 data.write_all(b"pong").expect("write");
1690 data.flush().expect("flush");
1691 let mut got = [0u8; 4];
1692 served.read_exact(&mut got).expect("read");
1693 assert_eq!(&got, b"pong", "bytes did not reach the backend");
1694 }
1695
1696 #[cfg(feature = "client-async")]
1704 #[test]
1705 fn a_dial_failure_is_not_reported_as_a_runtime_failure() {
1706 let (_bdir, broker_path) = temp_endpoint("abroker2");
1707 let broker_listener = ListenerOptions::new()
1708 .name(socket_name(&broker_path))
1709 .create_sync()
1710 .expect("bind broker");
1711 let broker_side = Stream::connect(socket_name(&broker_path)).expect("dial broker");
1712 let _broker_accepted = broker_listener.accept().expect("accept broker");
1713
1714 let (_kdir, dead_path) = temp_endpoint("anobody");
1715 let session = AsyncClientSession {
1716 inner: session_with(broker_side, &dead_path),
1717 };
1718 let err = runtime()
1719 .block_on(session.connect_backend())
1720 .expect_err("nothing is listening there");
1721 assert!(
1722 matches!(err, AsyncConnectError::Dial(BackendDialError::Connect(_))),
1723 "expected Dial(Connect), got {err:?}"
1724 );
1725 }
1726
1727 #[cfg(feature = "client-async")]
1729 #[test]
1730 fn an_empty_backend_pipe_survives_the_async_hop() {
1731 let (_dir, path) = temp_endpoint("aempty");
1732 let listener = ListenerOptions::new()
1733 .name(socket_name(&path))
1734 .create_sync()
1735 .expect("bind");
1736 let broker_side = Stream::connect(socket_name(&path)).expect("dial");
1737 let _accepted = listener.accept().expect("accept");
1738
1739 let session = AsyncClientSession {
1740 inner: session_with(broker_side, ""),
1741 };
1742 let err = runtime()
1743 .block_on(session.connect_backend())
1744 .expect_err("an empty pipe must not be dialed");
1745 assert!(
1746 matches!(
1747 err,
1748 AsyncConnectError::Dial(BackendDialError::EmptyBackendPipe)
1749 ),
1750 "expected Dial(EmptyBackendPipe), got {err:?}"
1751 );
1752 }
1753}