1use crate::fd_table::{
2 allocate_file_description_id, FdResult, FileDescription, ProcessFdTable, SharedFileDescription,
3 FILETYPE_CHARACTER_DEVICE, O_RDWR,
4};
5use crate::poll::{PollEvents, PollNotifier, POLLHUP, POLLIN, POLLOUT};
6use std::collections::{BTreeMap, VecDeque};
7use std::error::Error;
8use std::fmt;
9use std::sync::{Arc, Condvar, Mutex, MutexGuard};
10use std::time::Duration;
11use web_time::Instant;
12
13pub const MAX_PTY_BUFFER_BYTES: usize = 65_536;
14pub const MAX_CANON: usize = 4_096;
15pub const SIGINT: i32 = 2;
16pub const SIGQUIT: i32 = 3;
17pub const SIGTSTP: i32 = 20;
18const DEFAULT_PTY_COLUMNS: u16 = 80;
19const DEFAULT_PTY_ROWS: u16 = 24;
20
21pub type PtyResult<T> = Result<T, PtyError>;
22pub type SignalHandler = Arc<dyn Fn(u32, i32) + Send + Sync>;
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct PtyError {
26 code: &'static str,
27 message: String,
28}
29
30impl PtyError {
31 pub fn code(&self) -> &'static str {
32 self.code
33 }
34
35 fn bad_file_descriptor(message: impl Into<String>) -> Self {
36 Self {
37 code: "EBADF",
38 message: message.into(),
39 }
40 }
41
42 fn io(message: impl Into<String>) -> Self {
43 Self {
44 code: "EIO",
45 message: message.into(),
46 }
47 }
48
49 fn would_block(message: impl Into<String>) -> Self {
50 Self {
51 code: "EAGAIN",
52 message: message.into(),
53 }
54 }
55}
56
57impl fmt::Display for PtyError {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 write!(f, "{}: {}", self.code, self.message)
60 }
61}
62
63impl Error for PtyError {}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
66pub struct LineDisciplineConfig {
67 pub icrnl: Option<bool>,
68 pub canonical: Option<bool>,
69 pub echo: Option<bool>,
70 pub isig: Option<bool>,
71 pub opost: Option<bool>,
72 pub onlcr: Option<bool>,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct Termios {
77 pub icrnl: bool,
78 pub opost: bool,
79 pub onlcr: bool,
80 pub icanon: bool,
81 pub echo: bool,
82 pub isig: bool,
83 pub cc: TermiosControlChars,
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
87pub struct PartialTermios {
88 pub icrnl: Option<bool>,
89 pub opost: Option<bool>,
90 pub onlcr: Option<bool>,
91 pub icanon: Option<bool>,
92 pub echo: Option<bool>,
93 pub isig: Option<bool>,
94 pub cc: Option<PartialTermiosControlChars>,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct TermiosControlChars {
99 pub vintr: u8,
100 pub vquit: u8,
101 pub vsusp: u8,
102 pub veof: u8,
103 pub verase: u8,
104 pub vkill: u8,
105 pub vwerase: u8,
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
109pub struct PartialTermiosControlChars {
110 pub vintr: Option<u8>,
111 pub vquit: Option<u8>,
112 pub vsusp: Option<u8>,
113 pub veof: Option<u8>,
114 pub verase: Option<u8>,
115 pub vkill: Option<u8>,
116 pub vwerase: Option<u8>,
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub struct PtyWindowSize {
121 pub cols: u16,
122 pub rows: u16,
123}
124
125impl Default for PtyWindowSize {
126 fn default() -> Self {
127 Self {
128 cols: DEFAULT_PTY_COLUMNS,
129 rows: DEFAULT_PTY_ROWS,
130 }
131 }
132}
133
134impl Default for Termios {
135 fn default() -> Self {
136 Self {
137 icrnl: true,
138 opost: true,
139 onlcr: true,
140 icanon: true,
141 echo: true,
142 isig: true,
143 cc: TermiosControlChars {
144 vintr: 0x03,
145 vquit: 0x1c,
146 vsusp: 0x1a,
147 veof: 0x04,
148 verase: 0x7f,
149 vkill: 0x15,
150 vwerase: 0x17,
151 },
152 }
153 }
154}
155
156impl Termios {
157 fn merge(&mut self, update: PartialTermios) {
158 if let Some(icrnl) = update.icrnl {
159 self.icrnl = icrnl;
160 }
161 if let Some(opost) = update.opost {
162 self.opost = opost;
163 }
164 if let Some(onlcr) = update.onlcr {
165 self.onlcr = onlcr;
166 }
167 if let Some(icanon) = update.icanon {
168 self.icanon = icanon;
169 }
170 if let Some(echo) = update.echo {
171 self.echo = echo;
172 }
173 if let Some(isig) = update.isig {
174 self.isig = isig;
175 }
176 if let Some(cc) = update.cc {
177 self.cc.merge(cc);
178 }
179 }
180}
181
182impl TermiosControlChars {
183 fn merge(&mut self, update: PartialTermiosControlChars) {
184 if let Some(vintr) = update.vintr {
185 self.vintr = vintr;
186 }
187 if let Some(vquit) = update.vquit {
188 self.vquit = vquit;
189 }
190 if let Some(vsusp) = update.vsusp {
191 self.vsusp = vsusp;
192 }
193 if let Some(veof) = update.veof {
194 self.veof = veof;
195 }
196 if let Some(verase) = update.verase {
197 self.verase = verase;
198 }
199 if let Some(vkill) = update.vkill {
200 self.vkill = vkill;
201 }
202 if let Some(vwerase) = update.vwerase {
203 self.vwerase = vwerase;
204 }
205 }
206}
207
208#[derive(Debug, Clone)]
209pub struct PtyEnd {
210 pub description: SharedFileDescription,
211 pub filetype: u8,
212}
213
214#[derive(Debug, Clone)]
215pub struct PtyPair {
216 pub master: PtyEnd,
217 pub slave: PtyEnd,
218 pub path: String,
219}
220
221#[derive(Debug, Clone, Copy, PartialEq, Eq)]
222struct PtyRef {
223 pty_id: u64,
224 end: PtyEndKind,
225}
226
227#[derive(Debug, Clone, Copy, PartialEq, Eq)]
228enum PtyEndKind {
229 Master,
230 Slave,
231}
232
233#[derive(Debug, Default)]
234struct PendingRead {
235 length: usize,
236 result: Option<Option<Vec<u8>>>,
237}
238
239#[derive(Debug, Clone)]
240struct RawModeLease {
241 owner_pid: u32,
242 generation: u64,
243 applied_termios_generation: u64,
244 restore_termios: Termios,
245}
246
247#[derive(Debug, Clone, Default)]
248struct PtyState {
249 path: String,
250 input_buffer: VecDeque<Vec<u8>>,
251 output_buffer: VecDeque<Vec<u8>>,
252 input_eof_pending: bool,
253 closed_master: bool,
254 closed_slave: bool,
255 waiting_input_reads: VecDeque<u64>,
256 waiting_output_reads: VecDeque<u64>,
257 termios: Termios,
258 termios_generation: u64,
259 next_raw_mode_generation: u64,
260 raw_mode_leases: Vec<RawModeLease>,
261 line_buffer: Vec<u8>,
262 foreground_pgid: u32,
263 window_size: PtyWindowSize,
264}
265
266#[derive(Debug)]
267struct PtyManagerState {
268 ptys: BTreeMap<u64, PtyState>,
269 desc_to_pty: BTreeMap<u64, PtyRef>,
270 waiters: BTreeMap<u64, PendingRead>,
271 next_pty_id: u64,
272 next_waiter_id: u64,
273}
274
275impl Default for PtyManagerState {
276 fn default() -> Self {
277 Self {
278 ptys: BTreeMap::new(),
279 desc_to_pty: BTreeMap::new(),
280 waiters: BTreeMap::new(),
281 next_pty_id: 0,
282 next_waiter_id: 1,
283 }
284 }
285}
286
287#[derive(Debug)]
288struct PtyManagerInner {
289 state: Mutex<PtyManagerState>,
290 waiters: Condvar,
291}
292
293#[derive(Clone)]
294pub struct PtyManager {
295 inner: Arc<PtyManagerInner>,
296 on_signal: Option<SignalHandler>,
297 notifier: Option<PollNotifier>,
298}
299
300impl Default for PtyManager {
301 fn default() -> Self {
302 Self {
303 inner: Arc::new(PtyManagerInner {
304 state: Mutex::new(PtyManagerState::default()),
305 waiters: Condvar::new(),
306 }),
307 on_signal: None,
308 notifier: None,
309 }
310 }
311}
312
313impl PtyManager {
314 pub fn new() -> Self {
315 Self::default()
316 }
317
318 pub fn with_signal_handler(on_signal: SignalHandler) -> Self {
319 let mut manager = Self::new();
320 manager.on_signal = Some(on_signal);
321 manager
322 }
323
324 pub(crate) fn with_signal_handler_and_notifier(
325 on_signal: SignalHandler,
326 notifier: PollNotifier,
327 ) -> Self {
328 let mut manager = Self::with_notifier(notifier);
329 manager.on_signal = Some(on_signal);
330 manager
331 }
332
333 pub(crate) fn with_notifier(notifier: PollNotifier) -> Self {
334 Self {
335 notifier: Some(notifier),
336 ..Self::default()
337 }
338 }
339
340 pub fn create_pty(&self) -> PtyPair {
341 let mut state = lock_or_recover(&self.inner.state);
342 let pty_id = state.next_pty_id;
343 state.next_pty_id += 1;
344
345 let master_id = allocate_file_description_id();
346 let slave_id = allocate_file_description_id();
347
348 let path = format!("/dev/pts/{pty_id}");
349 state.ptys.insert(
350 pty_id,
351 PtyState {
352 path: path.clone(),
353 termios: Termios::default(),
354 window_size: PtyWindowSize::default(),
355 ..PtyState::default()
356 },
357 );
358 state.desc_to_pty.insert(
359 master_id,
360 PtyRef {
361 pty_id,
362 end: PtyEndKind::Master,
363 },
364 );
365 state.desc_to_pty.insert(
366 slave_id,
367 PtyRef {
368 pty_id,
369 end: PtyEndKind::Slave,
370 },
371 );
372 drop(state);
373
374 PtyPair {
375 master: PtyEnd {
376 description: Arc::new(FileDescription::with_ref_count(
377 master_id,
378 format!("pty:{pty_id}:master"),
379 O_RDWR,
380 0,
381 )),
382 filetype: FILETYPE_CHARACTER_DEVICE,
383 },
384 slave: PtyEnd {
385 description: Arc::new(FileDescription::with_ref_count(
386 slave_id,
387 path.clone(),
388 O_RDWR,
389 0,
390 )),
391 filetype: FILETYPE_CHARACTER_DEVICE,
392 },
393 path,
394 }
395 }
396
397 pub fn create_pty_fds(&self, fd_table: &mut ProcessFdTable) -> FdResult<(u32, u32, String)> {
398 let pty = self.create_pty();
399 let master_fd = fd_table.open_with(
400 Arc::clone(&pty.master.description),
401 FILETYPE_CHARACTER_DEVICE,
402 None,
403 )?;
404 match fd_table.open_with(
405 Arc::clone(&pty.slave.description),
406 FILETYPE_CHARACTER_DEVICE,
407 None,
408 ) {
409 Ok(slave_fd) => Ok((master_fd, slave_fd, pty.path)),
410 Err(error) => {
411 fd_table.close(master_fd);
412 self.close(pty.master.description.id());
413 self.close(pty.slave.description.id());
414 Err(error)
415 }
416 }
417 }
418
419 pub fn poll(&self, description_id: u64, requested: PollEvents) -> PtyResult<PollEvents> {
420 let state = lock_or_recover(&self.inner.state);
421 let pty_ref = state
422 .desc_to_pty
423 .get(&description_id)
424 .copied()
425 .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
426 let pty = state
427 .ptys
428 .get(&pty_ref.pty_id)
429 .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?;
430
431 let mut events = PollEvents::empty();
432 match pty_ref.end {
433 PtyEndKind::Master => {
434 if requested.intersects(POLLIN) && !pty.output_buffer.is_empty() {
435 events |= POLLIN;
436 }
437 if pty.closed_slave {
438 events |= POLLHUP;
439 } else if requested.intersects(POLLOUT)
440 && (available_capacity(&pty.input_buffer) > 0
441 || !pty.waiting_input_reads.is_empty())
442 {
443 events |= POLLOUT;
444 }
445 }
446 PtyEndKind::Slave => {
447 if requested.intersects(POLLIN)
448 && (pty.input_eof_pending || !pty.input_buffer.is_empty())
449 {
450 events |= POLLIN;
451 }
452 if pty.closed_master {
453 events |= POLLHUP;
454 } else if requested.intersects(POLLOUT)
455 && (available_capacity(&pty.output_buffer) > 0
456 || !pty.waiting_output_reads.is_empty())
457 {
458 events |= POLLOUT;
459 }
460 }
461 }
462
463 Ok(events)
464 }
465
466 pub fn write(&self, description_id: u64, data: impl AsRef<[u8]>) -> PtyResult<usize> {
467 let payload = data.as_ref();
468 let mut signals = Vec::new();
469
470 {
471 let mut state = lock_or_recover(&self.inner.state);
472 let pty_ref = state
473 .desc_to_pty
474 .get(&description_id)
475 .copied()
476 .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
477 let PtyManagerState { ptys, waiters, .. } = &mut *state;
478 let pty = ptys
479 .get_mut(&pty_ref.pty_id)
480 .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?;
481
482 match pty_ref.end {
483 PtyEndKind::Master => {
484 if pty.closed_master {
485 return Err(PtyError::io("master closed"));
486 }
487 if pty.closed_slave {
488 return Err(PtyError::io("slave closed"));
489 }
490 process_input(pty, waiters, payload, &mut signals)?;
491 }
492 PtyEndKind::Slave => {
493 if pty.closed_slave {
494 return Err(PtyError::io("slave closed"));
495 }
496 if pty.closed_master {
497 return Err(PtyError::io("master closed"));
498 }
499
500 let processed = process_output(&pty.termios, payload);
501 deliver_output(pty, waiters, &processed, false)?;
502 if contains_dsr_cursor_query(payload) {
508 deliver_input(pty, waiters, b"\x1b[1;1R")?;
509 }
510 }
511 }
512 }
513
514 self.notify_waiters_and_pollers();
515 if let Some(on_signal) = &self.on_signal {
516 for (pgid, signal) in signals {
517 if pgid > 0 {
518 on_signal(pgid, signal);
519 }
520 }
521 }
522
523 Ok(payload.len())
524 }
525
526 pub fn read(&self, description_id: u64, length: usize) -> PtyResult<Option<Vec<u8>>> {
527 self.read_with_timeout(description_id, length, None)
528 }
529
530 pub fn read_with_timeout(
531 &self,
532 description_id: u64,
533 length: usize,
534 timeout: Option<Duration>,
535 ) -> PtyResult<Option<Vec<u8>>> {
536 let mut state = lock_or_recover(&self.inner.state);
537 let pty_ref = state
538 .desc_to_pty
539 .get(&description_id)
540 .copied()
541 .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
542 let mut waiter_id = None;
543 let deadline = timeout.map(|duration| Instant::now() + duration);
544
545 loop {
546 if let Some(id) = waiter_id {
547 if let Some(waiter) = state.waiters.get_mut(&id) {
548 if let Some(result) = waiter.result.take() {
549 state.waiters.remove(&id);
550 return Ok(result);
551 }
552 }
553 }
554
555 {
556 let pty = state
557 .ptys
558 .get_mut(&pty_ref.pty_id)
559 .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?;
560
561 match pty_ref.end {
562 PtyEndKind::Master => {
563 if pty.closed_master {
564 if let Some(id) = waiter_id {
565 state.waiters.remove(&id);
566 }
567 return Err(PtyError::io("master closed"));
568 }
569
570 if !pty.output_buffer.is_empty() {
571 let result = drain_buffer(&mut pty.output_buffer, length);
572 if let Some(id) = waiter_id.take() {
576 pty.waiting_input_reads.retain(|queued| *queued != id);
577 pty.waiting_output_reads.retain(|queued| *queued != id);
578 state.waiters.remove(&id);
579 }
580 self.notify_waiters_and_pollers();
581 return Ok(Some(result));
582 }
583
584 if pty.closed_slave {
585 if let Some(id) = waiter_id {
586 state.waiters.remove(&id);
587 }
588 return Ok(None);
589 }
590 }
591 PtyEndKind::Slave => {
592 if pty.closed_slave {
593 if let Some(id) = waiter_id {
594 state.waiters.remove(&id);
595 }
596 return Err(PtyError::io("slave closed"));
597 }
598
599 if !pty.input_buffer.is_empty() {
600 let result = drain_buffer(&mut pty.input_buffer, length);
601 if let Some(id) = waiter_id.take() {
605 pty.waiting_input_reads.retain(|queued| *queued != id);
606 pty.waiting_output_reads.retain(|queued| *queued != id);
607 state.waiters.remove(&id);
608 }
609 self.notify_waiters_and_pollers();
610 return Ok(Some(result));
611 }
612
613 if pty.input_eof_pending {
614 pty.input_eof_pending = false;
615 if let Some(id) = waiter_id {
616 state.waiters.remove(&id);
617 }
618 self.notify_waiters_and_pollers();
619 return Ok(None);
620 }
621
622 if pty.closed_master {
623 if let Some(id) = waiter_id {
624 state.waiters.remove(&id);
625 }
626 return Ok(None);
627 }
628 }
629 }
630 }
631
632 if waiter_id.is_none() && deadline.is_some_and(|deadline| Instant::now() >= deadline) {
637 return Err(PtyError::would_block("PTY read timed out"));
638 }
639
640 let id = if let Some(id) = waiter_id {
641 id
642 } else {
643 let next = state.next_waiter_id;
644 state.next_waiter_id += 1;
645 state.waiters.insert(
646 next,
647 PendingRead {
648 length,
649 result: None,
650 },
651 );
652 let Some(pty) = state.ptys.get_mut(&pty_ref.pty_id) else {
653 state.waiters.remove(&next);
654 return Err(PtyError::bad_file_descriptor("PTY not found"));
655 };
656 match pty_ref.end {
657 PtyEndKind::Master => pty.waiting_output_reads.push_back(next),
658 PtyEndKind::Slave => pty.waiting_input_reads.push_back(next),
659 }
660 self.notify_waiters_and_pollers();
661 waiter_id = Some(next);
662 next
663 };
664
665 let Some(deadline) = deadline else {
666 state = wait_or_recover(&self.inner.waiters, state);
667 if !state.waiters.contains_key(&id) {
668 waiter_id = None;
669 }
670 continue;
671 };
672
673 let now = Instant::now();
674 if now >= deadline {
675 if let Some(id) = waiter_id.take() {
676 state.waiters.remove(&id);
677 if let Some(pty) = state.ptys.get_mut(&pty_ref.pty_id) {
678 pty.waiting_input_reads.retain(|queued| *queued != id);
679 pty.waiting_output_reads.retain(|queued| *queued != id);
680 }
681 self.notify_waiters_and_pollers();
682 }
683 return Err(PtyError::would_block("PTY read timed out"));
684 }
685
686 let remaining = deadline.saturating_duration_since(now);
687 let (next_state, wait_result) =
688 wait_timeout_or_recover(&self.inner.waiters, state, remaining);
689 state = next_state;
690 if !state.waiters.contains_key(&id) {
691 waiter_id = None;
692 }
693 if wait_result.timed_out() {
694 if let Some(id) = waiter_id.take() {
695 state.waiters.remove(&id);
696 if let Some(pty) = state.ptys.get_mut(&pty_ref.pty_id) {
697 pty.waiting_input_reads.retain(|queued| *queued != id);
698 pty.waiting_output_reads.retain(|queued| *queued != id);
699 }
700 self.notify_waiters_and_pollers();
701 }
702 return Err(PtyError::would_block("PTY read timed out"));
703 }
704 }
705 }
706
707 pub fn close(&self, description_id: u64) {
708 let mut state = lock_or_recover(&self.inner.state);
709 let Some(pty_ref) = state.desc_to_pty.remove(&description_id) else {
710 return;
711 };
712
713 let (waiter_ids, remove_pty) = if let Some(pty) = state.ptys.get_mut(&pty_ref.pty_id) {
714 match pty_ref.end {
715 PtyEndKind::Master => {
716 pty.closed_master = true;
717 let mut waiters = pty.waiting_input_reads.drain(..).collect::<Vec<_>>();
718 waiters.extend(pty.waiting_output_reads.drain(..));
719 (waiters, pty.closed_master && pty.closed_slave)
720 }
721 PtyEndKind::Slave => {
722 pty.closed_slave = true;
723 let mut waiters = pty.waiting_output_reads.drain(..).collect::<Vec<_>>();
724 waiters.extend(pty.waiting_input_reads.drain(..));
725 (waiters, pty.closed_master && pty.closed_slave)
726 }
727 }
728 } else {
729 (Vec::new(), false)
730 };
731
732 for waiter_id in waiter_ids {
733 if let Some(waiter) = state.waiters.get_mut(&waiter_id) {
734 waiter.result = Some(None);
735 }
736 }
737
738 if remove_pty {
739 state.ptys.remove(&pty_ref.pty_id);
740 }
741 self.notify_waiters_and_pollers();
742 }
743
744 pub fn is_pty(&self, description_id: u64) -> bool {
745 lock_or_recover(&self.inner.state)
746 .desc_to_pty
747 .contains_key(&description_id)
748 }
749
750 pub fn is_slave(&self, description_id: u64) -> bool {
751 lock_or_recover(&self.inner.state)
752 .desc_to_pty
753 .get(&description_id)
754 .map(|pty_ref| pty_ref.end == PtyEndKind::Slave)
755 .unwrap_or(false)
756 }
757
758 pub fn set_discipline(
759 &self,
760 description_id: u64,
761 config: LineDisciplineConfig,
762 ) -> PtyResult<()> {
763 let mut state = lock_or_recover(&self.inner.state);
764 let pty_ref = state
765 .desc_to_pty
766 .get(&description_id)
767 .copied()
768 .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
769 let pty = state
770 .ptys
771 .get_mut(&pty_ref.pty_id)
772 .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?;
773 pty.termios_generation = pty
774 .termios_generation
775 .checked_add(1)
776 .ok_or_else(|| PtyError::io("PTY terminal-attribute generation counter exhausted"))?;
777 if let Some(canonical) = config.canonical {
778 pty.termios.icanon = canonical;
779 }
780 if let Some(icrnl) = config.icrnl {
781 pty.termios.icrnl = icrnl;
782 }
783 if let Some(echo) = config.echo {
784 pty.termios.echo = echo;
785 }
786 if let Some(isig) = config.isig {
787 pty.termios.isig = isig;
788 }
789 if let Some(opost) = config.opost {
790 pty.termios.opost = opost;
791 }
792 if let Some(onlcr) = config.onlcr {
793 pty.termios.onlcr = onlcr;
794 }
795 Ok(())
796 }
797
798 pub fn set_raw_mode(
805 &self,
806 description_id: u64,
807 lease_owner_pid: Option<u32>,
808 enabled: bool,
809 ) -> PtyResult<Option<u64>> {
810 let mut state = lock_or_recover(&self.inner.state);
811 let pty_ref = state
812 .desc_to_pty
813 .get(&description_id)
814 .copied()
815 .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
816 let pty = state
817 .ptys
818 .get_mut(&pty_ref.pty_id)
819 .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?;
820
821 if !enabled {
822 if let Some(owner_pid) = lease_owner_pid {
823 if release_raw_mode_lease(pty, owner_pid, None)? {
824 return Ok(None);
825 }
826 }
827 advance_termios_generation(pty)?;
828 apply_raw_mode(&mut pty.termios, false);
829 return Ok(None);
830 }
831
832 let Some(owner_pid) = lease_owner_pid else {
833 advance_termios_generation(pty)?;
834 apply_raw_mode(&mut pty.termios, true);
835 return Ok(None);
836 };
837
838 if let Some(index) = pty
842 .raw_mode_leases
843 .iter()
844 .position(|lease| lease.owner_pid == owner_pid)
845 {
846 if index + 1 == pty.raw_mode_leases.len() {
847 advance_termios_generation(pty)?;
848 apply_raw_mode(&mut pty.termios, true);
849 let lease = pty
850 .raw_mode_leases
851 .last_mut()
852 .expect("raw-mode lease index was just validated");
853 lease.applied_termios_generation = pty.termios_generation;
854 return Ok(Some(lease.generation));
855 }
856 let generation = pty.raw_mode_leases[index].generation;
857 release_raw_mode_lease(pty, owner_pid, Some(generation))?;
858 }
859
860 let generation = pty
861 .next_raw_mode_generation
862 .checked_add(1)
863 .ok_or_else(|| PtyError::io("PTY raw-mode generation counter exhausted"))?;
864 pty.next_raw_mode_generation = generation;
865 let restore_termios = pty.termios.clone();
866 advance_termios_generation(pty)?;
867 apply_raw_mode(&mut pty.termios, true);
868 pty.raw_mode_leases.push(RawModeLease {
869 owner_pid,
870 generation,
871 applied_termios_generation: pty.termios_generation,
872 restore_termios,
873 });
874 Ok(Some(generation))
875 }
876
877 pub fn release_raw_mode(
882 &self,
883 description_id: u64,
884 owner_pid: u32,
885 generation: u64,
886 ) -> PtyResult<bool> {
887 let mut state = lock_or_recover(&self.inner.state);
888 let pty_ref = state
889 .desc_to_pty
890 .get(&description_id)
891 .copied()
892 .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
893 let pty = state
894 .ptys
895 .get_mut(&pty_ref.pty_id)
896 .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?;
897 release_raw_mode_lease(pty, owner_pid, Some(generation))
898 }
899
900 pub fn get_termios(&self, description_id: u64) -> PtyResult<Termios> {
901 let state = lock_or_recover(&self.inner.state);
902 let pty_ref = state
903 .desc_to_pty
904 .get(&description_id)
905 .copied()
906 .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
907 state
908 .ptys
909 .get(&pty_ref.pty_id)
910 .cloned()
911 .map(|pty| pty.termios)
912 .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))
913 }
914
915 pub fn set_termios(&self, description_id: u64, termios: PartialTermios) -> PtyResult<()> {
916 let mut state = lock_or_recover(&self.inner.state);
917 let pty_ref = state
918 .desc_to_pty
919 .get(&description_id)
920 .copied()
921 .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
922 let pty = state
923 .ptys
924 .get_mut(&pty_ref.pty_id)
925 .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?;
926 advance_termios_generation(pty)?;
927 pty.termios.merge(termios);
928 Ok(())
929 }
930
931 pub fn set_foreground_pgid(&self, description_id: u64, pgid: u32) -> PtyResult<()> {
932 let mut state = lock_or_recover(&self.inner.state);
933 let pty_ref = state
934 .desc_to_pty
935 .get(&description_id)
936 .copied()
937 .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
938 let pty = state
939 .ptys
940 .get_mut(&pty_ref.pty_id)
941 .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?;
942 pty.foreground_pgid = pgid;
943 Ok(())
944 }
945
946 pub fn get_foreground_pgid(&self, description_id: u64) -> PtyResult<u32> {
947 let state = lock_or_recover(&self.inner.state);
948 let pty_ref = state
949 .desc_to_pty
950 .get(&description_id)
951 .copied()
952 .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
953 state
954 .ptys
955 .get(&pty_ref.pty_id)
956 .map(|pty| pty.foreground_pgid)
957 .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))
958 }
959
960 pub fn window_size(&self, description_id: u64) -> PtyResult<PtyWindowSize> {
961 let state = lock_or_recover(&self.inner.state);
962 let pty_ref = state
963 .desc_to_pty
964 .get(&description_id)
965 .copied()
966 .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
967 state
968 .ptys
969 .get(&pty_ref.pty_id)
970 .map(|pty| pty.window_size)
971 .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))
972 }
973
974 pub fn resize(&self, description_id: u64, cols: u16, rows: u16) -> PtyResult<Option<u32>> {
975 let mut state = lock_or_recover(&self.inner.state);
976 let pty_ref = state
977 .desc_to_pty
978 .get(&description_id)
979 .copied()
980 .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
981 let pty = state
982 .ptys
983 .get_mut(&pty_ref.pty_id)
984 .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?;
985 let next_size = PtyWindowSize { cols, rows };
986 if pty.window_size == next_size {
987 return Ok(None);
988 }
989 pty.window_size = next_size;
990 Ok((pty.foreground_pgid > 0).then_some(pty.foreground_pgid))
991 }
992
993 pub fn pty_count(&self) -> usize {
994 lock_or_recover(&self.inner.state).ptys.len()
995 }
996
997 pub fn buffered_input_bytes(&self) -> usize {
998 lock_or_recover(&self.inner.state)
999 .ptys
1000 .values()
1001 .map(|pty| buffer_size(&pty.input_buffer))
1002 .sum()
1003 }
1004
1005 pub fn buffered_output_bytes(&self) -> usize {
1006 lock_or_recover(&self.inner.state)
1007 .ptys
1008 .values()
1009 .map(|pty| buffer_size(&pty.output_buffer))
1010 .sum()
1011 }
1012
1013 pub fn pending_read_waiter_count(&self) -> usize {
1014 lock_or_recover(&self.inner.state).waiters.len()
1015 }
1016
1017 pub fn queued_read_waiter_count(&self) -> usize {
1018 lock_or_recover(&self.inner.state)
1019 .ptys
1020 .values()
1021 .map(|pty| pty.waiting_input_reads.len() + pty.waiting_output_reads.len())
1022 .sum()
1023 }
1024
1025 pub fn path_for(&self, description_id: u64) -> Option<String> {
1026 let state = lock_or_recover(&self.inner.state);
1027 let pty_ref = state.desc_to_pty.get(&description_id)?;
1028 state.ptys.get(&pty_ref.pty_id).map(|pty| pty.path.clone())
1029 }
1030
1031 fn notify_waiters_and_pollers(&self) {
1032 self.inner.waiters.notify_all();
1033 if let Some(notifier) = &self.notifier {
1034 notifier.notify();
1035 }
1036 }
1037}
1038
1039fn contains_dsr_cursor_query(data: &[u8]) -> bool {
1042 const QUERY: &[u8] = b"\x1b[6n";
1043 data.windows(QUERY.len()).any(|window| window == QUERY)
1044}
1045
1046fn process_output(termios: &Termios, data: &[u8]) -> Vec<u8> {
1047 if !termios.opost || !termios.onlcr || !data.contains(&b'\n') {
1048 return data.to_vec();
1049 }
1050
1051 let extra_crs = data
1052 .iter()
1053 .enumerate()
1054 .filter(|(index, byte)| **byte == b'\n' && (*index == 0 || data[*index - 1] != b'\r'))
1055 .count();
1056 if extra_crs == 0 {
1057 return data.to_vec();
1058 }
1059
1060 let mut result = Vec::with_capacity(data.len() + extra_crs);
1061 for (index, byte) in data.iter().enumerate() {
1062 if *byte == b'\n' && (index == 0 || data[index - 1] != b'\r') {
1063 result.push(b'\r');
1064 }
1065 result.push(*byte);
1066 }
1067 result
1068}
1069
1070fn process_input(
1071 pty: &mut PtyState,
1072 waiters: &mut BTreeMap<u64, PendingRead>,
1073 data: &[u8],
1074 signals: &mut Vec<(u32, i32)>,
1075) -> PtyResult<()> {
1076 if !pty.termios.icanon && !pty.termios.echo && !pty.termios.isig {
1077 let translated = translate_input(&pty.termios, data);
1078 deliver_input(pty, waiters, &translated)?;
1079 return Ok(());
1080 }
1081
1082 for mut byte in data.iter().copied() {
1083 if pty.termios.icrnl && byte == b'\r' {
1084 byte = b'\n';
1085 }
1086
1087 if pty.termios.isig {
1088 if let Some(signal) = signal_for_byte(&pty.termios, byte) {
1089 if pty.termios.icanon {
1090 pty.line_buffer.clear();
1091 }
1092 let has_foreground_process_group = pty.foreground_pgid > 0;
1093 if pty.termios.echo && !has_foreground_process_group {
1099 deliver_output(pty, waiters, &echo_control_byte(byte), true)?;
1100 if pty.termios.icanon {
1101 deliver_output(pty, waiters, b"\r\n", true)?;
1102 }
1103 }
1104 if has_foreground_process_group {
1105 signals.push((pty.foreground_pgid, signal));
1106 } else if pty.termios.icanon {
1107 deliver_input(pty, waiters, b"\n")?;
1108 }
1109 continue;
1110 }
1111 }
1112
1113 if pty.termios.icanon {
1114 if byte == pty.termios.cc.veof {
1115 if pty.line_buffer.is_empty() {
1116 deliver_input_eof(pty, waiters);
1117 } else {
1118 let line = pty.line_buffer.clone();
1119 deliver_input(pty, waiters, &line)?;
1120 pty.line_buffer.clear();
1121 }
1122 continue;
1123 }
1124
1125 if byte == pty.termios.cc.verase || byte == 0x08 {
1126 if let Some(&erased) = pty.line_buffer.last() {
1127 if pty.termios.echo {
1128 deliver_output(pty, waiters, &erase_sequence(erased), true)?;
1129 }
1130 pty.line_buffer.pop();
1131 }
1132 continue;
1133 }
1134
1135 if byte == pty.termios.cc.vkill {
1136 if !pty.line_buffer.is_empty() {
1137 if pty.termios.echo {
1138 let erase: Vec<u8> = pty
1139 .line_buffer
1140 .iter()
1141 .flat_map(|b| erase_sequence(*b))
1142 .collect();
1143 deliver_output(pty, waiters, &erase, true)?;
1144 }
1145 pty.line_buffer.clear();
1146 }
1147 continue;
1148 }
1149
1150 if byte == pty.termios.cc.vwerase {
1151 let mut erased: Vec<u8> = Vec::new();
1152 while matches!(pty.line_buffer.last(), Some(b' ') | Some(b'\t')) {
1153 if let Some(b) = pty.line_buffer.pop() {
1154 erased.push(b);
1155 }
1156 }
1157 while let Some(&b) = pty.line_buffer.last() {
1158 if b == b' ' || b == b'\t' {
1159 break;
1160 }
1161 pty.line_buffer.pop();
1162 erased.push(b);
1163 }
1164 if pty.termios.echo && !erased.is_empty() {
1165 let sequence: Vec<u8> =
1166 erased.iter().flat_map(|b| erase_sequence(*b)).collect();
1167 deliver_output(pty, waiters, &sequence, true)?;
1168 }
1169 continue;
1170 }
1171
1172 if byte == b'\n' {
1173 let mut line = pty.line_buffer.clone();
1174 line.push(b'\n');
1175 if pty.termios.echo {
1176 deliver_output(pty, waiters, b"\r\n", true)?;
1177 }
1178 deliver_input(pty, waiters, &line)?;
1179 pty.line_buffer.clear();
1180 continue;
1181 }
1182
1183 if pty.line_buffer.len() >= MAX_CANON {
1184 continue;
1185 }
1186 if pty.termios.echo {
1187 deliver_output(pty, waiters, &echo_control_byte(byte), true)?;
1190 }
1191 pty.line_buffer.push(byte);
1192 } else {
1193 if pty.termios.echo {
1194 deliver_output(pty, waiters, &[byte], true)?;
1195 }
1196 deliver_input(pty, waiters, &[byte])?;
1197 }
1198 }
1199
1200 Ok(())
1201}
1202
1203fn translate_input(termios: &Termios, data: &[u8]) -> Vec<u8> {
1204 if !termios.icrnl || !data.contains(&b'\r') {
1205 return data.to_vec();
1206 }
1207
1208 data.iter()
1209 .map(|byte| if *byte == b'\r' { b'\n' } else { *byte })
1210 .collect()
1211}
1212
1213fn deliver_input(
1214 pty: &mut PtyState,
1215 waiters: &mut BTreeMap<u64, PendingRead>,
1216 data: &[u8],
1217) -> PtyResult<()> {
1218 if let Some(waiter_id) = pty.waiting_input_reads.pop_front() {
1219 if let Some(waiter) = waiters.get_mut(&waiter_id) {
1220 if data.len() <= waiter.length {
1221 waiter.result = Some(Some(data.to_vec()));
1222 } else {
1223 let tail_len = data.len() - waiter.length;
1228 if tail_len > available_capacity(&pty.input_buffer) {
1229 pty.waiting_input_reads.push_front(waiter_id);
1230 return Err(PtyError::would_block("PTY input buffer full"));
1231 }
1232 let (head, tail) = data.split_at(waiter.length);
1233 waiter.result = Some(Some(head.to_vec()));
1234 pty.input_buffer.push_front(tail.to_vec());
1235 }
1236 return Ok(());
1237 }
1238 }
1239
1240 if buffer_size(&pty.input_buffer).saturating_add(data.len()) > MAX_PTY_BUFFER_BYTES {
1241 return Err(PtyError::would_block("PTY input buffer full"));
1242 }
1243
1244 pty.input_buffer.push_back(data.to_vec());
1245 Ok(())
1246}
1247
1248fn deliver_input_eof(pty: &mut PtyState, waiters: &mut BTreeMap<u64, PendingRead>) {
1249 if let Some(waiter_id) = pty.waiting_input_reads.pop_front() {
1250 if let Some(waiter) = waiters.get_mut(&waiter_id) {
1251 waiter.result = Some(None);
1252 return;
1253 }
1254 }
1255
1256 pty.input_eof_pending = true;
1257}
1258
1259fn deliver_output(
1260 pty: &mut PtyState,
1261 waiters: &mut BTreeMap<u64, PendingRead>,
1262 data: &[u8],
1263 echo: bool,
1264) -> PtyResult<()> {
1265 if let Some(waiter_id) = pty.waiting_output_reads.pop_front() {
1266 if let Some(waiter) = waiters.get_mut(&waiter_id) {
1267 if data.len() <= waiter.length {
1268 waiter.result = Some(Some(data.to_vec()));
1269 } else {
1270 let tail_len = data.len() - waiter.length;
1272 if tail_len > available_capacity(&pty.output_buffer) {
1273 pty.waiting_output_reads.push_front(waiter_id);
1274 let message = if echo {
1275 "PTY output buffer full (echo backpressure)"
1276 } else {
1277 "PTY output buffer full"
1278 };
1279 return Err(PtyError::would_block(message));
1280 }
1281 let (head, tail) = data.split_at(waiter.length);
1282 waiter.result = Some(Some(head.to_vec()));
1283 pty.output_buffer.push_front(tail.to_vec());
1284 }
1285 return Ok(());
1286 }
1287 }
1288
1289 if buffer_size(&pty.output_buffer).saturating_add(data.len()) > MAX_PTY_BUFFER_BYTES {
1290 let message = if echo {
1291 "PTY output buffer full (echo backpressure)"
1292 } else {
1293 "PTY output buffer full"
1294 };
1295 return Err(PtyError::would_block(message));
1296 }
1297
1298 pty.output_buffer.push_back(data.to_vec());
1299 Ok(())
1300}
1301
1302fn advance_termios_generation(pty: &mut PtyState) -> PtyResult<()> {
1303 pty.termios_generation = pty
1304 .termios_generation
1305 .checked_add(1)
1306 .ok_or_else(|| PtyError::io("PTY terminal-attribute generation counter exhausted"))?;
1307 Ok(())
1308}
1309
1310fn apply_raw_mode(termios: &mut Termios, enabled: bool) {
1311 termios.icrnl = !enabled;
1312 termios.icanon = !enabled;
1313 termios.echo = !enabled;
1314 termios.isig = !enabled;
1315 termios.opost = !enabled;
1316 termios.onlcr = !enabled;
1317}
1318
1319fn release_raw_mode_lease(
1320 pty: &mut PtyState,
1321 owner_pid: u32,
1322 expected_generation: Option<u64>,
1323) -> PtyResult<bool> {
1324 let Some(index) = pty.raw_mode_leases.iter().position(|lease| {
1325 lease.owner_pid == owner_pid
1326 && expected_generation.is_none_or(|generation| lease.generation == generation)
1327 }) else {
1328 return Ok(false);
1329 };
1330
1331 let was_top = index + 1 == pty.raw_mode_leases.len();
1332 let lease = pty.raw_mode_leases.remove(index);
1333 if !was_top {
1334 pty.raw_mode_leases[index].restore_termios = lease.restore_termios;
1338 return Ok(true);
1339 }
1340
1341 if pty.termios_generation != lease.applied_termios_generation {
1344 return Ok(true);
1345 }
1346
1347 advance_termios_generation(pty)?;
1348 pty.termios = lease.restore_termios;
1349 let restored_generation = pty.termios_generation;
1350 if let Some(previous) = pty.raw_mode_leases.last_mut() {
1351 previous.applied_termios_generation = restored_generation;
1354 }
1355 Ok(true)
1356}
1357
1358fn signal_for_byte(termios: &Termios, byte: u8) -> Option<i32> {
1359 if byte == termios.cc.vintr {
1360 return Some(SIGINT);
1361 }
1362 if byte == termios.cc.vquit {
1363 return Some(SIGQUIT);
1364 }
1365 if byte == termios.cc.vsusp {
1366 return Some(SIGTSTP);
1367 }
1368 None
1369}
1370
1371fn echo_control_byte(byte: u8) -> Vec<u8> {
1372 if byte < 0x20 {
1373 vec![b'^', byte + 0x40]
1374 } else if byte == 0x7f {
1375 b"^?".to_vec()
1376 } else {
1377 vec![byte]
1378 }
1379}
1380
1381fn erase_sequence(byte: u8) -> Vec<u8> {
1387 let columns = echo_control_byte(byte).len();
1388 (0..columns).flat_map(|_| [0x08, 0x20, 0x08]).collect()
1389}
1390
1391fn buffer_size(buffer: &VecDeque<Vec<u8>>) -> usize {
1392 buffer.iter().map(Vec::len).sum()
1393}
1394
1395fn available_capacity(buffer: &VecDeque<Vec<u8>>) -> usize {
1396 MAX_PTY_BUFFER_BYTES.saturating_sub(buffer_size(buffer))
1397}
1398
1399fn drain_buffer(buffer: &mut VecDeque<Vec<u8>>, length: usize) -> Vec<u8> {
1400 let mut chunks = Vec::new();
1401 let mut remaining = length;
1402
1403 while remaining > 0 {
1404 let Some(chunk) = buffer.pop_front() else {
1405 break;
1406 };
1407 if chunk.len() <= remaining {
1408 remaining -= chunk.len();
1409 chunks.push(chunk);
1410 } else {
1411 let (head, tail) = chunk.split_at(remaining);
1412 chunks.push(head.to_vec());
1413 buffer.push_front(tail.to_vec());
1414 remaining = 0;
1415 }
1416 }
1417
1418 if chunks.len() == 1 {
1419 return chunks.pop().expect("single chunk should exist");
1420 }
1421
1422 let total = chunks.iter().map(Vec::len).sum();
1423 let mut result = Vec::with_capacity(total);
1424 for chunk in chunks {
1425 result.extend_from_slice(&chunk);
1426 }
1427 result
1428}
1429
1430fn lock_or_recover<'a, T>(mutex: &'a Mutex<T>) -> MutexGuard<'a, T> {
1431 match mutex.lock() {
1432 Ok(guard) => guard,
1433 Err(poisoned) => poisoned.into_inner(),
1434 }
1435}
1436
1437fn wait_or_recover<'a, T>(condvar: &Condvar, guard: MutexGuard<'a, T>) -> MutexGuard<'a, T> {
1438 match condvar.wait(guard) {
1439 Ok(guard) => guard,
1440 Err(poisoned) => poisoned.into_inner(),
1441 }
1442}
1443
1444fn wait_timeout_or_recover<'a, T>(
1445 condvar: &Condvar,
1446 guard: MutexGuard<'a, T>,
1447 timeout: Duration,
1448) -> (MutexGuard<'a, T>, std::sync::WaitTimeoutResult) {
1449 match condvar.wait_timeout(guard, timeout) {
1450 Ok(result) => result,
1451 Err(poisoned) => poisoned.into_inner(),
1452 }
1453}
1454
1455#[cfg(test)]
1456mod tests {
1457 use super::*;
1458
1459 #[test]
1460 fn zero_timeout_empty_read_does_not_publish_false_readiness() {
1461 let notifier = PollNotifier::default();
1462 let manager = PtyManager::with_notifier(notifier.clone());
1463 let pty = manager.create_pty();
1464 let observed = notifier.snapshot();
1465
1466 let error = manager
1467 .read_with_timeout(pty.slave.description.id(), 1, Some(Duration::ZERO))
1468 .expect_err("empty nonblocking read must return EAGAIN");
1469
1470 assert_eq!(error.code(), "EAGAIN");
1471 assert_eq!(notifier.snapshot(), observed);
1472 let state = lock_or_recover(&manager.inner.state);
1473 assert!(state.waiters.is_empty());
1474 assert_eq!(state.next_waiter_id, 1);
1475 }
1476}