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