1use std::ffi::OsStr;
4#[cfg(all(test, unix))]
5use std::ffi::OsString;
6#[cfg(all(test, unix))]
7use std::fs;
8use std::io::{self, Read, Write};
9#[cfg(all(test, unix))]
10use std::os::unix::ffi::{OsStrExt, OsStringExt};
11use std::path::{Path, PathBuf};
12use std::time::Duration;
13
14use crate::ClientError;
15use rmux_ipc::{connect_blocking, BlockingLocalStream, LocalEndpoint};
16use rmux_proto::{
17 encode_frame, AttachSessionResponse, ControlMode, ControlModeResponse, FrameDecoder,
18 HandshakeRequest, Request, Response, RmuxError, RMUX_FRAME_MAGIC, RMUX_WIRE_VERSION,
19};
20
21const READ_BUFFER_SIZE: usize = 8192;
23const SOCKET_CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
25const SOCKET_WRITE_TIMEOUT: Duration = Duration::from_secs(5);
27const SOCKET_RESPONSE_TIMEOUT: Duration = Duration::from_secs(15);
29const LEGACY_SHUTDOWN_MIN_WIRE_VERSION: u32 = 1;
31const LEGACY_SHUTDOWN_MAX_WIRE_VERSION: u32 = RMUX_WIRE_VERSION - 1;
32
33#[cfg(all(test, unix))]
34const FALLBACK_SOCKET_ROOT: &str = "/tmp";
35#[cfg(all(test, unix))]
36const SOCKET_DIR_PREFIX: &str = "rmux";
37
38pub fn default_socket_path() -> Result<PathBuf, ClientError> {
43 rmux_ipc::default_endpoint()
44 .map(LocalEndpoint::into_path)
45 .map_err(ClientError::Io)
46}
47
48pub fn socket_path_for_label(label: impl AsRef<OsStr>) -> Result<PathBuf, ClientError> {
50 rmux_ipc::endpoint_for_label(label)
51 .map(LocalEndpoint::into_path)
52 .map_err(ClientError::Io)
53}
54
55pub fn resolve_socket_path(
61 socket_name: Option<&OsStr>,
62 socket_path: Option<&Path>,
63) -> Result<PathBuf, ClientError> {
64 rmux_ipc::resolve_endpoint(socket_name, socket_path)
65 .map(LocalEndpoint::into_path)
66 .map_err(ClientError::Io)
67}
68
69pub fn resolve_tmux_compatible_socket_path(
73 socket_name: Option<&OsStr>,
74 socket_path: Option<&Path>,
75) -> Result<PathBuf, ClientError> {
76 rmux_ipc::resolve_tmux_compatible_endpoint(socket_name, socket_path)
77 .map(LocalEndpoint::into_path)
78 .map_err(ClientError::Io)
79}
80
81#[allow(clippy::large_enum_variant)]
85#[derive(Debug)]
86pub enum ConnectResult {
87 Connected(Connection),
89 Absent,
91}
92
93pub fn connect_or_absent(socket_path: &Path) -> Result<ConnectResult, ClientError> {
101 connect_or_absent_with_timeout(socket_path, SOCKET_CONNECT_TIMEOUT)
102}
103
104pub(crate) fn connect_or_absent_with_timeout(
110 socket_path: &Path,
111 timeout: Duration,
112) -> Result<ConnectResult, ClientError> {
113 connect_or_absent_with_timeout_using(socket_path, timeout, connect_stream_with_timeout)
114}
115
116pub fn connect(socket_path: &Path) -> Result<Connection, ClientError> {
118 connect_with_timeout_using(
119 socket_path,
120 SOCKET_CONNECT_TIMEOUT,
121 connect_stream_with_timeout,
122 )
123}
124
125#[derive(Debug)]
127pub struct Connection {
128 stream: BlockingLocalStream,
129 decoder: FrameDecoder,
130 handshake_capabilities: Option<Vec<String>>,
131}
132
133#[allow(clippy::large_enum_variant)]
136#[derive(Debug)]
137pub enum AttachTransition {
138 Upgraded(AttachSessionUpgrade),
140 Rejected(Response),
142}
143
144#[allow(clippy::large_enum_variant)]
147#[derive(Debug)]
148pub enum ControlTransition {
149 Upgraded(ControlModeUpgrade),
151 Rejected(Response),
153}
154
155#[derive(Debug)]
157pub struct AttachSessionUpgrade {
158 response: AttachSessionResponse,
159 stream: BlockingLocalStream,
160 initial_bytes: Vec<u8>,
161}
162
163#[derive(Debug)]
165pub struct ControlModeUpgrade {
166 pub(crate) response: ControlModeResponse,
167 pub(crate) stream: BlockingLocalStream,
168}
169
170impl AttachSessionUpgrade {
171 #[must_use]
173 pub const fn response(&self) -> &AttachSessionResponse {
174 &self.response
175 }
176
177 #[must_use]
179 pub fn into_stream(self) -> BlockingLocalStream {
180 self.stream
181 }
182
183 #[must_use]
186 pub fn into_parts(self) -> (BlockingLocalStream, Vec<u8>) {
187 (self.stream, self.initial_bytes)
188 }
189}
190
191impl ControlModeUpgrade {
192 #[must_use]
194 pub const fn response(&self) -> &ControlModeResponse {
195 &self.response
196 }
197
198 #[must_use]
200 pub const fn mode(&self) -> ControlMode {
201 self.response.mode
202 }
203
204 #[must_use]
206 pub fn into_stream(self) -> BlockingLocalStream {
207 self.stream
208 }
209}
210
211impl Connection {
212 pub(crate) fn new(stream: BlockingLocalStream) -> Result<Self, ClientError> {
213 set_read_timeout(&stream, Some(SOCKET_RESPONSE_TIMEOUT)).map_err(ClientError::Io)?;
214 set_write_timeout(&stream, Some(SOCKET_WRITE_TIMEOUT)).map_err(ClientError::Io)?;
215
216 Ok(Self {
217 stream,
218 decoder: FrameDecoder::new(),
219 handshake_capabilities: None,
220 })
221 }
222
223 pub fn roundtrip(&mut self, request: &Request) -> Result<Response, ClientError> {
229 self.write_request(request)?;
230 self.read_response()
231 }
232
233 pub fn supports_capability(&mut self, capability: &str) -> Result<bool, ClientError> {
240 if let Some(capabilities) = &self.handshake_capabilities {
241 return Ok(capabilities.iter().any(|supported| supported == capability));
242 }
243
244 match self.roundtrip(&Request::Handshake(HandshakeRequest::current()))? {
245 Response::Handshake(response) => {
246 self.handshake_capabilities = Some(response.capabilities);
247 Ok(self
248 .handshake_capabilities
249 .as_ref()
250 .expect("handshake capabilities were just cached")
251 .iter()
252 .any(|supported| supported == capability))
253 }
254 Response::Error(error) => {
255 if matches!(&error.error, RmuxError::UnsupportedWireVersion { .. }) {
256 return Err(ClientError::Protocol(error.error));
257 }
258 self.handshake_capabilities = Some(Vec::new());
259 Ok(false)
260 }
261 _ => {
262 self.handshake_capabilities = Some(Vec::new());
263 Ok(false)
264 }
265 }
266 }
267
268 pub(crate) fn roundtrip_without_read_timeout(
273 &mut self,
274 request: &Request,
275 ) -> Result<Response, ClientError> {
276 let previous_timeout = read_timeout(&self.stream).map_err(ClientError::Io)?;
277 set_read_timeout(&self.stream, None).map_err(ClientError::Io)?;
278 let result = self.roundtrip(request);
279 let restore_result =
280 set_read_timeout(&self.stream, previous_timeout).map_err(ClientError::Io);
281 finish_unbounded_roundtrip(result, restore_result)
282 }
283
284 pub fn read_response_without_read_timeout(&mut self) -> Result<Response, ClientError> {
289 let previous_timeout = read_timeout(&self.stream).map_err(ClientError::Io)?;
290 set_read_timeout(&self.stream, None).map_err(ClientError::Io)?;
291 let result = self.read_response();
292 let restore_result =
293 set_read_timeout(&self.stream, previous_timeout).map_err(ClientError::Io);
294
295 match (result, restore_result) {
296 (Err(error), _) => Err(error),
297 (Ok(response), Ok(())) => Ok(response),
298 (Ok(_), Err(error)) => Err(error),
299 }
300 }
301
302 pub fn read_response_with_read_timeout(
304 &mut self,
305 timeout: Duration,
306 ) -> Result<Response, ClientError> {
307 let previous_timeout = read_timeout(&self.stream).map_err(ClientError::Io)?;
308 set_read_timeout(&self.stream, Some(timeout)).map_err(ClientError::Io)?;
309 let result = self.read_response();
310 let restore_result =
311 set_read_timeout(&self.stream, previous_timeout).map_err(ClientError::Io);
312
313 match (result, restore_result) {
314 (Err(error), _) => Err(error),
315 (Ok(response), Ok(())) => Ok(response),
316 (Ok(_), Err(error)) => Err(error),
317 }
318 }
319
320 pub(crate) fn write_request(&mut self, request: &Request) -> Result<(), ClientError> {
321 let frame = encode_frame(request).map_err(ClientError::Protocol)?;
322 self.stream.write_all(&frame).map_err(ClientError::Io)
323 }
324
325 pub(crate) fn write_legacy_wire_request(
326 &mut self,
327 request: &Request,
328 wire_version: u32,
329 ) -> Result<(), ClientError> {
330 let frame = encode_legacy_wire_frame(request, wire_version)?;
331 self.stream.write_all(&frame).map_err(ClientError::Io)
332 }
333
334 pub(crate) fn read_response(&mut self) -> Result<Response, ClientError> {
335 let mut buffer = [0u8; READ_BUFFER_SIZE];
336
337 loop {
338 match self.decoder.next_frame::<Response>() {
339 Ok(Some(response)) => return Ok(response),
340 Ok(None) => {}
341 Err(error) => return Err(ClientError::Protocol(error)),
342 }
343
344 let bytes_read = match self.stream.read(&mut buffer) {
345 Ok(bytes_read) => bytes_read,
346 Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
347 Err(error) => return Err(ClientError::Io(error)),
348 };
349
350 if bytes_read == 0 {
351 return Err(ClientError::UnexpectedEof);
352 }
353
354 self.decoder.push_bytes(&buffer[..bytes_read]);
355 }
356 }
357
358 pub(crate) fn stream_mut(&mut self) -> &mut BlockingLocalStream {
359 &mut self.stream
360 }
361
362 pub(crate) fn into_attach_upgrade(
363 self,
364 response: AttachSessionResponse,
365 ) -> Result<AttachSessionUpgrade, ClientError> {
366 set_read_timeout(&self.stream, None).map_err(ClientError::Io)?;
367 set_write_timeout(&self.stream, None).map_err(ClientError::Io)?;
368 let initial_bytes = self.decoder.remaining_bytes().to_vec();
369
370 Ok(AttachSessionUpgrade {
371 response,
372 stream: self.stream,
373 initial_bytes,
374 })
375 }
376
377 pub(crate) fn into_control_upgrade(
378 self,
379 response: ControlModeResponse,
380 ) -> Result<ControlModeUpgrade, ClientError> {
381 set_read_timeout(&self.stream, None).map_err(ClientError::Io)?;
382 set_write_timeout(&self.stream, None).map_err(ClientError::Io)?;
383
384 Ok(ControlModeUpgrade {
385 response,
386 stream: self.stream,
387 })
388 }
389}
390
391fn finish_unbounded_roundtrip(
392 result: Result<Response, ClientError>,
393 restore_result: Result<(), ClientError>,
394) -> Result<Response, ClientError> {
395 match (result, restore_result) {
396 (Err(error), _) => Err(error),
397 (Ok(response), Ok(())) => Ok(response),
398 (Ok(response), Err(ClientError::Io(error)))
399 if completed_response_survives_timeout_restore_error(&error) =>
400 {
401 Ok(response)
402 }
403 (Ok(_), Err(error)) => Err(error),
404 }
405}
406
407fn completed_response_survives_timeout_restore_error(error: &io::Error) -> bool {
408 cfg!(target_os = "macos") && error.kind() == io::ErrorKind::InvalidInput
412}
413
414fn encode_legacy_wire_frame(request: &Request, wire_version: u32) -> Result<Vec<u8>, ClientError> {
415 if !(LEGACY_SHUTDOWN_MIN_WIRE_VERSION..=LEGACY_SHUTDOWN_MAX_WIRE_VERSION)
416 .contains(&wire_version)
417 {
418 return Err(ClientError::Protocol(RmuxError::UnsupportedWireVersion {
419 got: wire_version,
420 minimum: LEGACY_SHUTDOWN_MIN_WIRE_VERSION,
421 maximum: LEGACY_SHUTDOWN_MAX_WIRE_VERSION,
422 }));
423 }
424
425 let mut frame = encode_frame(request).map_err(ClientError::Protocol)?;
426 if frame.first().copied() != Some(RMUX_FRAME_MAGIC) {
427 return Err(ClientError::Protocol(RmuxError::Encode(
428 "current frame encoder produced an invalid RMUX envelope".to_owned(),
429 )));
430 }
431
432 if RMUX_WIRE_VERSION > 0x7f || wire_version > 0x7f {
433 return Err(ClientError::Protocol(RmuxError::Encode(
434 "legacy shutdown recovery expects single-byte wire versions".to_owned(),
435 )));
436 }
437
438 match frame.get_mut(1) {
439 Some(version) if *version == RMUX_WIRE_VERSION as u8 => {
440 *version = wire_version as u8;
441 Ok(frame)
442 }
443 _ => Err(ClientError::Protocol(RmuxError::Encode(
444 "current frame encoder used an unexpected wire-version envelope".to_owned(),
445 ))),
446 }
447}
448
449pub(crate) fn read_response_frame_exact(
450 stream: &mut BlockingLocalStream,
451) -> Result<Response, ClientError> {
452 let mut decoder = FrameDecoder::new();
453 let mut byte = [0_u8; 1];
454
455 loop {
456 match decoder.next_frame::<Response>() {
457 Ok(Some(response)) => return Ok(response),
458 Ok(None) => {}
459 Err(error) => return Err(ClientError::Protocol(error)),
460 }
461
462 read_exact_or_eof(stream, &mut byte)?;
463 decoder.push_bytes(&byte);
464 }
465}
466
467fn read_exact_or_eof(
468 stream: &mut BlockingLocalStream,
469 buffer: &mut [u8],
470) -> Result<(), ClientError> {
471 match stream.read_exact(buffer) {
472 Ok(()) => Ok(()),
473 Err(error) if error.kind() == io::ErrorKind::UnexpectedEof => {
474 Err(ClientError::UnexpectedEof)
475 }
476 Err(error) => Err(ClientError::Io(error)),
477 }
478}
479
480#[cfg(all(test, unix))]
481fn socket_path_from_parts(
482 rmux_tmpdir: Option<&OsStr>,
483 user_id: u32,
484 label: &OsStr,
485) -> io::Result<PathBuf> {
486 let root = socket_root_from_parts(rmux_tmpdir)?;
487 let base = root.join(format!("{SOCKET_DIR_PREFIX}-{user_id}"));
488 let mut path = base.into_os_string().into_vec();
489 path.push(b'/');
490 path.extend_from_slice(label.as_bytes());
491
492 Ok(PathBuf::from(OsString::from_vec(path)))
493}
494
495#[cfg(all(test, unix))]
496fn socket_root_from_parts(rmux_tmpdir: Option<&OsStr>) -> io::Result<PathBuf> {
497 let rmux_tmpdir = rmux_tmpdir
498 .filter(|value| !value.is_empty())
499 .map(PathBuf::from);
500 let candidates = rmux_tmpdir
501 .into_iter()
502 .chain(std::iter::once(PathBuf::from(FALLBACK_SOCKET_ROOT)));
503
504 for candidate in candidates {
505 if let Ok(resolved) = fs::canonicalize(&candidate) {
506 return Ok(resolved);
507 }
508 }
509
510 Err(io::Error::new(
511 io::ErrorKind::NotFound,
512 "no suitable rmux socket directory",
513 ))
514}
515
516fn connect_or_absent_with_timeout_using<F>(
517 socket_path: &Path,
518 timeout: Duration,
519 connect_stream: F,
520) -> Result<ConnectResult, ClientError>
521where
522 F: FnOnce(&Path, Duration) -> io::Result<BlockingLocalStream>,
523{
524 match connect_stream(socket_path, timeout) {
525 Ok(stream) => Ok(ConnectResult::Connected(Connection::new(stream)?)),
526 Err(error) if is_absent_error(&error) => Ok(ConnectResult::Absent),
527 Err(error) => Err(ClientError::Io(error)),
528 }
529}
530
531fn connect_with_timeout_using<F>(
532 socket_path: &Path,
533 timeout: Duration,
534 connect_stream: F,
535) -> Result<Connection, ClientError>
536where
537 F: FnOnce(&Path, Duration) -> io::Result<BlockingLocalStream>,
538{
539 let stream = connect_stream(socket_path, timeout).map_err(ClientError::Io)?;
540 Connection::new(stream)
541}
542
543fn connect_stream_with_timeout(
544 socket_path: &Path,
545 timeout: Duration,
546) -> io::Result<BlockingLocalStream> {
547 connect_blocking(
548 &LocalEndpoint::from_path(socket_path.to_path_buf()),
549 timeout,
550 )
551}
552
553fn read_timeout(stream: &BlockingLocalStream) -> io::Result<Option<Duration>> {
554 stream.read_timeout()
555}
556
557fn set_read_timeout(stream: &BlockingLocalStream, timeout: Option<Duration>) -> io::Result<()> {
558 stream.set_read_timeout(timeout)
559}
560
561fn set_write_timeout(stream: &BlockingLocalStream, timeout: Option<Duration>) -> io::Result<()> {
562 stream.set_write_timeout(timeout)
563}
564
565fn is_absent_error(error: &io::Error) -> bool {
567 matches!(
568 error.kind(),
569 io::ErrorKind::NotFound | io::ErrorKind::ConnectionRefused
570 )
571}
572
573#[cfg(all(test, unix))]
574mod tests {
575 include!("connection/tests.rs");
576}