1use crate::fd_table::{
2 FdResult, FileDescription, ProcessFdTable, SharedFileDescription, FILETYPE_CHARACTER_DEVICE,
3 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_desc_id: u64,
273 next_waiter_id: u64,
274}
275
276impl Default for PtyManagerState {
277 fn default() -> Self {
278 Self {
279 ptys: BTreeMap::new(),
280 desc_to_pty: BTreeMap::new(),
281 waiters: BTreeMap::new(),
282 next_pty_id: 0,
283 next_desc_id: 200_000,
284 next_waiter_id: 1,
285 }
286 }
287}
288
289#[derive(Debug)]
290struct PtyManagerInner {
291 state: Mutex<PtyManagerState>,
292 waiters: Condvar,
293}
294
295#[derive(Clone)]
296pub struct PtyManager {
297 inner: Arc<PtyManagerInner>,
298 on_signal: Option<SignalHandler>,
299 notifier: Option<PollNotifier>,
300}
301
302impl Default for PtyManager {
303 fn default() -> Self {
304 Self {
305 inner: Arc::new(PtyManagerInner {
306 state: Mutex::new(PtyManagerState::default()),
307 waiters: Condvar::new(),
308 }),
309 on_signal: None,
310 notifier: None,
311 }
312 }
313}
314
315impl PtyManager {
316 pub fn new() -> Self {
317 Self::default()
318 }
319
320 pub fn with_signal_handler(on_signal: SignalHandler) -> Self {
321 let mut manager = Self::new();
322 manager.on_signal = Some(on_signal);
323 manager
324 }
325
326 pub(crate) fn with_signal_handler_and_notifier(
327 on_signal: SignalHandler,
328 notifier: PollNotifier,
329 ) -> Self {
330 let mut manager = Self::with_notifier(notifier);
331 manager.on_signal = Some(on_signal);
332 manager
333 }
334
335 pub(crate) fn with_notifier(notifier: PollNotifier) -> Self {
336 Self {
337 notifier: Some(notifier),
338 ..Self::default()
339 }
340 }
341
342 pub fn create_pty(&self) -> PtyPair {
343 let mut state = lock_or_recover(&self.inner.state);
344 let pty_id = state.next_pty_id;
345 state.next_pty_id += 1;
346
347 let master_id = state.next_desc_id;
348 state.next_desc_id += 1;
349 let slave_id = state.next_desc_id;
350 state.next_desc_id += 1;
351
352 let path = format!("/dev/pts/{pty_id}");
353 state.ptys.insert(
354 pty_id,
355 PtyState {
356 path: path.clone(),
357 termios: Termios::default(),
358 window_size: PtyWindowSize::default(),
359 ..PtyState::default()
360 },
361 );
362 state.desc_to_pty.insert(
363 master_id,
364 PtyRef {
365 pty_id,
366 end: PtyEndKind::Master,
367 },
368 );
369 state.desc_to_pty.insert(
370 slave_id,
371 PtyRef {
372 pty_id,
373 end: PtyEndKind::Slave,
374 },
375 );
376 drop(state);
377
378 PtyPair {
379 master: PtyEnd {
380 description: Arc::new(FileDescription::with_ref_count(
381 master_id,
382 format!("pty:{pty_id}:master"),
383 O_RDWR,
384 0,
385 )),
386 filetype: FILETYPE_CHARACTER_DEVICE,
387 },
388 slave: PtyEnd {
389 description: Arc::new(FileDescription::with_ref_count(
390 slave_id,
391 path.clone(),
392 O_RDWR,
393 0,
394 )),
395 filetype: FILETYPE_CHARACTER_DEVICE,
396 },
397 path,
398 }
399 }
400
401 pub fn create_pty_fds(&self, fd_table: &mut ProcessFdTable) -> FdResult<(u32, u32, String)> {
402 let pty = self.create_pty();
403 let master_fd = fd_table.open_with(
404 Arc::clone(&pty.master.description),
405 FILETYPE_CHARACTER_DEVICE,
406 None,
407 )?;
408 match fd_table.open_with(
409 Arc::clone(&pty.slave.description),
410 FILETYPE_CHARACTER_DEVICE,
411 None,
412 ) {
413 Ok(slave_fd) => Ok((master_fd, slave_fd, pty.path)),
414 Err(error) => {
415 fd_table.close(master_fd);
416 self.close(pty.master.description.id());
417 self.close(pty.slave.description.id());
418 Err(error)
419 }
420 }
421 }
422
423 pub fn poll(&self, description_id: u64, requested: PollEvents) -> PtyResult<PollEvents> {
424 let state = lock_or_recover(&self.inner.state);
425 let pty_ref = state
426 .desc_to_pty
427 .get(&description_id)
428 .copied()
429 .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
430 let pty = state
431 .ptys
432 .get(&pty_ref.pty_id)
433 .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?;
434
435 let mut events = PollEvents::empty();
436 match pty_ref.end {
437 PtyEndKind::Master => {
438 if requested.intersects(POLLIN) && !pty.output_buffer.is_empty() {
439 events |= POLLIN;
440 }
441 if pty.closed_slave {
442 events |= POLLHUP;
443 } else if requested.intersects(POLLOUT)
444 && (available_capacity(&pty.input_buffer) > 0
445 || !pty.waiting_input_reads.is_empty())
446 {
447 events |= POLLOUT;
448 }
449 }
450 PtyEndKind::Slave => {
451 if requested.intersects(POLLIN)
452 && (pty.input_eof_pending || !pty.input_buffer.is_empty())
453 {
454 events |= POLLIN;
455 }
456 if pty.closed_master {
457 events |= POLLHUP;
458 } else if requested.intersects(POLLOUT)
459 && (available_capacity(&pty.output_buffer) > 0
460 || !pty.waiting_output_reads.is_empty())
461 {
462 events |= POLLOUT;
463 }
464 }
465 }
466
467 Ok(events)
468 }
469
470 pub fn write(&self, description_id: u64, data: impl AsRef<[u8]>) -> PtyResult<usize> {
471 let payload = data.as_ref();
472 let mut signals = Vec::new();
473
474 {
475 let mut state = lock_or_recover(&self.inner.state);
476 let pty_ref = state
477 .desc_to_pty
478 .get(&description_id)
479 .copied()
480 .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
481 let PtyManagerState { ptys, waiters, .. } = &mut *state;
482 let pty = ptys
483 .get_mut(&pty_ref.pty_id)
484 .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?;
485
486 match pty_ref.end {
487 PtyEndKind::Master => {
488 if pty.closed_master {
489 return Err(PtyError::io("master closed"));
490 }
491 if pty.closed_slave {
492 return Err(PtyError::io("slave closed"));
493 }
494 process_input(pty, waiters, payload, &mut signals)?;
495 }
496 PtyEndKind::Slave => {
497 if pty.closed_slave {
498 return Err(PtyError::io("slave closed"));
499 }
500 if pty.closed_master {
501 return Err(PtyError::io("master closed"));
502 }
503
504 let processed = process_output(&pty.termios, payload);
505 deliver_output(pty, waiters, &processed, false)?;
506 if contains_dsr_cursor_query(payload) {
512 deliver_input(pty, waiters, b"\x1b[1;1R")?;
513 }
514 }
515 }
516 }
517
518 self.notify_waiters_and_pollers();
519 if let Some(on_signal) = &self.on_signal {
520 for (pgid, signal) in signals {
521 if pgid > 0 {
522 on_signal(pgid, signal);
523 }
524 }
525 }
526
527 Ok(payload.len())
528 }
529
530 pub fn read(&self, description_id: u64, length: usize) -> PtyResult<Option<Vec<u8>>> {
531 self.read_with_timeout(description_id, length, None)
532 }
533
534 pub fn read_with_timeout(
535 &self,
536 description_id: u64,
537 length: usize,
538 timeout: Option<Duration>,
539 ) -> PtyResult<Option<Vec<u8>>> {
540 let mut state = lock_or_recover(&self.inner.state);
541 let pty_ref = state
542 .desc_to_pty
543 .get(&description_id)
544 .copied()
545 .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
546 let mut waiter_id = None;
547 let deadline = timeout.map(|duration| Instant::now() + duration);
548
549 loop {
550 if let Some(id) = waiter_id {
551 if let Some(waiter) = state.waiters.get_mut(&id) {
552 if let Some(result) = waiter.result.take() {
553 state.waiters.remove(&id);
554 return Ok(result);
555 }
556 }
557 }
558
559 {
560 let pty = state
561 .ptys
562 .get_mut(&pty_ref.pty_id)
563 .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?;
564
565 match pty_ref.end {
566 PtyEndKind::Master => {
567 if pty.closed_master {
568 if let Some(id) = waiter_id {
569 state.waiters.remove(&id);
570 }
571 return Err(PtyError::io("master closed"));
572 }
573
574 if !pty.output_buffer.is_empty() {
575 let result = drain_buffer(&mut pty.output_buffer, length);
576 if let Some(id) = waiter_id.take() {
580 pty.waiting_input_reads.retain(|queued| *queued != id);
581 pty.waiting_output_reads.retain(|queued| *queued != id);
582 state.waiters.remove(&id);
583 }
584 self.notify_waiters_and_pollers();
585 return Ok(Some(result));
586 }
587
588 if pty.closed_slave {
589 if let Some(id) = waiter_id {
590 state.waiters.remove(&id);
591 }
592 return Ok(None);
593 }
594 }
595 PtyEndKind::Slave => {
596 if pty.closed_slave {
597 if let Some(id) = waiter_id {
598 state.waiters.remove(&id);
599 }
600 return Err(PtyError::io("slave closed"));
601 }
602
603 if !pty.input_buffer.is_empty() {
604 let result = drain_buffer(&mut pty.input_buffer, length);
605 if let Some(id) = waiter_id.take() {
609 pty.waiting_input_reads.retain(|queued| *queued != id);
610 pty.waiting_output_reads.retain(|queued| *queued != id);
611 state.waiters.remove(&id);
612 }
613 self.notify_waiters_and_pollers();
614 return Ok(Some(result));
615 }
616
617 if pty.input_eof_pending {
618 pty.input_eof_pending = false;
619 if let Some(id) = waiter_id {
620 state.waiters.remove(&id);
621 }
622 self.notify_waiters_and_pollers();
623 return Ok(None);
624 }
625
626 if pty.closed_master {
627 if let Some(id) = waiter_id {
628 state.waiters.remove(&id);
629 }
630 return Ok(None);
631 }
632 }
633 }
634 }
635
636 let id = if let Some(id) = waiter_id {
637 id
638 } else {
639 let next = state.next_waiter_id;
640 state.next_waiter_id += 1;
641 state.waiters.insert(
642 next,
643 PendingRead {
644 length,
645 result: None,
646 },
647 );
648 let Some(pty) = state.ptys.get_mut(&pty_ref.pty_id) else {
649 state.waiters.remove(&next);
650 return Err(PtyError::bad_file_descriptor("PTY not found"));
651 };
652 match pty_ref.end {
653 PtyEndKind::Master => pty.waiting_output_reads.push_back(next),
654 PtyEndKind::Slave => pty.waiting_input_reads.push_back(next),
655 }
656 self.notify_waiters_and_pollers();
657 waiter_id = Some(next);
658 next
659 };
660
661 let Some(deadline) = deadline else {
662 state = wait_or_recover(&self.inner.waiters, state);
663 if !state.waiters.contains_key(&id) {
664 waiter_id = None;
665 }
666 continue;
667 };
668
669 let now = Instant::now();
670 if now >= deadline {
671 if let Some(id) = waiter_id.take() {
672 state.waiters.remove(&id);
673 if let Some(pty) = state.ptys.get_mut(&pty_ref.pty_id) {
674 pty.waiting_input_reads.retain(|queued| *queued != id);
675 pty.waiting_output_reads.retain(|queued| *queued != id);
676 }
677 self.notify_waiters_and_pollers();
678 }
679 return Err(PtyError::would_block("PTY read timed out"));
680 }
681
682 let remaining = deadline.saturating_duration_since(now);
683 let (next_state, wait_result) =
684 wait_timeout_or_recover(&self.inner.waiters, state, remaining);
685 state = next_state;
686 if !state.waiters.contains_key(&id) {
687 waiter_id = None;
688 }
689 if wait_result.timed_out() {
690 if let Some(id) = waiter_id.take() {
691 state.waiters.remove(&id);
692 if let Some(pty) = state.ptys.get_mut(&pty_ref.pty_id) {
693 pty.waiting_input_reads.retain(|queued| *queued != id);
694 pty.waiting_output_reads.retain(|queued| *queued != id);
695 }
696 self.notify_waiters_and_pollers();
697 }
698 return Err(PtyError::would_block("PTY read timed out"));
699 }
700 }
701 }
702
703 pub fn close(&self, description_id: u64) {
704 let mut state = lock_or_recover(&self.inner.state);
705 let Some(pty_ref) = state.desc_to_pty.remove(&description_id) else {
706 return;
707 };
708
709 let (waiter_ids, remove_pty) = if let Some(pty) = state.ptys.get_mut(&pty_ref.pty_id) {
710 match pty_ref.end {
711 PtyEndKind::Master => {
712 pty.closed_master = true;
713 let mut waiters = pty.waiting_input_reads.drain(..).collect::<Vec<_>>();
714 waiters.extend(pty.waiting_output_reads.drain(..));
715 (waiters, pty.closed_master && pty.closed_slave)
716 }
717 PtyEndKind::Slave => {
718 pty.closed_slave = true;
719 let mut waiters = pty.waiting_output_reads.drain(..).collect::<Vec<_>>();
720 waiters.extend(pty.waiting_input_reads.drain(..));
721 (waiters, pty.closed_master && pty.closed_slave)
722 }
723 }
724 } else {
725 (Vec::new(), false)
726 };
727
728 for waiter_id in waiter_ids {
729 if let Some(waiter) = state.waiters.get_mut(&waiter_id) {
730 waiter.result = Some(None);
731 }
732 }
733
734 if remove_pty {
735 state.ptys.remove(&pty_ref.pty_id);
736 }
737 self.notify_waiters_and_pollers();
738 }
739
740 pub fn is_pty(&self, description_id: u64) -> bool {
741 lock_or_recover(&self.inner.state)
742 .desc_to_pty
743 .contains_key(&description_id)
744 }
745
746 pub fn is_slave(&self, description_id: u64) -> bool {
747 lock_or_recover(&self.inner.state)
748 .desc_to_pty
749 .get(&description_id)
750 .map(|pty_ref| pty_ref.end == PtyEndKind::Slave)
751 .unwrap_or(false)
752 }
753
754 pub fn set_discipline(
755 &self,
756 description_id: u64,
757 config: LineDisciplineConfig,
758 ) -> PtyResult<()> {
759 let mut state = lock_or_recover(&self.inner.state);
760 let pty_ref = state
761 .desc_to_pty
762 .get(&description_id)
763 .copied()
764 .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
765 let pty = state
766 .ptys
767 .get_mut(&pty_ref.pty_id)
768 .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?;
769 pty.termios_generation = pty
770 .termios_generation
771 .checked_add(1)
772 .ok_or_else(|| PtyError::io("PTY terminal-attribute generation counter exhausted"))?;
773 if let Some(canonical) = config.canonical {
774 pty.termios.icanon = canonical;
775 }
776 if let Some(icrnl) = config.icrnl {
777 pty.termios.icrnl = icrnl;
778 }
779 if let Some(echo) = config.echo {
780 pty.termios.echo = echo;
781 }
782 if let Some(isig) = config.isig {
783 pty.termios.isig = isig;
784 }
785 if let Some(opost) = config.opost {
786 pty.termios.opost = opost;
787 }
788 if let Some(onlcr) = config.onlcr {
789 pty.termios.onlcr = onlcr;
790 }
791 Ok(())
792 }
793
794 pub fn set_raw_mode(
801 &self,
802 description_id: u64,
803 lease_owner_pid: Option<u32>,
804 enabled: bool,
805 ) -> PtyResult<Option<u64>> {
806 let mut state = lock_or_recover(&self.inner.state);
807 let pty_ref = state
808 .desc_to_pty
809 .get(&description_id)
810 .copied()
811 .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
812 let pty = state
813 .ptys
814 .get_mut(&pty_ref.pty_id)
815 .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?;
816
817 if !enabled {
818 if let Some(owner_pid) = lease_owner_pid {
819 if release_raw_mode_lease(pty, owner_pid, None)? {
820 return Ok(None);
821 }
822 }
823 advance_termios_generation(pty)?;
824 apply_raw_mode(&mut pty.termios, false);
825 return Ok(None);
826 }
827
828 let Some(owner_pid) = lease_owner_pid else {
829 advance_termios_generation(pty)?;
830 apply_raw_mode(&mut pty.termios, true);
831 return Ok(None);
832 };
833
834 if let Some(index) = pty
838 .raw_mode_leases
839 .iter()
840 .position(|lease| lease.owner_pid == owner_pid)
841 {
842 if index + 1 == pty.raw_mode_leases.len() {
843 advance_termios_generation(pty)?;
844 apply_raw_mode(&mut pty.termios, true);
845 let lease = pty
846 .raw_mode_leases
847 .last_mut()
848 .expect("raw-mode lease index was just validated");
849 lease.applied_termios_generation = pty.termios_generation;
850 return Ok(Some(lease.generation));
851 }
852 let generation = pty.raw_mode_leases[index].generation;
853 release_raw_mode_lease(pty, owner_pid, Some(generation))?;
854 }
855
856 let generation = pty
857 .next_raw_mode_generation
858 .checked_add(1)
859 .ok_or_else(|| PtyError::io("PTY raw-mode generation counter exhausted"))?;
860 pty.next_raw_mode_generation = generation;
861 let restore_termios = pty.termios.clone();
862 advance_termios_generation(pty)?;
863 apply_raw_mode(&mut pty.termios, true);
864 pty.raw_mode_leases.push(RawModeLease {
865 owner_pid,
866 generation,
867 applied_termios_generation: pty.termios_generation,
868 restore_termios,
869 });
870 Ok(Some(generation))
871 }
872
873 pub fn release_raw_mode(
878 &self,
879 description_id: u64,
880 owner_pid: u32,
881 generation: u64,
882 ) -> PtyResult<bool> {
883 let mut state = lock_or_recover(&self.inner.state);
884 let pty_ref = state
885 .desc_to_pty
886 .get(&description_id)
887 .copied()
888 .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
889 let pty = state
890 .ptys
891 .get_mut(&pty_ref.pty_id)
892 .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?;
893 release_raw_mode_lease(pty, owner_pid, Some(generation))
894 }
895
896 pub fn get_termios(&self, description_id: u64) -> PtyResult<Termios> {
897 let state = lock_or_recover(&self.inner.state);
898 let pty_ref = state
899 .desc_to_pty
900 .get(&description_id)
901 .copied()
902 .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
903 state
904 .ptys
905 .get(&pty_ref.pty_id)
906 .cloned()
907 .map(|pty| pty.termios)
908 .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))
909 }
910
911 pub fn set_termios(&self, description_id: u64, termios: PartialTermios) -> PtyResult<()> {
912 let mut state = lock_or_recover(&self.inner.state);
913 let pty_ref = state
914 .desc_to_pty
915 .get(&description_id)
916 .copied()
917 .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
918 let pty = state
919 .ptys
920 .get_mut(&pty_ref.pty_id)
921 .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?;
922 advance_termios_generation(pty)?;
923 pty.termios.merge(termios);
924 Ok(())
925 }
926
927 pub fn set_foreground_pgid(&self, description_id: u64, pgid: u32) -> PtyResult<()> {
928 let mut state = lock_or_recover(&self.inner.state);
929 let pty_ref = state
930 .desc_to_pty
931 .get(&description_id)
932 .copied()
933 .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
934 let pty = state
935 .ptys
936 .get_mut(&pty_ref.pty_id)
937 .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?;
938 pty.foreground_pgid = pgid;
939 Ok(())
940 }
941
942 pub fn get_foreground_pgid(&self, description_id: u64) -> PtyResult<u32> {
943 let state = lock_or_recover(&self.inner.state);
944 let pty_ref = state
945 .desc_to_pty
946 .get(&description_id)
947 .copied()
948 .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
949 state
950 .ptys
951 .get(&pty_ref.pty_id)
952 .map(|pty| pty.foreground_pgid)
953 .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))
954 }
955
956 pub fn window_size(&self, description_id: u64) -> PtyResult<PtyWindowSize> {
957 let state = lock_or_recover(&self.inner.state);
958 let pty_ref = state
959 .desc_to_pty
960 .get(&description_id)
961 .copied()
962 .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
963 state
964 .ptys
965 .get(&pty_ref.pty_id)
966 .map(|pty| pty.window_size)
967 .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))
968 }
969
970 pub fn resize(&self, description_id: u64, cols: u16, rows: u16) -> PtyResult<Option<u32>> {
971 let mut state = lock_or_recover(&self.inner.state);
972 let pty_ref = state
973 .desc_to_pty
974 .get(&description_id)
975 .copied()
976 .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
977 let pty = state
978 .ptys
979 .get_mut(&pty_ref.pty_id)
980 .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?;
981 let next_size = PtyWindowSize { cols, rows };
982 if pty.window_size == next_size {
983 return Ok(None);
984 }
985 pty.window_size = next_size;
986 Ok((pty.foreground_pgid > 0).then_some(pty.foreground_pgid))
987 }
988
989 pub fn pty_count(&self) -> usize {
990 lock_or_recover(&self.inner.state).ptys.len()
991 }
992
993 pub fn buffered_input_bytes(&self) -> usize {
994 lock_or_recover(&self.inner.state)
995 .ptys
996 .values()
997 .map(|pty| buffer_size(&pty.input_buffer))
998 .sum()
999 }
1000
1001 pub fn buffered_output_bytes(&self) -> usize {
1002 lock_or_recover(&self.inner.state)
1003 .ptys
1004 .values()
1005 .map(|pty| buffer_size(&pty.output_buffer))
1006 .sum()
1007 }
1008
1009 pub fn pending_read_waiter_count(&self) -> usize {
1010 lock_or_recover(&self.inner.state).waiters.len()
1011 }
1012
1013 pub fn queued_read_waiter_count(&self) -> usize {
1014 lock_or_recover(&self.inner.state)
1015 .ptys
1016 .values()
1017 .map(|pty| pty.waiting_input_reads.len() + pty.waiting_output_reads.len())
1018 .sum()
1019 }
1020
1021 pub fn path_for(&self, description_id: u64) -> Option<String> {
1022 let state = lock_or_recover(&self.inner.state);
1023 let pty_ref = state.desc_to_pty.get(&description_id)?;
1024 state.ptys.get(&pty_ref.pty_id).map(|pty| pty.path.clone())
1025 }
1026
1027 fn notify_waiters_and_pollers(&self) {
1028 self.inner.waiters.notify_all();
1029 if let Some(notifier) = &self.notifier {
1030 notifier.notify();
1031 }
1032 }
1033}
1034
1035fn contains_dsr_cursor_query(data: &[u8]) -> bool {
1038 const QUERY: &[u8] = b"\x1b[6n";
1039 data.windows(QUERY.len()).any(|window| window == QUERY)
1040}
1041
1042fn process_output(termios: &Termios, data: &[u8]) -> Vec<u8> {
1043 if !termios.opost || !termios.onlcr || !data.contains(&b'\n') {
1044 return data.to_vec();
1045 }
1046
1047 let extra_crs = data
1048 .iter()
1049 .enumerate()
1050 .filter(|(index, byte)| **byte == b'\n' && (*index == 0 || data[*index - 1] != b'\r'))
1051 .count();
1052 if extra_crs == 0 {
1053 return data.to_vec();
1054 }
1055
1056 let mut result = Vec::with_capacity(data.len() + extra_crs);
1057 for (index, byte) in data.iter().enumerate() {
1058 if *byte == b'\n' && (index == 0 || data[index - 1] != b'\r') {
1059 result.push(b'\r');
1060 }
1061 result.push(*byte);
1062 }
1063 result
1064}
1065
1066fn process_input(
1067 pty: &mut PtyState,
1068 waiters: &mut BTreeMap<u64, PendingRead>,
1069 data: &[u8],
1070 signals: &mut Vec<(u32, i32)>,
1071) -> PtyResult<()> {
1072 if !pty.termios.icanon && !pty.termios.echo && !pty.termios.isig {
1073 let translated = translate_input(&pty.termios, data);
1074 deliver_input(pty, waiters, &translated)?;
1075 return Ok(());
1076 }
1077
1078 for mut byte in data.iter().copied() {
1079 if pty.termios.icrnl && byte == b'\r' {
1080 byte = b'\n';
1081 }
1082
1083 if pty.termios.isig {
1084 if let Some(signal) = signal_for_byte(&pty.termios, byte) {
1085 if pty.termios.icanon {
1086 pty.line_buffer.clear();
1087 }
1088 let has_foreground_process_group = pty.foreground_pgid > 0;
1089 if pty.termios.echo && !has_foreground_process_group {
1095 deliver_output(pty, waiters, &echo_control_byte(byte), true)?;
1096 if pty.termios.icanon {
1097 deliver_output(pty, waiters, b"\r\n", true)?;
1098 }
1099 }
1100 if has_foreground_process_group {
1101 signals.push((pty.foreground_pgid, signal));
1102 } else if pty.termios.icanon {
1103 deliver_input(pty, waiters, b"\n")?;
1104 }
1105 continue;
1106 }
1107 }
1108
1109 if pty.termios.icanon {
1110 if byte == pty.termios.cc.veof {
1111 if pty.line_buffer.is_empty() {
1112 deliver_input_eof(pty, waiters);
1113 } else {
1114 let line = pty.line_buffer.clone();
1115 deliver_input(pty, waiters, &line)?;
1116 pty.line_buffer.clear();
1117 }
1118 continue;
1119 }
1120
1121 if byte == pty.termios.cc.verase || byte == 0x08 {
1122 if let Some(&erased) = pty.line_buffer.last() {
1123 if pty.termios.echo {
1124 deliver_output(pty, waiters, &erase_sequence(erased), true)?;
1125 }
1126 pty.line_buffer.pop();
1127 }
1128 continue;
1129 }
1130
1131 if byte == pty.termios.cc.vkill {
1132 if !pty.line_buffer.is_empty() {
1133 if pty.termios.echo {
1134 let erase: Vec<u8> = pty
1135 .line_buffer
1136 .iter()
1137 .flat_map(|b| erase_sequence(*b))
1138 .collect();
1139 deliver_output(pty, waiters, &erase, true)?;
1140 }
1141 pty.line_buffer.clear();
1142 }
1143 continue;
1144 }
1145
1146 if byte == pty.termios.cc.vwerase {
1147 let mut erased: Vec<u8> = Vec::new();
1148 while matches!(pty.line_buffer.last(), Some(b' ') | Some(b'\t')) {
1149 if let Some(b) = pty.line_buffer.pop() {
1150 erased.push(b);
1151 }
1152 }
1153 while let Some(&b) = pty.line_buffer.last() {
1154 if b == b' ' || b == b'\t' {
1155 break;
1156 }
1157 pty.line_buffer.pop();
1158 erased.push(b);
1159 }
1160 if pty.termios.echo && !erased.is_empty() {
1161 let sequence: Vec<u8> =
1162 erased.iter().flat_map(|b| erase_sequence(*b)).collect();
1163 deliver_output(pty, waiters, &sequence, true)?;
1164 }
1165 continue;
1166 }
1167
1168 if byte == b'\n' {
1169 let mut line = pty.line_buffer.clone();
1170 line.push(b'\n');
1171 if pty.termios.echo {
1172 deliver_output(pty, waiters, b"\r\n", true)?;
1173 }
1174 deliver_input(pty, waiters, &line)?;
1175 pty.line_buffer.clear();
1176 continue;
1177 }
1178
1179 if pty.line_buffer.len() >= MAX_CANON {
1180 continue;
1181 }
1182 if pty.termios.echo {
1183 deliver_output(pty, waiters, &echo_control_byte(byte), true)?;
1186 }
1187 pty.line_buffer.push(byte);
1188 } else {
1189 if pty.termios.echo {
1190 deliver_output(pty, waiters, &[byte], true)?;
1191 }
1192 deliver_input(pty, waiters, &[byte])?;
1193 }
1194 }
1195
1196 Ok(())
1197}
1198
1199fn translate_input(termios: &Termios, data: &[u8]) -> Vec<u8> {
1200 if !termios.icrnl || !data.contains(&b'\r') {
1201 return data.to_vec();
1202 }
1203
1204 data.iter()
1205 .map(|byte| if *byte == b'\r' { b'\n' } else { *byte })
1206 .collect()
1207}
1208
1209fn deliver_input(
1210 pty: &mut PtyState,
1211 waiters: &mut BTreeMap<u64, PendingRead>,
1212 data: &[u8],
1213) -> PtyResult<()> {
1214 if let Some(waiter_id) = pty.waiting_input_reads.pop_front() {
1215 if let Some(waiter) = waiters.get_mut(&waiter_id) {
1216 if data.len() <= waiter.length {
1217 waiter.result = Some(Some(data.to_vec()));
1218 } else {
1219 let tail_len = data.len() - waiter.length;
1224 if tail_len > available_capacity(&pty.input_buffer) {
1225 pty.waiting_input_reads.push_front(waiter_id);
1226 return Err(PtyError::would_block("PTY input buffer full"));
1227 }
1228 let (head, tail) = data.split_at(waiter.length);
1229 waiter.result = Some(Some(head.to_vec()));
1230 pty.input_buffer.push_front(tail.to_vec());
1231 }
1232 return Ok(());
1233 }
1234 }
1235
1236 if buffer_size(&pty.input_buffer).saturating_add(data.len()) > MAX_PTY_BUFFER_BYTES {
1237 return Err(PtyError::would_block("PTY input buffer full"));
1238 }
1239
1240 pty.input_buffer.push_back(data.to_vec());
1241 Ok(())
1242}
1243
1244fn deliver_input_eof(pty: &mut PtyState, waiters: &mut BTreeMap<u64, PendingRead>) {
1245 if let Some(waiter_id) = pty.waiting_input_reads.pop_front() {
1246 if let Some(waiter) = waiters.get_mut(&waiter_id) {
1247 waiter.result = Some(None);
1248 return;
1249 }
1250 }
1251
1252 pty.input_eof_pending = true;
1253}
1254
1255fn deliver_output(
1256 pty: &mut PtyState,
1257 waiters: &mut BTreeMap<u64, PendingRead>,
1258 data: &[u8],
1259 echo: bool,
1260) -> PtyResult<()> {
1261 if let Some(waiter_id) = pty.waiting_output_reads.pop_front() {
1262 if let Some(waiter) = waiters.get_mut(&waiter_id) {
1263 if data.len() <= waiter.length {
1264 waiter.result = Some(Some(data.to_vec()));
1265 } else {
1266 let tail_len = data.len() - waiter.length;
1268 if tail_len > available_capacity(&pty.output_buffer) {
1269 pty.waiting_output_reads.push_front(waiter_id);
1270 let message = if echo {
1271 "PTY output buffer full (echo backpressure)"
1272 } else {
1273 "PTY output buffer full"
1274 };
1275 return Err(PtyError::would_block(message));
1276 }
1277 let (head, tail) = data.split_at(waiter.length);
1278 waiter.result = Some(Some(head.to_vec()));
1279 pty.output_buffer.push_front(tail.to_vec());
1280 }
1281 return Ok(());
1282 }
1283 }
1284
1285 if buffer_size(&pty.output_buffer).saturating_add(data.len()) > MAX_PTY_BUFFER_BYTES {
1286 let message = if echo {
1287 "PTY output buffer full (echo backpressure)"
1288 } else {
1289 "PTY output buffer full"
1290 };
1291 return Err(PtyError::would_block(message));
1292 }
1293
1294 pty.output_buffer.push_back(data.to_vec());
1295 Ok(())
1296}
1297
1298fn advance_termios_generation(pty: &mut PtyState) -> PtyResult<()> {
1299 pty.termios_generation = pty
1300 .termios_generation
1301 .checked_add(1)
1302 .ok_or_else(|| PtyError::io("PTY terminal-attribute generation counter exhausted"))?;
1303 Ok(())
1304}
1305
1306fn apply_raw_mode(termios: &mut Termios, enabled: bool) {
1307 termios.icrnl = !enabled;
1308 termios.icanon = !enabled;
1309 termios.echo = !enabled;
1310 termios.isig = !enabled;
1311 termios.opost = !enabled;
1312 termios.onlcr = !enabled;
1313}
1314
1315fn release_raw_mode_lease(
1316 pty: &mut PtyState,
1317 owner_pid: u32,
1318 expected_generation: Option<u64>,
1319) -> PtyResult<bool> {
1320 let Some(index) = pty.raw_mode_leases.iter().position(|lease| {
1321 lease.owner_pid == owner_pid
1322 && expected_generation.is_none_or(|generation| lease.generation == generation)
1323 }) else {
1324 return Ok(false);
1325 };
1326
1327 let was_top = index + 1 == pty.raw_mode_leases.len();
1328 let lease = pty.raw_mode_leases.remove(index);
1329 if !was_top {
1330 pty.raw_mode_leases[index].restore_termios = lease.restore_termios;
1334 return Ok(true);
1335 }
1336
1337 if pty.termios_generation != lease.applied_termios_generation {
1340 return Ok(true);
1341 }
1342
1343 advance_termios_generation(pty)?;
1344 pty.termios = lease.restore_termios;
1345 let restored_generation = pty.termios_generation;
1346 if let Some(previous) = pty.raw_mode_leases.last_mut() {
1347 previous.applied_termios_generation = restored_generation;
1350 }
1351 Ok(true)
1352}
1353
1354fn signal_for_byte(termios: &Termios, byte: u8) -> Option<i32> {
1355 if byte == termios.cc.vintr {
1356 return Some(SIGINT);
1357 }
1358 if byte == termios.cc.vquit {
1359 return Some(SIGQUIT);
1360 }
1361 if byte == termios.cc.vsusp {
1362 return Some(SIGTSTP);
1363 }
1364 None
1365}
1366
1367fn echo_control_byte(byte: u8) -> Vec<u8> {
1368 if byte < 0x20 {
1369 vec![b'^', byte + 0x40]
1370 } else if byte == 0x7f {
1371 b"^?".to_vec()
1372 } else {
1373 vec![byte]
1374 }
1375}
1376
1377fn erase_sequence(byte: u8) -> Vec<u8> {
1383 let columns = echo_control_byte(byte).len();
1384 (0..columns).flat_map(|_| [0x08, 0x20, 0x08]).collect()
1385}
1386
1387fn buffer_size(buffer: &VecDeque<Vec<u8>>) -> usize {
1388 buffer.iter().map(Vec::len).sum()
1389}
1390
1391fn available_capacity(buffer: &VecDeque<Vec<u8>>) -> usize {
1392 MAX_PTY_BUFFER_BYTES.saturating_sub(buffer_size(buffer))
1393}
1394
1395fn drain_buffer(buffer: &mut VecDeque<Vec<u8>>, length: usize) -> Vec<u8> {
1396 let mut chunks = Vec::new();
1397 let mut remaining = length;
1398
1399 while remaining > 0 {
1400 let Some(chunk) = buffer.pop_front() else {
1401 break;
1402 };
1403 if chunk.len() <= remaining {
1404 remaining -= chunk.len();
1405 chunks.push(chunk);
1406 } else {
1407 let (head, tail) = chunk.split_at(remaining);
1408 chunks.push(head.to_vec());
1409 buffer.push_front(tail.to_vec());
1410 remaining = 0;
1411 }
1412 }
1413
1414 if chunks.len() == 1 {
1415 return chunks.pop().expect("single chunk should exist");
1416 }
1417
1418 let total = chunks.iter().map(Vec::len).sum();
1419 let mut result = Vec::with_capacity(total);
1420 for chunk in chunks {
1421 result.extend_from_slice(&chunk);
1422 }
1423 result
1424}
1425
1426fn lock_or_recover<'a, T>(mutex: &'a Mutex<T>) -> MutexGuard<'a, T> {
1427 match mutex.lock() {
1428 Ok(guard) => guard,
1429 Err(poisoned) => poisoned.into_inner(),
1430 }
1431}
1432
1433fn wait_or_recover<'a, T>(condvar: &Condvar, guard: MutexGuard<'a, T>) -> MutexGuard<'a, T> {
1434 match condvar.wait(guard) {
1435 Ok(guard) => guard,
1436 Err(poisoned) => poisoned.into_inner(),
1437 }
1438}
1439
1440fn wait_timeout_or_recover<'a, T>(
1441 condvar: &Condvar,
1442 guard: MutexGuard<'a, T>,
1443 timeout: Duration,
1444) -> (MutexGuard<'a, T>, std::sync::WaitTimeoutResult) {
1445 match condvar.wait_timeout(guard, timeout) {
1446 Ok(result) => result,
1447 Err(poisoned) => poisoned.into_inner(),
1448 }
1449}