1use std::io::{self, Read, Write};
4use std::net::Shutdown;
5use std::os::fd::AsFd;
6use std::os::unix::net::UnixStream;
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::sync::{mpsc, Arc};
9use std::thread;
10use std::time::Duration;
11
12use rmux_proto::{
13 encode_attach_message, AttachFrameDecoder, AttachMessage, AttachedKeystroke, RmuxError,
14 TerminalGeometry, TerminalSize,
15};
16use rustix::event::{poll, PollFd, PollFlags, Timespec};
17use rustix::process::{kill_process, Signal};
18
19use crate::ClientError;
20
21#[path = "attach/resize.rs"]
22mod resize;
23#[path = "attach/screen.rs"]
24mod screen;
25#[path = "attach/terminal.rs"]
26mod terminal;
27#[path = "attach/terminal_cleanup.rs"]
28mod terminal_cleanup;
29
30#[cfg(test)]
31use resize::terminal_size_from_fd;
32use resize::{terminal_geometry_from_fd, ResizeWatcher, SignalMaskGuard};
33use screen::{
34 contains_subslice, AttachScreenTracker, AttachStopDetector, ALT_SCREEN_EXIT_FALLBACK,
35 DETACHED_BANNER_PREFIX, EXITED_BANNER,
36};
37use terminal::current_process_pid;
38pub use terminal::{AttachError, RawTerminal, Result};
39
40#[cfg(test)]
41use terminal_cleanup::fallback_attach_stop_sequence;
42
43const READ_BUFFER_SIZE: usize = 8192;
44const POLL_TIMEOUT: Timespec = Timespec {
45 tv_sec: 0,
46 tv_nsec: 100_000_000,
47};
48
49pub fn attach_terminal(stream: UnixStream) -> std::result::Result<(), ClientError> {
51 attach_terminal_with_initial_bytes(stream, Vec::new())
52}
53
54pub fn attach_terminal_with_initial_bytes(
56 stream: UnixStream,
57 initial_bytes: Vec<u8>,
58) -> std::result::Result<(), ClientError> {
59 attach_terminal_with_initial_bytes_and_geometry_flag(stream, initial_bytes, false)
60}
61
62pub fn attach_terminal_with_initial_bytes_and_resize_geometry(
68 stream: UnixStream,
69 initial_bytes: Vec<u8>,
70) -> std::result::Result<(), ClientError> {
71 attach_terminal_with_initial_bytes_and_geometry_flag(stream, initial_bytes, true)
72}
73
74fn attach_terminal_with_initial_bytes_and_geometry_flag(
75 stream: UnixStream,
76 initial_bytes: Vec<u8>,
77 resize_geometry_enabled: bool,
78) -> std::result::Result<(), ClientError> {
79 let terminal = io::stdin();
80 let input = io::stdin();
81 let output = io::stdout();
82
83 attach_with_terminal_with_initial_bytes(
84 stream,
85 initial_bytes,
86 &terminal,
87 input,
88 output,
89 resize_geometry_enabled,
90 )
91}
92
93pub fn attach_with_terminal<Terminal, Input, Output>(
98 stream: UnixStream,
99 terminal: &Terminal,
100 input: Input,
101 output: Output,
102) -> std::result::Result<(), ClientError>
103where
104 Terminal: AsFd,
105 Input: Read + AsFd + Send + 'static,
106 Output: Write + Send + 'static,
107{
108 attach_with_terminal_with_initial_bytes(stream, Vec::new(), terminal, input, output, false)
109}
110
111fn attach_with_terminal_with_initial_bytes<Terminal, Input, Output>(
112 stream: UnixStream,
113 initial_bytes: Vec<u8>,
114 terminal: &Terminal,
115 input: Input,
116 output: Output,
117 resize_geometry_enabled: bool,
118) -> std::result::Result<(), ClientError>
119where
120 Terminal: AsFd,
121 Input: Read + AsFd + Send + 'static,
122 Output: Write + Send + 'static,
123{
124 let raw_terminal = RawTerminal::from_fd(terminal).map_err(ClientError::from)?;
125 let _ = raw_terminal.flush_pending_input();
126 let screen_tracker = AttachScreenTracker::default();
127 let attach_state = AttachTerminalState {
128 stream,
129 initial_bytes,
130 terminal,
131 raw_terminal: &raw_terminal,
132 screen_tracker: &screen_tracker,
133 resize_geometry_enabled,
134 };
135 let result = drive_attach_with_terminal_state(attach_state, input, output);
136 if result.is_err() && !screen_tracker.was_stopped() {
137 let _ = raw_terminal.restore_attach_terminal_state();
138 }
139 let _ = raw_terminal.flush_pending_input();
140 drop(raw_terminal);
141 result
142}
143
144struct AttachTerminalState<'a, Terminal> {
145 stream: UnixStream,
146 initial_bytes: Vec<u8>,
147 terminal: &'a Terminal,
148 raw_terminal: &'a RawTerminal,
149 screen_tracker: &'a AttachScreenTracker,
150 resize_geometry_enabled: bool,
151}
152
153struct AttachStreamState<'a> {
154 stream: UnixStream,
155 initial_bytes: Vec<u8>,
156 raw_terminal: Option<&'a RawTerminal>,
157 screen_tracker: AttachScreenTracker,
158 resize_events: mpsc::Receiver<TerminalGeometry>,
159 resize_geometry_enabled: bool,
160}
161
162fn drive_attach_with_terminal_state<Terminal, Input, Output>(
163 state: AttachTerminalState<'_, Terminal>,
164 input: Input,
165 output: Output,
166) -> std::result::Result<(), ClientError>
167where
168 Terminal: AsFd,
169 Input: Read + AsFd + Send + 'static,
170 Output: Write + Send + 'static,
171{
172 let _signal_mask = SignalMaskGuard::block_winch().map_err(ClientError::from)?;
175 let (resize_tx, resize_rx) = mpsc::channel();
176 let initial_geometry = terminal_geometry_from_fd(state.terminal).map_err(ClientError::from)?;
177 let terminal_fd = state
178 .terminal
179 .as_fd()
180 .try_clone_to_owned()
181 .map_err(AttachError::from)?;
182
183 if let Some(initial_geometry) = initial_geometry {
184 resize_tx.send(initial_geometry).map_err(|_| {
185 ClientError::Io(io::Error::other(
186 "resize channel closed before attach start",
187 ))
188 })?;
189 }
190
191 let resize_watcher = ResizeWatcher::spawn(terminal_fd, resize_tx)?;
192 let stream_state = AttachStreamState {
193 stream: state.stream,
194 initial_bytes: state.initial_bytes,
195 raw_terminal: Some(state.raw_terminal),
196 screen_tracker: state.screen_tracker.clone(),
197 resize_events: resize_rx,
198 resize_geometry_enabled: state.resize_geometry_enabled,
199 };
200 let attach_result = drive_attach_stream_inner(stream_state, input, output);
201 drop(resize_watcher);
202 attach_result
203}
204
205pub fn drive_attach_stream<Input, Output>(
207 stream: UnixStream,
208 input: Input,
209 output: Output,
210 resize_events: mpsc::Receiver<TerminalSize>,
211) -> std::result::Result<(), ClientError>
212where
213 Input: Read + AsFd + Send + 'static,
214 Output: Write + Send + 'static,
215{
216 let resize_events = geometry_resize_events_from_size_events(resize_events);
217 let stream_state = AttachStreamState {
218 stream,
219 initial_bytes: Vec::new(),
220 raw_terminal: None,
221 screen_tracker: AttachScreenTracker::default(),
222 resize_events,
223 resize_geometry_enabled: false,
224 };
225 drive_attach_stream_inner(stream_state, input, output)
226}
227
228fn drive_attach_stream_inner<Input, Output>(
229 state: AttachStreamState<'_>,
230 input: Input,
231 output: Output,
232) -> std::result::Result<(), ClientError>
233where
234 Input: Read + AsFd + Send + 'static,
235 Output: Write + Send + 'static,
236{
237 let control = state.stream.try_clone().map_err(ClientError::Io)?;
238 let mut lock_stream = state.stream.try_clone().map_err(ClientError::Io)?;
239 let input_stream = state.stream.try_clone().map_err(ClientError::Io)?;
240 let closed = Arc::new(AtomicBool::new(false));
241 let input_closed = Arc::clone(&closed);
242 let output_closed = Arc::clone(&closed);
243 let locked = Arc::new(AtomicBool::new(false));
244 let input_locked = Arc::clone(&locked);
245 let output_locked = Arc::clone(&locked);
246 let (action_tx, action_rx) = mpsc::channel();
247
248 let input_thread = thread::spawn(move || {
249 input_loop(
250 input_stream,
251 input,
252 state.resize_events,
253 state.resize_geometry_enabled,
254 input_closed,
255 input_locked,
256 )
257 });
258 let output_screen_tracker = state.screen_tracker.clone();
259 let output_thread = thread::spawn(move || {
260 output_loop(
261 state.stream,
262 state.initial_bytes,
263 output,
264 output_closed,
265 output_locked,
266 output_screen_tracker,
267 action_tx,
268 )
269 });
270
271 let output_result = wait_for_output_thread(
272 output_thread,
273 state.raw_terminal,
274 &mut lock_stream,
275 &locked,
276 action_rx,
277 )?;
278 closed.store(true, Ordering::SeqCst);
279 let _ = control.shutdown(Shutdown::Both);
280 let input_result = join_attach_thread(input_thread)?;
281
282 output_result?;
283 input_result
284}
285
286fn geometry_resize_events_from_size_events(
287 resize_events: mpsc::Receiver<TerminalSize>,
288) -> mpsc::Receiver<TerminalGeometry> {
289 let (geometry_tx, geometry_rx) = mpsc::channel();
290 let _forwarder = thread::spawn(move || {
291 while let Ok(size) = resize_events.recv() {
292 if geometry_tx.send(TerminalGeometry::from_size(size)).is_err() {
293 break;
294 }
295 }
296 });
297 geometry_rx
298}
299
300fn input_loop<Input>(
301 mut stream: UnixStream,
302 mut input: Input,
303 resize_events: mpsc::Receiver<TerminalGeometry>,
304 resize_geometry_enabled: bool,
305 closed: Arc<AtomicBool>,
306 locked: Arc<AtomicBool>,
307) -> std::result::Result<(), ClientError>
308where
309 Input: Read + AsFd,
310{
311 let mut read_buffer = [0_u8; READ_BUFFER_SIZE];
312
313 loop {
314 if closed.load(Ordering::SeqCst) {
315 return Ok(());
316 }
317
318 drain_resize_events(&mut stream, &resize_events, resize_geometry_enabled)?;
319 if locked.load(Ordering::SeqCst) {
320 thread::sleep(Duration::from_millis(20));
321 continue;
322 }
323
324 let mut fds = [PollFd::new(
325 &input,
326 PollFlags::IN | PollFlags::ERR | PollFlags::HUP,
327 )];
328 match poll(&mut fds, Some(&POLL_TIMEOUT)) {
329 Ok(0) => continue,
330 Ok(_) => {}
331 Err(rustix::io::Errno::INTR) => continue,
332 Err(error) => return Err(ClientError::Io(error.into())),
333 }
334
335 let ready = fds[0].revents();
336 if ready.is_empty() {
337 continue;
338 }
339 if closed.load(Ordering::SeqCst) {
340 return Ok(());
341 }
342 if !ready.contains(PollFlags::IN) {
343 if ready.contains(PollFlags::HUP) || ready.contains(PollFlags::ERR) {
344 shutdown_attach_writes(&stream)?;
345 return Ok(());
346 }
347 continue;
348 }
349
350 let bytes_read = match input.read(&mut read_buffer) {
351 Ok(0) => {
352 shutdown_attach_writes(&stream)?;
353 return Ok(());
354 }
355 Ok(bytes_read) => bytes_read,
356 Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
357 Err(error) => return Err(ClientError::Io(error)),
358 };
359
360 write_attach_message(
361 &mut stream,
362 AttachMessage::Keystroke(AttachedKeystroke::new(read_buffer[..bytes_read].to_vec())),
363 )?;
364 }
365}
366
367fn output_loop<Output>(
368 mut stream: UnixStream,
369 initial_bytes: Vec<u8>,
370 mut output: Output,
371 closed: Arc<AtomicBool>,
372 locked: Arc<AtomicBool>,
373 screen_tracker: AttachScreenTracker,
374 action_tx: mpsc::Sender<ClientAttachAction>,
375) -> std::result::Result<(), ClientError>
376where
377 Output: Write,
378{
379 let mut decoder = AttachFrameDecoder::new();
380 decoder.push_bytes(&initial_bytes);
381 let mut read_buffer = [0_u8; READ_BUFFER_SIZE];
382 let mut stop_detector = AttachStopDetector::new(screen_tracker.clone());
383
384 loop {
385 while let Some(message) = decoder.next_message().map_err(ClientError::from)? {
386 match message {
387 AttachMessage::Data(bytes) => {
388 if contains_subslice(&bytes, ALT_SCREEN_EXIT_FALLBACK)
389 || contains_subslice(&bytes, DETACHED_BANNER_PREFIX)
390 || contains_subslice(&bytes, EXITED_BANNER)
391 {
392 screen_tracker.mark_stopped();
393 }
394 stop_detector.observe(&bytes);
395 if locked.load(Ordering::SeqCst) {
396 continue;
397 }
398 output.write_all(&bytes).map_err(ClientError::Io)?;
399 output.flush().map_err(ClientError::Io)?;
400 }
401 AttachMessage::KeyDispatched(_) => {}
402 AttachMessage::Resize(_) | AttachMessage::ResizeGeometry(_) => {
403 return Err(ClientError::Protocol(RmuxError::Decode(
404 "received unexpected resize message from attach stream".to_owned(),
405 )));
406 }
407 AttachMessage::Lock(command) => {
408 locked.store(true, Ordering::SeqCst);
409 action_tx
410 .send(ClientAttachAction::Lock(command))
411 .map_err(|_| {
412 ClientError::Io(io::Error::other("lock request receiver closed"))
413 })?;
414 }
415 AttachMessage::LockShellCommand(command) => {
416 locked.store(true, Ordering::SeqCst);
417 action_tx
418 .send(ClientAttachAction::Lock(command.command().to_owned()))
419 .map_err(|_| {
420 ClientError::Io(io::Error::other("lock request receiver closed"))
421 })?;
422 }
423 AttachMessage::Suspend => {
424 locked.store(true, Ordering::SeqCst);
425 action_tx.send(ClientAttachAction::Suspend).map_err(|_| {
426 ClientError::Io(io::Error::other("suspend request receiver closed"))
427 })?;
428 }
429 AttachMessage::DetachKill => {
430 closed.store(true, Ordering::SeqCst);
431 action_tx
432 .send(ClientAttachAction::DetachKill)
433 .map_err(|_| {
434 ClientError::Io(io::Error::other("detach request receiver closed"))
435 })?;
436 return Ok(());
437 }
438 AttachMessage::DetachExec(command) => {
439 closed.store(true, Ordering::SeqCst);
440 action_tx
441 .send(ClientAttachAction::DetachExec(command))
442 .map_err(|_| {
443 ClientError::Io(io::Error::other("detach request receiver closed"))
444 })?;
445 return Ok(());
446 }
447 AttachMessage::DetachExecShellCommand(command) => {
448 closed.store(true, Ordering::SeqCst);
449 action_tx
450 .send(ClientAttachAction::DetachExec(command.command().to_owned()))
451 .map_err(|_| {
452 ClientError::Io(io::Error::other("detach request receiver closed"))
453 })?;
454 return Ok(());
455 }
456 AttachMessage::Unlock => {
457 return Err(ClientError::Protocol(RmuxError::Decode(
458 "received unexpected unlock message from attach stream".to_owned(),
459 )));
460 }
461 AttachMessage::Keystroke(_) => {
462 return Err(ClientError::Protocol(RmuxError::Decode(
463 "received unexpected keystroke message from attach stream".to_owned(),
464 )));
465 }
466 }
467 }
468
469 let bytes_read = match stream.read(&mut read_buffer) {
470 Ok(0) => {
471 closed.store(true, Ordering::SeqCst);
472 if screen_tracker.was_stopped() {
473 return Ok(());
474 }
475 return Err(ClientError::Io(io::Error::new(
476 io::ErrorKind::UnexpectedEof,
477 "attach stream closed before attach-stop sequence",
478 )));
479 }
480 Ok(bytes_read) => bytes_read,
481 Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
482 Err(error)
483 if screen_tracker.was_stopped()
484 && matches!(
485 error.kind(),
486 io::ErrorKind::ConnectionReset | io::ErrorKind::BrokenPipe
487 ) =>
488 {
489 return Ok(());
490 }
491 Err(error) => return Err(ClientError::Io(error)),
492 };
493
494 decoder.push_bytes(&read_buffer[..bytes_read]);
495 }
496}
497
498fn wait_for_output_thread(
499 output_thread: thread::JoinHandle<std::result::Result<(), ClientError>>,
500 raw_terminal: Option<&RawTerminal>,
501 lock_stream: &mut UnixStream,
502 locked: &Arc<AtomicBool>,
503 action_rx: mpsc::Receiver<ClientAttachAction>,
504) -> std::result::Result<std::result::Result<(), ClientError>, ClientError> {
505 loop {
506 match action_rx.recv_timeout(Duration::from_millis(20)) {
507 Ok(action) => handle_attach_action(raw_terminal, lock_stream, locked, action)?,
508 Err(mpsc::RecvTimeoutError::Timeout) if output_thread.is_finished() => break,
509 Err(mpsc::RecvTimeoutError::Timeout) => {}
510 Err(mpsc::RecvTimeoutError::Disconnected) => break,
511 }
512 }
513
514 while let Ok(action) = action_rx.try_recv() {
515 handle_attach_action(raw_terminal, lock_stream, locked, action)?;
516 }
517
518 join_attach_thread(output_thread)
519}
520
521fn handle_attach_action(
522 raw_terminal: Option<&RawTerminal>,
523 lock_stream: &mut UnixStream,
524 locked: &Arc<AtomicBool>,
525 action: ClientAttachAction,
526) -> std::result::Result<(), ClientError> {
527 match action {
528 ClientAttachAction::Lock(command) => {
529 let Some(raw_terminal) = raw_terminal else {
530 locked.store(false, Ordering::SeqCst);
531 return Err(ClientError::Protocol(RmuxError::Decode(
532 "received unexpected lock request without a managed terminal".to_owned(),
533 )));
534 };
535 raw_terminal
536 .run_lock_command(&command)
537 .map_err(ClientError::from)?;
538 write_attach_message(lock_stream, AttachMessage::Unlock)?;
539 locked.store(false, Ordering::SeqCst);
540 Ok(())
541 }
542 ClientAttachAction::Suspend => {
543 let Some(raw_terminal) = raw_terminal else {
544 locked.store(false, Ordering::SeqCst);
545 return Err(ClientError::Protocol(RmuxError::Decode(
546 "received unexpected suspend request without a managed terminal".to_owned(),
547 )));
548 };
549 raw_terminal.suspend_self().map_err(ClientError::from)?;
550 write_attach_message(lock_stream, AttachMessage::Unlock)?;
551 locked.store(false, Ordering::SeqCst);
552 Ok(())
553 }
554 ClientAttachAction::DetachKill => {
555 if let Some(raw_terminal) = raw_terminal {
556 raw_terminal.restore().map_err(ClientError::from)?;
557 }
558 kill_process(current_process_pid().map_err(ClientError::Io)?, Signal::HUP)
559 .map_err(|error| ClientError::Io(error.into()))?;
560 Ok(())
561 }
562 ClientAttachAction::DetachExec(command) => {
563 let Some(raw_terminal) = raw_terminal else {
564 return Err(ClientError::Protocol(RmuxError::Decode(
565 "received unexpected detach exec request without a managed terminal".to_owned(),
566 )));
567 };
568 raw_terminal
569 .run_detach_exec_command(&command)
570 .map_err(ClientError::from)
571 }
572 }
573}
574
575fn drain_resize_events(
576 stream: &mut UnixStream,
577 resize_events: &mpsc::Receiver<TerminalGeometry>,
578 resize_geometry_enabled: bool,
579) -> std::result::Result<(), ClientError> {
580 while let Ok(geometry) = resize_events.try_recv() {
581 let message = if resize_geometry_enabled && geometry.pixels.is_some() {
582 AttachMessage::ResizeGeometry(geometry)
583 } else {
584 AttachMessage::Resize(geometry.size)
585 };
586 write_attach_message(stream, message)?;
587 }
588
589 Ok(())
590}
591
592fn write_attach_message(
593 stream: &mut UnixStream,
594 message: AttachMessage,
595) -> std::result::Result<(), ClientError> {
596 let frame = encode_attach_message(&message).map_err(ClientError::from)?;
597 stream.write_all(&frame).map_err(ClientError::Io)
598}
599
600fn join_attach_thread(
601 thread: thread::JoinHandle<std::result::Result<(), ClientError>>,
602) -> std::result::Result<std::result::Result<(), ClientError>, ClientError> {
603 thread
604 .join()
605 .map_err(|_| ClientError::Io(io::Error::other("attach thread panicked")))
606}
607
608fn shutdown_attach_writes(stream: &UnixStream) -> std::result::Result<(), ClientError> {
609 match stream.shutdown(Shutdown::Write) {
610 Ok(()) => Ok(()),
611 Err(error) if error.kind() == io::ErrorKind::NotConnected => Ok(()),
612 Err(error) => Err(ClientError::Io(error)),
613 }
614}
615
616#[derive(Debug)]
617enum ClientAttachAction {
618 Lock(String),
619 Suspend,
620 DetachKill,
621 DetachExec(String),
622}
623
624#[cfg(test)]
625mod tests;