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_using(
102 socket_path,
103 SOCKET_CONNECT_TIMEOUT,
104 connect_stream_with_timeout,
105 )
106}
107
108pub fn connect(socket_path: &Path) -> Result<Connection, ClientError> {
110 connect_with_timeout_using(
111 socket_path,
112 SOCKET_CONNECT_TIMEOUT,
113 connect_stream_with_timeout,
114 )
115}
116
117#[derive(Debug)]
119pub struct Connection {
120 stream: BlockingLocalStream,
121 decoder: FrameDecoder,
122 handshake_capabilities: Option<Vec<String>>,
123}
124
125#[allow(clippy::large_enum_variant)]
128#[derive(Debug)]
129pub enum AttachTransition {
130 Upgraded(AttachSessionUpgrade),
132 Rejected(Response),
134}
135
136#[allow(clippy::large_enum_variant)]
139#[derive(Debug)]
140pub enum ControlTransition {
141 Upgraded(ControlModeUpgrade),
143 Rejected(Response),
145}
146
147#[derive(Debug)]
149pub struct AttachSessionUpgrade {
150 response: AttachSessionResponse,
151 stream: BlockingLocalStream,
152 initial_bytes: Vec<u8>,
153}
154
155#[derive(Debug)]
157pub struct ControlModeUpgrade {
158 pub(crate) response: ControlModeResponse,
159 pub(crate) stream: BlockingLocalStream,
160}
161
162impl AttachSessionUpgrade {
163 #[must_use]
165 pub const fn response(&self) -> &AttachSessionResponse {
166 &self.response
167 }
168
169 #[must_use]
171 pub fn into_stream(self) -> BlockingLocalStream {
172 self.stream
173 }
174
175 #[must_use]
178 pub fn into_parts(self) -> (BlockingLocalStream, Vec<u8>) {
179 (self.stream, self.initial_bytes)
180 }
181}
182
183impl ControlModeUpgrade {
184 #[must_use]
186 pub const fn response(&self) -> &ControlModeResponse {
187 &self.response
188 }
189
190 #[must_use]
192 pub const fn mode(&self) -> ControlMode {
193 self.response.mode
194 }
195
196 #[must_use]
198 pub fn into_stream(self) -> BlockingLocalStream {
199 self.stream
200 }
201}
202
203impl Connection {
204 pub(crate) fn new(stream: BlockingLocalStream) -> Result<Self, ClientError> {
205 set_read_timeout(&stream, Some(SOCKET_RESPONSE_TIMEOUT)).map_err(ClientError::Io)?;
206 set_write_timeout(&stream, Some(SOCKET_WRITE_TIMEOUT)).map_err(ClientError::Io)?;
207
208 Ok(Self {
209 stream,
210 decoder: FrameDecoder::new(),
211 handshake_capabilities: None,
212 })
213 }
214
215 pub fn roundtrip(&mut self, request: &Request) -> Result<Response, ClientError> {
221 self.write_request(request)?;
222 self.read_response()
223 }
224
225 pub fn supports_capability(&mut self, capability: &str) -> Result<bool, ClientError> {
232 if let Some(capabilities) = &self.handshake_capabilities {
233 return Ok(capabilities.iter().any(|supported| supported == capability));
234 }
235
236 match self.roundtrip(&Request::Handshake(HandshakeRequest::current()))? {
237 Response::Handshake(response) => {
238 self.handshake_capabilities = Some(response.capabilities);
239 Ok(self
240 .handshake_capabilities
241 .as_ref()
242 .expect("handshake capabilities were just cached")
243 .iter()
244 .any(|supported| supported == capability))
245 }
246 Response::Error(error) => {
247 if matches!(&error.error, RmuxError::UnsupportedWireVersion { .. }) {
248 return Err(ClientError::Protocol(error.error));
249 }
250 self.handshake_capabilities = Some(Vec::new());
251 Ok(false)
252 }
253 _ => {
254 self.handshake_capabilities = Some(Vec::new());
255 Ok(false)
256 }
257 }
258 }
259
260 pub(crate) fn roundtrip_without_read_timeout(
265 &mut self,
266 request: &Request,
267 ) -> Result<Response, ClientError> {
268 let previous_timeout = read_timeout(&self.stream).map_err(ClientError::Io)?;
269 set_read_timeout(&self.stream, None).map_err(ClientError::Io)?;
270 let result = self.roundtrip(request);
271 let restore_result =
272 set_read_timeout(&self.stream, previous_timeout).map_err(ClientError::Io);
273
274 match (result, restore_result) {
275 (Err(error), _) => Err(error),
276 (Ok(response), Ok(())) => Ok(response),
277 (Ok(_), Err(error)) => Err(error),
278 }
279 }
280
281 pub fn read_response_without_read_timeout(&mut self) -> Result<Response, ClientError> {
286 let previous_timeout = read_timeout(&self.stream).map_err(ClientError::Io)?;
287 set_read_timeout(&self.stream, None).map_err(ClientError::Io)?;
288 let result = self.read_response();
289 let restore_result =
290 set_read_timeout(&self.stream, previous_timeout).map_err(ClientError::Io);
291
292 match (result, restore_result) {
293 (Err(error), _) => Err(error),
294 (Ok(response), Ok(())) => Ok(response),
295 (Ok(_), Err(error)) => Err(error),
296 }
297 }
298
299 pub fn read_response_with_read_timeout(
301 &mut self,
302 timeout: Duration,
303 ) -> Result<Response, ClientError> {
304 let previous_timeout = read_timeout(&self.stream).map_err(ClientError::Io)?;
305 set_read_timeout(&self.stream, Some(timeout)).map_err(ClientError::Io)?;
306 let result = self.read_response();
307 let restore_result =
308 set_read_timeout(&self.stream, previous_timeout).map_err(ClientError::Io);
309
310 match (result, restore_result) {
311 (Err(error), _) => Err(error),
312 (Ok(response), Ok(())) => Ok(response),
313 (Ok(_), Err(error)) => Err(error),
314 }
315 }
316
317 pub(crate) fn write_request(&mut self, request: &Request) -> Result<(), ClientError> {
318 let frame = encode_frame(request).map_err(ClientError::Protocol)?;
319 self.stream.write_all(&frame).map_err(ClientError::Io)
320 }
321
322 pub(crate) fn write_legacy_wire_request(
323 &mut self,
324 request: &Request,
325 wire_version: u32,
326 ) -> Result<(), ClientError> {
327 let frame = encode_legacy_wire_frame(request, wire_version)?;
328 self.stream.write_all(&frame).map_err(ClientError::Io)
329 }
330
331 pub(crate) fn read_response(&mut self) -> Result<Response, ClientError> {
332 let mut buffer = [0u8; READ_BUFFER_SIZE];
333
334 loop {
335 match self.decoder.next_frame::<Response>() {
336 Ok(Some(response)) => return Ok(response),
337 Ok(None) => {}
338 Err(error) => return Err(ClientError::Protocol(error)),
339 }
340
341 let bytes_read = match self.stream.read(&mut buffer) {
342 Ok(bytes_read) => bytes_read,
343 Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
344 Err(error) => return Err(ClientError::Io(error)),
345 };
346
347 if bytes_read == 0 {
348 return Err(ClientError::UnexpectedEof);
349 }
350
351 self.decoder.push_bytes(&buffer[..bytes_read]);
352 }
353 }
354
355 pub(crate) fn stream_mut(&mut self) -> &mut BlockingLocalStream {
356 &mut self.stream
357 }
358
359 pub(crate) fn into_attach_upgrade(
360 self,
361 response: AttachSessionResponse,
362 ) -> Result<AttachSessionUpgrade, ClientError> {
363 set_read_timeout(&self.stream, None).map_err(ClientError::Io)?;
364 set_write_timeout(&self.stream, None).map_err(ClientError::Io)?;
365 let initial_bytes = self.decoder.remaining_bytes().to_vec();
366
367 Ok(AttachSessionUpgrade {
368 response,
369 stream: self.stream,
370 initial_bytes,
371 })
372 }
373
374 pub(crate) fn into_control_upgrade(
375 self,
376 response: ControlModeResponse,
377 ) -> Result<ControlModeUpgrade, ClientError> {
378 set_read_timeout(&self.stream, None).map_err(ClientError::Io)?;
379 set_write_timeout(&self.stream, None).map_err(ClientError::Io)?;
380
381 Ok(ControlModeUpgrade {
382 response,
383 stream: self.stream,
384 })
385 }
386}
387
388fn encode_legacy_wire_frame(request: &Request, wire_version: u32) -> Result<Vec<u8>, ClientError> {
389 if !(LEGACY_SHUTDOWN_MIN_WIRE_VERSION..=LEGACY_SHUTDOWN_MAX_WIRE_VERSION)
390 .contains(&wire_version)
391 {
392 return Err(ClientError::Protocol(RmuxError::UnsupportedWireVersion {
393 got: wire_version,
394 minimum: LEGACY_SHUTDOWN_MIN_WIRE_VERSION,
395 maximum: LEGACY_SHUTDOWN_MAX_WIRE_VERSION,
396 }));
397 }
398
399 let mut frame = encode_frame(request).map_err(ClientError::Protocol)?;
400 if frame.first().copied() != Some(RMUX_FRAME_MAGIC) {
401 return Err(ClientError::Protocol(RmuxError::Encode(
402 "current frame encoder produced an invalid RMUX envelope".to_owned(),
403 )));
404 }
405
406 if RMUX_WIRE_VERSION > 0x7f || wire_version > 0x7f {
407 return Err(ClientError::Protocol(RmuxError::Encode(
408 "legacy shutdown recovery expects single-byte wire versions".to_owned(),
409 )));
410 }
411
412 match frame.get_mut(1) {
413 Some(version) if *version == RMUX_WIRE_VERSION as u8 => {
414 *version = wire_version as u8;
415 Ok(frame)
416 }
417 _ => Err(ClientError::Protocol(RmuxError::Encode(
418 "current frame encoder used an unexpected wire-version envelope".to_owned(),
419 ))),
420 }
421}
422
423pub(crate) fn read_response_frame_exact(
424 stream: &mut BlockingLocalStream,
425) -> Result<Response, ClientError> {
426 let mut decoder = FrameDecoder::new();
427 let mut byte = [0_u8; 1];
428
429 loop {
430 match decoder.next_frame::<Response>() {
431 Ok(Some(response)) => return Ok(response),
432 Ok(None) => {}
433 Err(error) => return Err(ClientError::Protocol(error)),
434 }
435
436 read_exact_or_eof(stream, &mut byte)?;
437 decoder.push_bytes(&byte);
438 }
439}
440
441fn read_exact_or_eof(
442 stream: &mut BlockingLocalStream,
443 buffer: &mut [u8],
444) -> Result<(), ClientError> {
445 match stream.read_exact(buffer) {
446 Ok(()) => Ok(()),
447 Err(error) if error.kind() == io::ErrorKind::UnexpectedEof => {
448 Err(ClientError::UnexpectedEof)
449 }
450 Err(error) => Err(ClientError::Io(error)),
451 }
452}
453
454#[cfg(all(test, unix))]
455fn socket_path_from_parts(
456 rmux_tmpdir: Option<&OsStr>,
457 user_id: u32,
458 label: &OsStr,
459) -> io::Result<PathBuf> {
460 let root = socket_root_from_parts(rmux_tmpdir)?;
461 let base = root.join(format!("{SOCKET_DIR_PREFIX}-{user_id}"));
462 let mut path = base.into_os_string().into_vec();
463 path.push(b'/');
464 path.extend_from_slice(label.as_bytes());
465
466 Ok(PathBuf::from(OsString::from_vec(path)))
467}
468
469#[cfg(all(test, unix))]
470fn socket_root_from_parts(rmux_tmpdir: Option<&OsStr>) -> io::Result<PathBuf> {
471 let rmux_tmpdir = rmux_tmpdir
472 .filter(|value| !value.is_empty())
473 .map(PathBuf::from);
474 let candidates = rmux_tmpdir
475 .into_iter()
476 .chain(std::iter::once(PathBuf::from(FALLBACK_SOCKET_ROOT)));
477
478 for candidate in candidates {
479 if let Ok(resolved) = fs::canonicalize(&candidate) {
480 return Ok(resolved);
481 }
482 }
483
484 Err(io::Error::new(
485 io::ErrorKind::NotFound,
486 "no suitable rmux socket directory",
487 ))
488}
489
490fn connect_or_absent_with_timeout_using<F>(
491 socket_path: &Path,
492 timeout: Duration,
493 connect_stream: F,
494) -> Result<ConnectResult, ClientError>
495where
496 F: FnOnce(&Path, Duration) -> io::Result<BlockingLocalStream>,
497{
498 match connect_stream(socket_path, timeout) {
499 Ok(stream) => Ok(ConnectResult::Connected(Connection::new(stream)?)),
500 Err(error) if is_absent_error(&error) => Ok(ConnectResult::Absent),
501 Err(error) => Err(ClientError::Io(error)),
502 }
503}
504
505fn connect_with_timeout_using<F>(
506 socket_path: &Path,
507 timeout: Duration,
508 connect_stream: F,
509) -> Result<Connection, ClientError>
510where
511 F: FnOnce(&Path, Duration) -> io::Result<BlockingLocalStream>,
512{
513 let stream = connect_stream(socket_path, timeout).map_err(ClientError::Io)?;
514 Connection::new(stream)
515}
516
517fn connect_stream_with_timeout(
518 socket_path: &Path,
519 timeout: Duration,
520) -> io::Result<BlockingLocalStream> {
521 connect_blocking(
522 &LocalEndpoint::from_path(socket_path.to_path_buf()),
523 timeout,
524 )
525}
526
527fn read_timeout(stream: &BlockingLocalStream) -> io::Result<Option<Duration>> {
528 stream.read_timeout()
529}
530
531fn set_read_timeout(stream: &BlockingLocalStream, timeout: Option<Duration>) -> io::Result<()> {
532 stream.set_read_timeout(timeout)
533}
534
535fn set_write_timeout(stream: &BlockingLocalStream, timeout: Option<Duration>) -> io::Result<()> {
536 stream.set_write_timeout(timeout)
537}
538
539fn is_absent_error(error: &io::Error) -> bool {
541 matches!(
542 error.kind(),
543 io::ErrorKind::NotFound | io::ErrorKind::ConnectionRefused
544 )
545}
546
547#[cfg(all(test, unix))]
548mod tests {
549 include!("connection/tests.rs");
550}