1use std::fs::File;
4use std::io::{self, Read, Write};
5use std::net::Shutdown;
6use std::os::fd::AsFd;
7use std::os::unix::net::UnixStream;
8use std::sync::atomic::{AtomicBool, Ordering};
9use std::sync::{mpsc, Arc};
10use std::thread;
11use std::time::{Duration, Instant};
12
13use rmux_proto::{
14 decode_attach_data_frame, encode_attach_data, encode_attach_data_into_slice,
15 encode_attach_message, AttachFrameDecoder, AttachMessage, AttachShellCommand, RmuxError,
16 TerminalGeometry, TerminalSize, ATTACH_DATA_HEADER_LEN,
17};
18use rustix::event::{poll, PollFd, PollFlags, Timespec};
19use rustix::process::{kill_process, Signal};
20
21use crate::attach_lock_state::AttachLockState;
22use crate::ClientError;
23
24#[path = "attach/render_drain.rs"]
25mod render_drain;
26#[path = "attach/resize.rs"]
27mod resize;
28#[path = "attach/screen.rs"]
29mod screen;
30#[path = "attach/terminal.rs"]
31mod terminal;
32#[path = "attach/terminal_cleanup.rs"]
33mod terminal_cleanup;
34#[path = "attach/termination.rs"]
35mod termination;
36
37use render_drain::{drain_available_attach_stream, flush_pending_render};
38#[cfg(test)]
39use resize::terminal_size_from_fd;
40use resize::{terminal_geometry_from_fd, ResizeWatcher, SignalMaskGuard};
41use screen::{AttachScreenTracker, AttachStopDetector, AttachStopGeneration};
42use terminal::current_process_pid;
43pub use terminal::{AttachError, RawTerminal, Result};
44use termination::AttachTerminationGuard;
45
46#[cfg(test)]
47use terminal_cleanup::fallback_attach_stop_sequence;
48
49const READ_BUFFER_SIZE: usize = 8192;
50const STACK_ATTACH_DATA_PAYLOAD: usize = 1024;
51const POLL_TIMEOUT: Timespec = Timespec {
52 tv_sec: 0,
53 tv_nsec: 100_000_000,
54};
55const RENDER_MAX_PENDING: Duration = Duration::from_millis(8);
56const TERMINATION_POLL_INTERVAL: Duration = Duration::from_millis(100);
57const TERMINATION_OUTPUT_SHUTDOWN_TIMEOUT: Duration = Duration::from_millis(500);
58
59pub fn attach_terminal(stream: UnixStream) -> std::result::Result<(), ClientError> {
61 attach_terminal_with_initial_bytes(stream, Vec::new())
62}
63
64pub fn attach_terminal_with_initial_bytes(
66 stream: UnixStream,
67 initial_bytes: Vec<u8>,
68) -> std::result::Result<(), ClientError> {
69 attach_terminal_with_initial_bytes_and_geometry_flag(stream, initial_bytes, false)
70}
71
72pub fn attach_terminal_with_initial_bytes_and_resize_geometry(
78 stream: UnixStream,
79 initial_bytes: Vec<u8>,
80) -> std::result::Result<(), ClientError> {
81 attach_terminal_with_initial_bytes_and_geometry_flag(stream, initial_bytes, true)
82}
83
84fn attach_terminal_with_initial_bytes_and_geometry_flag(
85 stream: UnixStream,
86 initial_bytes: Vec<u8>,
87 resize_geometry_enabled: bool,
88) -> std::result::Result<(), ClientError> {
89 let terminal = io::stdin();
90 let input = io::stdin();
91 let output = File::from(
92 io::stdout()
93 .as_fd()
94 .try_clone_to_owned()
95 .map_err(AttachError::from)?,
96 );
97
98 attach_with_terminal_with_initial_bytes(
99 stream,
100 initial_bytes,
101 &terminal,
102 input,
103 output,
104 resize_geometry_enabled,
105 )
106}
107
108pub fn attach_with_terminal<Terminal, Input, Output>(
113 stream: UnixStream,
114 terminal: &Terminal,
115 input: Input,
116 output: Output,
117) -> std::result::Result<(), ClientError>
118where
119 Terminal: AsFd,
120 Input: Read + AsFd + Send + 'static,
121 Output: Write + Send + 'static,
122{
123 attach_with_terminal_with_initial_bytes(stream, Vec::new(), terminal, input, output, false)
124}
125
126fn attach_with_terminal_with_initial_bytes<Terminal, Input, Output>(
127 stream: UnixStream,
128 initial_bytes: Vec<u8>,
129 terminal: &Terminal,
130 input: Input,
131 output: Output,
132 resize_geometry_enabled: bool,
133) -> std::result::Result<(), ClientError>
134where
135 Terminal: AsFd,
136 Input: Read + AsFd + Send + 'static,
137 Output: Write + Send + 'static,
138{
139 let termination_guard = AttachTerminationGuard::install().map_err(ClientError::Io)?;
140 let raw_terminal = RawTerminal::from_fd(terminal).map_err(ClientError::from)?;
141 let _ = raw_terminal.flush_pending_input();
142 let screen_tracker = AttachScreenTracker::default();
143 let attach_state = AttachTerminalState {
144 stream,
145 initial_bytes,
146 terminal,
147 raw_terminal: &raw_terminal,
148 screen_tracker: &screen_tracker,
149 resize_geometry_enabled,
150 termination_signals_enabled: true,
151 };
152 let result = drive_attach_with_terminal_state(attach_state, input, output);
153 if result.is_err() {
154 if termination::was_requested() {
155 let _ = raw_terminal.restore_after_termination();
156 } else if !screen_tracker.was_stopped() {
157 let _ = raw_terminal.restore_attach_terminal_state();
158 }
159 }
160 let _ = raw_terminal.flush_pending_input();
161 drop(raw_terminal);
162 termination_guard.finish().map_err(ClientError::Io)?;
163 result
164}
165
166struct AttachTerminalState<'a, Terminal> {
167 stream: UnixStream,
168 initial_bytes: Vec<u8>,
169 terminal: &'a Terminal,
170 raw_terminal: &'a RawTerminal,
171 screen_tracker: &'a AttachScreenTracker,
172 resize_geometry_enabled: bool,
173 termination_signals_enabled: bool,
174}
175
176struct AttachStreamState<'a> {
177 stream: UnixStream,
178 initial_bytes: Vec<u8>,
179 raw_terminal: Option<&'a RawTerminal>,
180 screen_tracker: AttachScreenTracker,
181 resize_events: mpsc::Receiver<TerminalGeometry>,
182 resize_geometry_enabled: bool,
183 termination_signals_enabled: bool,
184}
185
186struct AttachInputReadLease<'a> {
187 state: &'a AttachLockState,
188}
189
190impl<'a> AttachInputReadLease<'a> {
191 fn acquire(state: &'a AttachLockState) -> Option<Self> {
192 state.begin_input_read().then_some(Self { state })
193 }
194}
195
196impl Drop for AttachInputReadLease<'_> {
197 fn drop(&mut self) {
198 self.state.finish_input_read();
199 }
200}
201
202fn drive_attach_with_terminal_state<Terminal, Input, Output>(
203 state: AttachTerminalState<'_, Terminal>,
204 input: Input,
205 output: Output,
206) -> std::result::Result<(), ClientError>
207where
208 Terminal: AsFd,
209 Input: Read + AsFd + Send + 'static,
210 Output: Write + Send + 'static,
211{
212 let _signal_mask = SignalMaskGuard::block_winch().map_err(ClientError::from)?;
215 let (resize_tx, resize_rx) = mpsc::channel();
216 let initial_geometry = terminal_geometry_from_fd(state.terminal).map_err(ClientError::from)?;
217 let terminal_fd = state
218 .terminal
219 .as_fd()
220 .try_clone_to_owned()
221 .map_err(AttachError::from)?;
222
223 if let Some(initial_geometry) = initial_geometry {
224 resize_tx.send(initial_geometry).map_err(|_| {
225 ClientError::Io(io::Error::other(
226 "resize channel closed before attach start",
227 ))
228 })?;
229 }
230
231 let resize_watcher = ResizeWatcher::spawn(terminal_fd, resize_tx)?;
232 let stream_state = AttachStreamState {
233 stream: state.stream,
234 initial_bytes: state.initial_bytes,
235 raw_terminal: Some(state.raw_terminal),
236 screen_tracker: state.screen_tracker.clone(),
237 resize_events: resize_rx,
238 resize_geometry_enabled: state.resize_geometry_enabled,
239 termination_signals_enabled: state.termination_signals_enabled,
240 };
241 let attach_result = drive_attach_stream_inner(stream_state, input, output);
242 drop(resize_watcher);
243 attach_result
244}
245
246pub fn drive_attach_stream<Input, Output>(
248 stream: UnixStream,
249 input: Input,
250 output: Output,
251 resize_events: mpsc::Receiver<TerminalSize>,
252) -> std::result::Result<(), ClientError>
253where
254 Input: Read + AsFd + Send + 'static,
255 Output: Write + Send + 'static,
256{
257 let resize_events = geometry_resize_events_from_size_events(resize_events);
258 let stream_state = AttachStreamState {
259 stream,
260 initial_bytes: Vec::new(),
261 raw_terminal: None,
262 screen_tracker: AttachScreenTracker::default(),
263 resize_events,
264 resize_geometry_enabled: false,
265 termination_signals_enabled: false,
266 };
267 drive_attach_stream_inner(stream_state, input, output)
268}
269
270fn drive_attach_stream_inner<Input, Output>(
271 state: AttachStreamState<'_>,
272 input: Input,
273 output: Output,
274) -> std::result::Result<(), ClientError>
275where
276 Input: Read + AsFd + Send + 'static,
277 Output: Write + Send + 'static,
278{
279 let control = state.stream.try_clone().map_err(ClientError::Io)?;
280 let mut lock_stream = state.stream.try_clone().map_err(ClientError::Io)?;
281 let input_stream = state.stream.try_clone().map_err(ClientError::Io)?;
282 let (input_wakeup, wake_input_thread) = UnixStream::pair().map_err(ClientError::Io)?;
283 let closed = Arc::new(AtomicBool::new(false));
284 let input_closed = Arc::clone(&closed);
285 let output_closed = Arc::clone(&closed);
286 let locked = Arc::new(AttachLockState::default());
287 let input_locked = Arc::clone(&locked);
288 let output_locked = Arc::clone(&locked);
289 let (event_tx, event_rx) = mpsc::channel();
290
291 let input_thread = thread::spawn(move || {
292 input_loop(
293 input_stream,
294 input,
295 state.resize_events,
296 state.resize_geometry_enabled,
297 input_closed,
298 input_locked,
299 wake_input_thread,
300 )
301 });
302 let output_screen_tracker = state.screen_tracker.clone();
303 let action_screen_tracker = state.screen_tracker.clone();
304 let output_thread = thread::spawn(move || {
305 let result = output_loop_with_termination(
306 state.stream,
307 state.initial_bytes,
308 output,
309 output_closed,
310 output_locked,
311 output_screen_tracker,
312 event_tx.clone(),
313 state.termination_signals_enabled,
314 );
315 let _ = event_tx.send(ClientAttachEvent::OutputDone);
316 result
317 });
318
319 let output_result = wait_for_output_thread(
320 output_thread,
321 state.raw_terminal,
322 &mut lock_stream,
323 &locked,
324 &action_screen_tracker,
325 event_rx,
326 state.termination_signals_enabled,
327 );
328 locked.close();
329 closed.store(true, Ordering::SeqCst);
330 let _ = control.shutdown(Shutdown::Both);
331 let _ = input_wakeup.shutdown(Shutdown::Both);
332 let input_result = join_attach_thread(input_thread)?;
333
334 let output_result = output_result?;
335 output_result?;
336 input_result
337}
338
339fn geometry_resize_events_from_size_events(
340 resize_events: mpsc::Receiver<TerminalSize>,
341) -> mpsc::Receiver<TerminalGeometry> {
342 let (geometry_tx, geometry_rx) = mpsc::channel();
343 let _forwarder = thread::spawn(move || {
344 while let Ok(size) = resize_events.recv() {
345 if geometry_tx.send(TerminalGeometry::from_size(size)).is_err() {
346 break;
347 }
348 }
349 });
350 geometry_rx
351}
352
353fn input_loop<Input>(
354 mut stream: UnixStream,
355 mut input: Input,
356 resize_events: mpsc::Receiver<TerminalGeometry>,
357 resize_geometry_enabled: bool,
358 closed: Arc<AtomicBool>,
359 locked: Arc<AttachLockState>,
360 wakeup: UnixStream,
361) -> std::result::Result<(), ClientError>
362where
363 Input: Read + AsFd,
364{
365 let mut read_buffer = [0_u8; READ_BUFFER_SIZE];
366
367 loop {
368 if closed.load(Ordering::SeqCst) {
369 return Ok(());
370 }
371
372 drain_resize_events(&mut stream, &resize_events, resize_geometry_enabled)?;
373 if locked.is_locked() {
374 thread::sleep(Duration::from_millis(20));
375 continue;
376 }
377
378 let mut fds = [
379 PollFd::new(&input, PollFlags::IN | PollFlags::ERR | PollFlags::HUP),
380 PollFd::new(&wakeup, PollFlags::IN | PollFlags::ERR | PollFlags::HUP),
381 ];
382 match poll(&mut fds, Some(&POLL_TIMEOUT)) {
383 Ok(0) => continue,
384 Ok(_) => {}
385 Err(rustix::io::Errno::INTR) => continue,
386 Err(error) => return Err(ClientError::Io(error.into())),
387 }
388
389 if !fds[1].revents().is_empty() {
390 return Ok(());
391 }
392
393 if locked.is_locked() {
397 continue;
398 }
399
400 let ready = fds[0].revents();
401 if ready.is_empty() {
402 continue;
403 }
404 if closed.load(Ordering::SeqCst) {
405 return Ok(());
406 }
407 if !ready.contains(PollFlags::IN) {
408 if ready.contains(PollFlags::HUP) || ready.contains(PollFlags::ERR) {
409 shutdown_attach_writes(&stream)?;
410 return Ok(());
411 }
412 continue;
413 }
414
415 let Some(_input_read_lease) = AttachInputReadLease::acquire(&locked) else {
416 continue;
417 };
418
419 let bytes_read = match input.read(&mut read_buffer) {
420 Ok(0) => {
421 shutdown_attach_writes(&stream)?;
422 return Ok(());
423 }
424 Ok(bytes_read) => bytes_read,
425 Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
426 Err(error) => return Err(ClientError::Io(error)),
427 };
428
429 write_attach_data(&mut stream, &read_buffer[..bytes_read])?;
430 }
431}
432
433#[cfg(test)]
434fn output_loop<Output>(
435 stream: UnixStream,
436 initial_bytes: Vec<u8>,
437 output: Output,
438 closed: Arc<AtomicBool>,
439 locked: Arc<AttachLockState>,
440 screen_tracker: AttachScreenTracker,
441 event_tx: mpsc::Sender<ClientAttachEvent>,
442) -> std::result::Result<(), ClientError>
443where
444 Output: Write,
445{
446 output_loop_with_termination(
447 stream,
448 initial_bytes,
449 output,
450 closed,
451 locked,
452 screen_tracker,
453 event_tx,
454 false,
455 )
456}
457
458#[allow(clippy::too_many_arguments)]
459fn output_loop_with_termination<Output>(
460 mut stream: UnixStream,
461 initial_bytes: Vec<u8>,
462 output: Output,
463 closed: Arc<AtomicBool>,
464 locked: Arc<AttachLockState>,
465 screen_tracker: AttachScreenTracker,
466 event_tx: mpsc::Sender<ClientAttachEvent>,
467 termination_signals_enabled: bool,
468) -> std::result::Result<(), ClientError>
469where
470 Output: Write,
471{
472 let mut output = termination::TerminationAwareWriter::new(output, termination_signals_enabled);
473 let mut decoder = AttachFrameDecoder::new();
474 decoder.push_bytes(&initial_bytes);
475 let mut read_buffer = [0_u8; READ_BUFFER_SIZE];
476 let mut stop_detector = AttachStopDetector::new(screen_tracker.clone());
477 let mut pending_render = None::<Vec<u8>>;
478 let mut pending_render_started_at = None::<Instant>;
479 let mut pending_render_drained_after_deadline = false;
480 let mut painted_render_frame = false;
481 let mut data_scratch = [0_u8; READ_BUFFER_SIZE];
482
483 loop {
484 fail_if_attach_termination_requested(termination_signals_enabled)?;
485 loop {
486 while let Some(bytes) = decoder
487 .next_data_payload_into(&mut data_scratch)
488 .map_err(ClientError::from)?
489 {
490 flush_pending_render_state(
491 &mut output,
492 &mut pending_render,
493 &mut pending_render_started_at,
494 )?;
495 handle_attach_data_payload(&mut output, &locked, &mut stop_detector, bytes)?;
496 }
497
498 let Some(message) = decoder.next_message().map_err(ClientError::from)? else {
499 break;
500 };
501 match message {
502 AttachMessage::Data(bytes) => {
503 flush_pending_render_state(
504 &mut output,
505 &mut pending_render,
506 &mut pending_render_started_at,
507 )?;
508 handle_attach_data_payload(&mut output, &locked, &mut stop_detector, &bytes)?;
509 }
510 AttachMessage::Render(bytes) => {
511 if locked.is_locked() {
512 continue;
513 }
514 if pending_render.is_none() {
515 pending_render_started_at = Some(Instant::now());
516 pending_render_drained_after_deadline = false;
517 }
518 pending_render = Some(bytes);
519 if !painted_render_frame
520 && flush_pending_render_state(
521 &mut output,
522 &mut pending_render,
523 &mut pending_render_started_at,
524 )?
525 {
526 pending_render_drained_after_deadline = false;
527 painted_render_frame = true;
528 }
529 }
530 AttachMessage::KeyDispatched(_) => {}
531 AttachMessage::Resize(_) | AttachMessage::ResizeGeometry(_) => {
532 flush_pending_render_state(
533 &mut output,
534 &mut pending_render,
535 &mut pending_render_started_at,
536 )?;
537 return Err(ClientError::Protocol(RmuxError::Decode(
538 "received unexpected resize message from attach stream".to_owned(),
539 )));
540 }
541 AttachMessage::Lock(command) => {
542 flush_pending_render_state(
543 &mut output,
544 &mut pending_render,
545 &mut pending_render_started_at,
546 )?;
547 locked.lock();
548 send_attach_action(
549 &event_tx,
550 ClientAttachAction::Lock {
551 command,
552 stop_generation: screen_tracker.current_stop_generation(),
553 },
554 )?;
555 }
556 AttachMessage::LockShellCommand(command) => {
557 flush_pending_render_state(
558 &mut output,
559 &mut pending_render,
560 &mut pending_render_started_at,
561 )?;
562 locked.lock();
563 send_attach_action(
564 &event_tx,
565 ClientAttachAction::LockShell {
566 command,
567 stop_generation: screen_tracker.current_stop_generation(),
568 },
569 )?;
570 }
571 AttachMessage::Suspend => {
572 flush_pending_render_state(
573 &mut output,
574 &mut pending_render,
575 &mut pending_render_started_at,
576 )?;
577 locked.lock();
578 send_attach_action(
579 &event_tx,
580 ClientAttachAction::Suspend {
581 stop_generation: screen_tracker.current_stop_generation(),
582 },
583 )?;
584 }
585 AttachMessage::DetachKill => {
586 flush_pending_render_state(
587 &mut output,
588 &mut pending_render,
589 &mut pending_render_started_at,
590 )?;
591 closed.store(true, Ordering::SeqCst);
592 send_attach_action(&event_tx, ClientAttachAction::DetachKill)?;
593 return Ok(());
594 }
595 AttachMessage::DetachExec(command) => {
596 flush_pending_render_state(
597 &mut output,
598 &mut pending_render,
599 &mut pending_render_started_at,
600 )?;
601 closed.store(true, Ordering::SeqCst);
602 send_attach_action(&event_tx, ClientAttachAction::DetachExec(command))?;
603 return Ok(());
604 }
605 AttachMessage::DetachExecShellCommand(command) => {
606 flush_pending_render_state(
607 &mut output,
608 &mut pending_render,
609 &mut pending_render_started_at,
610 )?;
611 closed.store(true, Ordering::SeqCst);
612 send_attach_action(&event_tx, ClientAttachAction::DetachExecShell(command))?;
613 return Ok(());
614 }
615 AttachMessage::Unlock => {
616 flush_pending_render_state(
617 &mut output,
618 &mut pending_render,
619 &mut pending_render_started_at,
620 )?;
621 return Err(ClientError::Protocol(RmuxError::Decode(
622 "received unexpected unlock message from attach stream".to_owned(),
623 )));
624 }
625 AttachMessage::Keystroke(_) => {
626 flush_pending_render_state(
627 &mut output,
628 &mut pending_render,
629 &mut pending_render_started_at,
630 )?;
631 return Err(ClientError::Protocol(RmuxError::Decode(
632 "received unexpected keystroke message from attach stream".to_owned(),
633 )));
634 }
635 }
636 }
637
638 if pending_render.is_some() {
639 let pending_expired = pending_render_expired(pending_render_started_at);
640 if (!pending_expired || !pending_render_drained_after_deadline)
641 && drain_available_attach_stream(&mut stream, &mut decoder, &mut read_buffer)?
642 {
643 if pending_expired {
644 pending_render_drained_after_deadline = true;
645 }
646 continue;
647 }
648 }
649 if pending_render.is_some() && !pending_render_expired(pending_render_started_at) {
650 sleep_until_pending_render_deadline(pending_render_started_at);
651 if drain_available_attach_stream(&mut stream, &mut decoder, &mut read_buffer)? {
652 continue;
653 }
654 }
655 if flush_pending_render_state(
656 &mut output,
657 &mut pending_render,
658 &mut pending_render_started_at,
659 )? {
660 pending_render_drained_after_deadline = false;
661 painted_render_frame = true;
662 }
663
664 let read_result =
665 read_attach_stream(&mut stream, &mut read_buffer, termination_signals_enabled);
666 let bytes_read = match read_result {
667 Ok(0) => {
668 closed.store(true, Ordering::SeqCst);
669 if screen_tracker.was_stopped() {
670 return Ok(());
671 }
672 return Err(ClientError::Io(io::Error::new(
673 io::ErrorKind::UnexpectedEof,
674 "attach stream closed before attach-stop sequence",
675 )));
676 }
677 Ok(bytes_read) => bytes_read,
678 Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
679 Err(error)
680 if screen_tracker.was_stopped()
681 && matches!(
682 error.kind(),
683 io::ErrorKind::ConnectionReset | io::ErrorKind::BrokenPipe
684 ) =>
685 {
686 return Ok(());
687 }
688 Err(error) => return Err(ClientError::Io(error)),
689 };
690
691 let mut consumed = 0;
692 if decoder.is_empty() {
693 while consumed < bytes_read {
694 let Some(frame) = decode_attach_data_frame(&read_buffer[consumed..])
695 .map_err(ClientError::from)?
696 else {
697 break;
698 };
699 handle_attach_data_payload(
700 &mut output,
701 &locked,
702 &mut stop_detector,
703 frame.payload(),
704 )?;
705 consumed += frame.frame_len();
706 }
707 }
708 if consumed < bytes_read {
709 decoder.push_bytes(&read_buffer[consumed..bytes_read]);
710 }
711 }
712}
713
714fn read_attach_stream(
715 stream: &mut UnixStream,
716 read_buffer: &mut [u8],
717 termination_signals_enabled: bool,
718) -> io::Result<usize> {
719 if !termination_signals_enabled {
720 return stream.read(read_buffer);
721 }
722
723 loop {
724 if termination::was_requested() {
725 return Err(termination::interruption_error());
726 }
727 let mut fds = [PollFd::new(
728 &*stream,
729 PollFlags::IN | PollFlags::ERR | PollFlags::HUP,
730 )];
731 match poll(&mut fds, Some(&POLL_TIMEOUT)) {
732 Ok(0) => continue,
733 Ok(_) => {}
734 Err(rustix::io::Errno::INTR) => continue,
735 Err(error) => return Err(error.into()),
736 }
737 if termination::was_requested() {
738 return Err(termination::interruption_error());
739 }
740 if fds[0].revents().is_empty() {
741 continue;
742 }
743 return stream.read(read_buffer);
744 }
745}
746
747fn fail_if_attach_termination_requested(
748 termination_signals_enabled: bool,
749) -> std::result::Result<(), ClientError> {
750 if termination_signals_enabled && termination::was_requested() {
751 return Err(ClientError::Io(termination::interruption_error()));
752 }
753 Ok(())
754}
755
756fn handle_attach_data_payload<Output>(
757 output: &mut Output,
758 locked: &Arc<AttachLockState>,
759 stop_detector: &mut AttachStopDetector,
760 bytes: &[u8],
761) -> std::result::Result<(), ClientError>
762where
763 Output: Write,
764{
765 stop_detector.observe(bytes);
766 if locked.is_locked() {
767 return Ok(());
768 }
769 output.write_all(bytes).map_err(ClientError::Io)?;
770 output.flush().map_err(ClientError::Io)?;
771 Ok(())
772}
773
774fn pending_render_expired(started_at: Option<Instant>) -> bool {
775 started_at.is_some_and(|started_at| started_at.elapsed() >= RENDER_MAX_PENDING)
776}
777
778fn sleep_until_pending_render_deadline(started_at: Option<Instant>) {
779 let Some(started_at) = started_at else {
780 return;
781 };
782 let Some(remaining) = RENDER_MAX_PENDING.checked_sub(started_at.elapsed()) else {
783 return;
784 };
785 if !remaining.is_zero() {
786 thread::sleep(remaining);
787 }
788}
789
790fn flush_pending_render_state<Output>(
791 output: &mut Output,
792 pending_render: &mut Option<Vec<u8>>,
793 pending_render_started_at: &mut Option<Instant>,
794) -> std::result::Result<bool, ClientError>
795where
796 Output: Write,
797{
798 let flushed = pending_render.is_some();
799 flush_pending_render(output, pending_render)?;
800 *pending_render_started_at = None;
801 Ok(flushed)
802}
803
804fn wait_for_output_thread(
805 output_thread: thread::JoinHandle<std::result::Result<(), ClientError>>,
806 raw_terminal: Option<&RawTerminal>,
807 lock_stream: &mut UnixStream,
808 locked: &Arc<AttachLockState>,
809 screen_tracker: &AttachScreenTracker,
810 event_rx: mpsc::Receiver<ClientAttachEvent>,
811 termination_signals_enabled: bool,
812) -> std::result::Result<std::result::Result<(), ClientError>, ClientError> {
813 if termination_signals_enabled {
814 loop {
815 if termination::was_requested() {
816 wait_for_backpressured_output_shutdown(&output_thread, raw_terminal, &event_rx);
817 return Err(ClientError::Io(termination::interruption_error()));
818 }
819 match event_rx.recv_timeout(TERMINATION_POLL_INTERVAL) {
820 Ok(ClientAttachEvent::Action(action)) => {
821 handle_attach_action(
822 raw_terminal,
823 lock_stream,
824 locked,
825 screen_tracker,
826 action,
827 )?;
828 }
829 Ok(ClientAttachEvent::OutputDone) | Err(mpsc::RecvTimeoutError::Disconnected) => {
830 break;
831 }
832 Err(mpsc::RecvTimeoutError::Timeout) => {}
833 }
834 }
835 } else {
836 while let Ok(ClientAttachEvent::Action(action)) = event_rx.recv() {
837 handle_attach_action(raw_terminal, lock_stream, locked, screen_tracker, action)?;
838 }
839 }
840
841 while let Ok(event) = event_rx.try_recv() {
842 match event {
843 ClientAttachEvent::Action(action) => {
844 handle_attach_action(raw_terminal, lock_stream, locked, screen_tracker, action)?;
845 }
846 ClientAttachEvent::OutputDone => {}
847 }
848 }
849
850 join_attach_thread(output_thread)
851}
852
853fn wait_for_backpressured_output_shutdown(
854 output_thread: &thread::JoinHandle<std::result::Result<(), ClientError>>,
855 raw_terminal: Option<&RawTerminal>,
856 event_rx: &mpsc::Receiver<ClientAttachEvent>,
857) {
858 termination::interrupt_thread(output_thread);
859 let _nonblocking_output =
860 raw_terminal.and_then(|raw_terminal| raw_terminal.interrupt_output_writer().ok());
861 let deadline = Instant::now() + TERMINATION_OUTPUT_SHUTDOWN_TIMEOUT;
862 loop {
863 let remaining = deadline.saturating_duration_since(Instant::now());
864 if remaining.is_zero() {
865 break;
866 }
867 match event_rx.recv_timeout(remaining) {
868 Ok(ClientAttachEvent::OutputDone) | Err(mpsc::RecvTimeoutError::Disconnected) => break,
869 Ok(ClientAttachEvent::Action(_)) => {}
870 Err(mpsc::RecvTimeoutError::Timeout) => break,
871 }
872 }
873}
874
875fn send_attach_action(
876 event_tx: &mpsc::Sender<ClientAttachEvent>,
877 action: ClientAttachAction,
878) -> std::result::Result<(), ClientError> {
879 event_tx
880 .send(ClientAttachEvent::Action(action))
881 .map_err(|_| ClientError::Io(io::Error::other("attach event receiver closed")))
882}
883
884fn handle_attach_action(
885 raw_terminal: Option<&RawTerminal>,
886 lock_stream: &mut UnixStream,
887 locked: &Arc<AttachLockState>,
888 screen_tracker: &AttachScreenTracker,
889 action: ClientAttachAction,
890) -> std::result::Result<(), ClientError> {
891 match action {
892 ClientAttachAction::Lock {
893 command,
894 stop_generation,
895 } => {
896 locked.wait_until_input_idle();
897 let Some(raw_terminal) = raw_terminal else {
898 locked.unlock();
899 return Err(ClientError::Protocol(RmuxError::Decode(
900 "received unexpected lock request without a managed terminal".to_owned(),
901 )));
902 };
903 let result = raw_terminal
904 .run_lock_command(&command)
905 .map_err(ClientError::from)
906 .and_then(|()| write_attach_unlock(lock_stream, screen_tracker, stop_generation));
907 locked.unlock();
908 result
909 }
910 ClientAttachAction::LockShell {
911 command,
912 stop_generation,
913 } => {
914 locked.wait_until_input_idle();
915 let Some(raw_terminal) = raw_terminal else {
916 locked.unlock();
917 return Err(ClientError::Protocol(RmuxError::Decode(
918 "received unexpected lock request without a managed terminal".to_owned(),
919 )));
920 };
921 let result = raw_terminal
922 .run_lock_shell_command(&command)
923 .map_err(ClientError::from)
924 .and_then(|()| write_attach_unlock(lock_stream, screen_tracker, stop_generation));
925 locked.unlock();
926 result
927 }
928 ClientAttachAction::Suspend { stop_generation } => {
929 locked.wait_until_input_idle();
930 let Some(raw_terminal) = raw_terminal else {
931 locked.unlock();
932 return Err(ClientError::Protocol(RmuxError::Decode(
933 "received unexpected suspend request without a managed terminal".to_owned(),
934 )));
935 };
936 let result = raw_terminal
937 .suspend_self()
938 .map_err(ClientError::from)
939 .and_then(|()| write_attach_unlock(lock_stream, screen_tracker, stop_generation));
940 locked.unlock();
941 result
942 }
943 ClientAttachAction::DetachKill => {
944 if let Some(raw_terminal) = raw_terminal {
945 raw_terminal.restore().map_err(ClientError::from)?;
946 }
947 kill_process(current_process_pid().map_err(ClientError::Io)?, Signal::HUP)
948 .map_err(|error| ClientError::Io(error.into()))?;
949 Ok(())
950 }
951 ClientAttachAction::DetachExec(command) => {
952 let Some(raw_terminal) = raw_terminal else {
953 return Err(ClientError::Protocol(RmuxError::Decode(
954 "received unexpected detach exec request without a managed terminal".to_owned(),
955 )));
956 };
957 raw_terminal
958 .run_detach_exec_command(&command)
959 .map_err(ClientError::from)
960 }
961 ClientAttachAction::DetachExecShell(command) => {
962 let Some(raw_terminal) = raw_terminal else {
963 return Err(ClientError::Protocol(RmuxError::Decode(
964 "received unexpected detach exec request without a managed terminal".to_owned(),
965 )));
966 };
967 raw_terminal
968 .run_detach_exec_shell_command(&command)
969 .map_err(ClientError::from)
970 }
971 }
972}
973
974fn drain_resize_events(
975 stream: &mut UnixStream,
976 resize_events: &mpsc::Receiver<TerminalGeometry>,
977 resize_geometry_enabled: bool,
978) -> std::result::Result<(), ClientError> {
979 while let Ok(geometry) = resize_events.try_recv() {
980 let message = if resize_geometry_enabled && geometry.pixels.is_some() {
981 AttachMessage::ResizeGeometry(geometry)
982 } else {
983 AttachMessage::Resize(geometry.size)
984 };
985 write_attach_message(stream, message)?;
986 }
987
988 Ok(())
989}
990
991fn write_attach_message(
992 stream: &mut UnixStream,
993 message: AttachMessage,
994) -> std::result::Result<(), ClientError> {
995 let frame = encode_attach_message(&message).map_err(ClientError::from)?;
996 stream.write_all(&frame).map_err(ClientError::Io)
997}
998
999fn write_attach_unlock(
1000 stream: &mut UnixStream,
1001 screen_tracker: &AttachScreenTracker,
1002 stop_generation: Option<AttachStopGeneration>,
1003) -> std::result::Result<(), ClientError> {
1004 let rearmed =
1008 stop_generation.is_some_and(|generation| screen_tracker.rearm_if_current(generation));
1009 if !rearmed && screen_tracker.was_stopped() {
1010 return Ok(());
1011 }
1012
1013 match write_attach_message(stream, AttachMessage::Unlock) {
1014 Err(ClientError::Io(error))
1015 if screen_tracker.was_stopped()
1016 && matches!(
1017 error.kind(),
1018 io::ErrorKind::ConnectionReset | io::ErrorKind::BrokenPipe
1019 ) =>
1020 {
1021 Ok(())
1022 }
1023 result => result,
1024 }
1025}
1026
1027fn write_attach_data(
1028 stream: &mut UnixStream,
1029 bytes: &[u8],
1030) -> std::result::Result<(), ClientError> {
1031 if bytes.len() <= STACK_ATTACH_DATA_PAYLOAD {
1032 let mut frame = [0_u8; STACK_ATTACH_DATA_PAYLOAD + ATTACH_DATA_HEADER_LEN];
1033 let len = encode_attach_data_into_slice(bytes, &mut frame).map_err(ClientError::from)?;
1034 return stream.write_all(&frame[..len]).map_err(ClientError::Io);
1035 }
1036
1037 let frame = encode_attach_data(bytes).map_err(ClientError::from)?;
1038 stream.write_all(&frame).map_err(ClientError::Io)
1039}
1040
1041fn join_attach_thread(
1042 thread: thread::JoinHandle<std::result::Result<(), ClientError>>,
1043) -> std::result::Result<std::result::Result<(), ClientError>, ClientError> {
1044 thread
1045 .join()
1046 .map_err(|_| ClientError::Io(io::Error::other("attach thread panicked")))
1047}
1048
1049fn shutdown_attach_writes(stream: &UnixStream) -> std::result::Result<(), ClientError> {
1050 match stream.shutdown(Shutdown::Write) {
1051 Ok(()) => Ok(()),
1052 Err(error) if error.kind() == io::ErrorKind::NotConnected => Ok(()),
1053 Err(error) => Err(ClientError::Io(error)),
1054 }
1055}
1056
1057#[derive(Debug)]
1058enum ClientAttachAction {
1059 Lock {
1060 command: String,
1061 stop_generation: Option<AttachStopGeneration>,
1062 },
1063 LockShell {
1064 command: AttachShellCommand,
1065 stop_generation: Option<AttachStopGeneration>,
1066 },
1067 Suspend {
1068 stop_generation: Option<AttachStopGeneration>,
1069 },
1070 DetachKill,
1071 DetachExec(String),
1072 DetachExecShell(AttachShellCommand),
1073}
1074
1075#[derive(Debug)]
1076enum ClientAttachEvent {
1077 Action(ClientAttachAction),
1078 OutputDone,
1079}
1080
1081#[cfg(test)]
1082mod tests;