Skip to main content

agent_os_kernel/
pty.rs

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, Instant};
11
12pub const MAX_PTY_BUFFER_BYTES: usize = 65_536;
13pub const MAX_CANON: usize = 4_096;
14pub const SIGINT: i32 = 2;
15pub const SIGQUIT: i32 = 3;
16pub const SIGTSTP: i32 = 20;
17const DEFAULT_PTY_COLUMNS: u16 = 80;
18const DEFAULT_PTY_ROWS: u16 = 24;
19
20pub type PtyResult<T> = Result<T, PtyError>;
21pub type SignalHandler = Arc<dyn Fn(u32, i32) + Send + Sync>;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct PtyError {
25    code: &'static str,
26    message: String,
27}
28
29impl PtyError {
30    pub fn code(&self) -> &'static str {
31        self.code
32    }
33
34    fn bad_file_descriptor(message: impl Into<String>) -> Self {
35        Self {
36            code: "EBADF",
37            message: message.into(),
38        }
39    }
40
41    fn io(message: impl Into<String>) -> Self {
42        Self {
43            code: "EIO",
44            message: message.into(),
45        }
46    }
47
48    fn would_block(message: impl Into<String>) -> Self {
49        Self {
50            code: "EAGAIN",
51            message: message.into(),
52        }
53    }
54}
55
56impl fmt::Display for PtyError {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        write!(f, "{}: {}", self.code, self.message)
59    }
60}
61
62impl Error for PtyError {}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
65pub struct LineDisciplineConfig {
66    pub canonical: Option<bool>,
67    pub echo: Option<bool>,
68    pub isig: Option<bool>,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct Termios {
73    pub icrnl: bool,
74    pub opost: bool,
75    pub onlcr: bool,
76    pub icanon: bool,
77    pub echo: bool,
78    pub isig: bool,
79    pub cc: TermiosControlChars,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
83pub struct PartialTermios {
84    pub icrnl: Option<bool>,
85    pub opost: Option<bool>,
86    pub onlcr: Option<bool>,
87    pub icanon: Option<bool>,
88    pub echo: Option<bool>,
89    pub isig: Option<bool>,
90    pub cc: Option<PartialTermiosControlChars>,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct TermiosControlChars {
95    pub vintr: u8,
96    pub vquit: u8,
97    pub vsusp: u8,
98    pub veof: u8,
99    pub verase: u8,
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
103pub struct PartialTermiosControlChars {
104    pub vintr: Option<u8>,
105    pub vquit: Option<u8>,
106    pub vsusp: Option<u8>,
107    pub veof: Option<u8>,
108    pub verase: Option<u8>,
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112pub struct PtyWindowSize {
113    pub cols: u16,
114    pub rows: u16,
115}
116
117impl Default for PtyWindowSize {
118    fn default() -> Self {
119        Self {
120            cols: DEFAULT_PTY_COLUMNS,
121            rows: DEFAULT_PTY_ROWS,
122        }
123    }
124}
125
126impl Default for Termios {
127    fn default() -> Self {
128        Self {
129            icrnl: true,
130            opost: true,
131            onlcr: true,
132            icanon: true,
133            echo: true,
134            isig: true,
135            cc: TermiosControlChars {
136                vintr: 0x03,
137                vquit: 0x1c,
138                vsusp: 0x1a,
139                veof: 0x04,
140                verase: 0x7f,
141            },
142        }
143    }
144}
145
146impl Termios {
147    fn merge(&mut self, update: PartialTermios) {
148        if let Some(icrnl) = update.icrnl {
149            self.icrnl = icrnl;
150        }
151        if let Some(opost) = update.opost {
152            self.opost = opost;
153        }
154        if let Some(onlcr) = update.onlcr {
155            self.onlcr = onlcr;
156        }
157        if let Some(icanon) = update.icanon {
158            self.icanon = icanon;
159        }
160        if let Some(echo) = update.echo {
161            self.echo = echo;
162        }
163        if let Some(isig) = update.isig {
164            self.isig = isig;
165        }
166        if let Some(cc) = update.cc {
167            self.cc.merge(cc);
168        }
169    }
170}
171
172impl TermiosControlChars {
173    fn merge(&mut self, update: PartialTermiosControlChars) {
174        if let Some(vintr) = update.vintr {
175            self.vintr = vintr;
176        }
177        if let Some(vquit) = update.vquit {
178            self.vquit = vquit;
179        }
180        if let Some(vsusp) = update.vsusp {
181            self.vsusp = vsusp;
182        }
183        if let Some(veof) = update.veof {
184            self.veof = veof;
185        }
186        if let Some(verase) = update.verase {
187            self.verase = verase;
188        }
189    }
190}
191
192#[derive(Debug, Clone)]
193pub struct PtyEnd {
194    pub description: SharedFileDescription,
195    pub filetype: u8,
196}
197
198#[derive(Debug, Clone)]
199pub struct PtyPair {
200    pub master: PtyEnd,
201    pub slave: PtyEnd,
202    pub path: String,
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206struct PtyRef {
207    pty_id: u64,
208    end: PtyEndKind,
209}
210
211#[derive(Debug, Clone, Copy, PartialEq, Eq)]
212enum PtyEndKind {
213    Master,
214    Slave,
215}
216
217#[derive(Debug, Default)]
218struct PendingRead {
219    result: Option<Option<Vec<u8>>>,
220}
221
222#[derive(Debug, Clone, Default)]
223struct PtyState {
224    path: String,
225    input_buffer: VecDeque<Vec<u8>>,
226    output_buffer: VecDeque<Vec<u8>>,
227    closed_master: bool,
228    closed_slave: bool,
229    waiting_input_reads: VecDeque<u64>,
230    waiting_output_reads: VecDeque<u64>,
231    termios: Termios,
232    line_buffer: Vec<u8>,
233    foreground_pgid: u32,
234    window_size: PtyWindowSize,
235}
236
237#[derive(Debug)]
238struct PtyManagerState {
239    ptys: BTreeMap<u64, PtyState>,
240    desc_to_pty: BTreeMap<u64, PtyRef>,
241    waiters: BTreeMap<u64, PendingRead>,
242    next_pty_id: u64,
243    next_desc_id: u64,
244    next_waiter_id: u64,
245}
246
247impl Default for PtyManagerState {
248    fn default() -> Self {
249        Self {
250            ptys: BTreeMap::new(),
251            desc_to_pty: BTreeMap::new(),
252            waiters: BTreeMap::new(),
253            next_pty_id: 0,
254            next_desc_id: 200_000,
255            next_waiter_id: 1,
256        }
257    }
258}
259
260#[derive(Debug)]
261struct PtyManagerInner {
262    state: Mutex<PtyManagerState>,
263    waiters: Condvar,
264}
265
266#[derive(Clone)]
267pub struct PtyManager {
268    inner: Arc<PtyManagerInner>,
269    on_signal: Option<SignalHandler>,
270    notifier: Option<PollNotifier>,
271}
272
273impl Default for PtyManager {
274    fn default() -> Self {
275        Self {
276            inner: Arc::new(PtyManagerInner {
277                state: Mutex::new(PtyManagerState::default()),
278                waiters: Condvar::new(),
279            }),
280            on_signal: None,
281            notifier: None,
282        }
283    }
284}
285
286impl PtyManager {
287    pub fn new() -> Self {
288        Self::default()
289    }
290
291    pub fn with_signal_handler(on_signal: SignalHandler) -> Self {
292        let mut manager = Self::new();
293        manager.on_signal = Some(on_signal);
294        manager
295    }
296
297    pub(crate) fn with_signal_handler_and_notifier(
298        on_signal: SignalHandler,
299        notifier: PollNotifier,
300    ) -> Self {
301        let mut manager = Self::with_notifier(notifier);
302        manager.on_signal = Some(on_signal);
303        manager
304    }
305
306    pub(crate) fn with_notifier(notifier: PollNotifier) -> Self {
307        Self {
308            notifier: Some(notifier),
309            ..Self::default()
310        }
311    }
312
313    pub fn create_pty(&self) -> PtyPair {
314        let mut state = lock_or_recover(&self.inner.state);
315        let pty_id = state.next_pty_id;
316        state.next_pty_id += 1;
317
318        let master_id = state.next_desc_id;
319        state.next_desc_id += 1;
320        let slave_id = state.next_desc_id;
321        state.next_desc_id += 1;
322
323        let path = format!("/dev/pts/{pty_id}");
324        state.ptys.insert(
325            pty_id,
326            PtyState {
327                path: path.clone(),
328                termios: Termios::default(),
329                window_size: PtyWindowSize::default(),
330                ..PtyState::default()
331            },
332        );
333        state.desc_to_pty.insert(
334            master_id,
335            PtyRef {
336                pty_id,
337                end: PtyEndKind::Master,
338            },
339        );
340        state.desc_to_pty.insert(
341            slave_id,
342            PtyRef {
343                pty_id,
344                end: PtyEndKind::Slave,
345            },
346        );
347        drop(state);
348
349        PtyPair {
350            master: PtyEnd {
351                description: Arc::new(FileDescription::with_ref_count(
352                    master_id,
353                    format!("pty:{pty_id}:master"),
354                    O_RDWR,
355                    0,
356                )),
357                filetype: FILETYPE_CHARACTER_DEVICE,
358            },
359            slave: PtyEnd {
360                description: Arc::new(FileDescription::with_ref_count(
361                    slave_id,
362                    path.clone(),
363                    O_RDWR,
364                    0,
365                )),
366                filetype: FILETYPE_CHARACTER_DEVICE,
367            },
368            path,
369        }
370    }
371
372    pub fn create_pty_fds(&self, fd_table: &mut ProcessFdTable) -> FdResult<(u32, u32, String)> {
373        let pty = self.create_pty();
374        let master_fd = fd_table.open_with(
375            Arc::clone(&pty.master.description),
376            FILETYPE_CHARACTER_DEVICE,
377            None,
378        )?;
379        match fd_table.open_with(
380            Arc::clone(&pty.slave.description),
381            FILETYPE_CHARACTER_DEVICE,
382            None,
383        ) {
384            Ok(slave_fd) => Ok((master_fd, slave_fd, pty.path)),
385            Err(error) => {
386                fd_table.close(master_fd);
387                self.close(pty.master.description.id());
388                self.close(pty.slave.description.id());
389                Err(error)
390            }
391        }
392    }
393
394    pub fn poll(&self, description_id: u64, requested: PollEvents) -> PtyResult<PollEvents> {
395        let state = lock_or_recover(&self.inner.state);
396        let pty_ref = state
397            .desc_to_pty
398            .get(&description_id)
399            .copied()
400            .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
401        let pty = state
402            .ptys
403            .get(&pty_ref.pty_id)
404            .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?;
405
406        let mut events = PollEvents::empty();
407        match pty_ref.end {
408            PtyEndKind::Master => {
409                if requested.intersects(POLLIN) && !pty.output_buffer.is_empty() {
410                    events |= POLLIN;
411                }
412                if pty.closed_slave {
413                    events |= POLLHUP;
414                } else if requested.intersects(POLLOUT)
415                    && (available_capacity(&pty.input_buffer) > 0
416                        || !pty.waiting_input_reads.is_empty())
417                {
418                    events |= POLLOUT;
419                }
420            }
421            PtyEndKind::Slave => {
422                if requested.intersects(POLLIN) && !pty.input_buffer.is_empty() {
423                    events |= POLLIN;
424                }
425                if pty.closed_master {
426                    events |= POLLHUP;
427                } else if requested.intersects(POLLOUT)
428                    && (available_capacity(&pty.output_buffer) > 0
429                        || !pty.waiting_output_reads.is_empty())
430                {
431                    events |= POLLOUT;
432                }
433            }
434        }
435
436        Ok(events)
437    }
438
439    pub fn write(&self, description_id: u64, data: impl AsRef<[u8]>) -> PtyResult<usize> {
440        let payload = data.as_ref();
441        let mut signals = Vec::new();
442
443        {
444            let mut state = lock_or_recover(&self.inner.state);
445            let pty_ref = state
446                .desc_to_pty
447                .get(&description_id)
448                .copied()
449                .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
450            let PtyManagerState { ptys, waiters, .. } = &mut *state;
451            let pty = ptys
452                .get_mut(&pty_ref.pty_id)
453                .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?;
454
455            match pty_ref.end {
456                PtyEndKind::Master => {
457                    if pty.closed_master {
458                        return Err(PtyError::io("master closed"));
459                    }
460                    if pty.closed_slave {
461                        return Err(PtyError::io("slave closed"));
462                    }
463                    process_input(pty, waiters, payload, &mut signals)?;
464                }
465                PtyEndKind::Slave => {
466                    if pty.closed_slave {
467                        return Err(PtyError::io("slave closed"));
468                    }
469                    if pty.closed_master {
470                        return Err(PtyError::io("master closed"));
471                    }
472
473                    let processed = process_output(&pty.termios, payload);
474                    deliver_output(pty, waiters, &processed, false)?;
475                }
476            }
477        }
478
479        self.notify_waiters_and_pollers();
480        if let Some(on_signal) = &self.on_signal {
481            for (pgid, signal) in signals {
482                if pgid > 0 {
483                    on_signal(pgid, signal);
484                }
485            }
486        }
487
488        Ok(payload.len())
489    }
490
491    pub fn read(&self, description_id: u64, length: usize) -> PtyResult<Option<Vec<u8>>> {
492        self.read_with_timeout(description_id, length, None)
493    }
494
495    pub fn read_with_timeout(
496        &self,
497        description_id: u64,
498        length: usize,
499        timeout: Option<Duration>,
500    ) -> PtyResult<Option<Vec<u8>>> {
501        let mut state = lock_or_recover(&self.inner.state);
502        let pty_ref = state
503            .desc_to_pty
504            .get(&description_id)
505            .copied()
506            .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
507        let mut waiter_id = None;
508        let deadline = timeout.map(|duration| Instant::now() + duration);
509
510        loop {
511            if let Some(id) = waiter_id {
512                if let Some(waiter) = state.waiters.get_mut(&id) {
513                    if let Some(result) = waiter.result.take() {
514                        state.waiters.remove(&id);
515                        return Ok(result);
516                    }
517                }
518            }
519
520            {
521                let pty = state
522                    .ptys
523                    .get_mut(&pty_ref.pty_id)
524                    .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?;
525
526                match pty_ref.end {
527                    PtyEndKind::Master => {
528                        if pty.closed_master {
529                            if let Some(id) = waiter_id {
530                                state.waiters.remove(&id);
531                            }
532                            return Err(PtyError::io("master closed"));
533                        }
534
535                        if !pty.output_buffer.is_empty() {
536                            let result = drain_buffer(&mut pty.output_buffer, length);
537                            self.notify_waiters_and_pollers();
538                            return Ok(Some(result));
539                        }
540
541                        if pty.closed_slave {
542                            if let Some(id) = waiter_id {
543                                state.waiters.remove(&id);
544                            }
545                            return Ok(None);
546                        }
547                    }
548                    PtyEndKind::Slave => {
549                        if pty.closed_slave {
550                            if let Some(id) = waiter_id {
551                                state.waiters.remove(&id);
552                            }
553                            return Err(PtyError::io("slave closed"));
554                        }
555
556                        if !pty.input_buffer.is_empty() {
557                            let result = drain_buffer(&mut pty.input_buffer, length);
558                            self.notify_waiters_and_pollers();
559                            return Ok(Some(result));
560                        }
561
562                        if pty.closed_master {
563                            if let Some(id) = waiter_id {
564                                state.waiters.remove(&id);
565                            }
566                            return Ok(None);
567                        }
568                    }
569                }
570            }
571
572            let id = if let Some(id) = waiter_id {
573                id
574            } else {
575                let next = state.next_waiter_id;
576                state.next_waiter_id += 1;
577                state.waiters.insert(next, PendingRead::default());
578                let Some(pty) = state.ptys.get_mut(&pty_ref.pty_id) else {
579                    state.waiters.remove(&next);
580                    return Err(PtyError::bad_file_descriptor("PTY not found"));
581                };
582                match pty_ref.end {
583                    PtyEndKind::Master => pty.waiting_output_reads.push_back(next),
584                    PtyEndKind::Slave => pty.waiting_input_reads.push_back(next),
585                }
586                self.notify_waiters_and_pollers();
587                waiter_id = Some(next);
588                next
589            };
590
591            let Some(deadline) = deadline else {
592                state = wait_or_recover(&self.inner.waiters, state);
593                if !state.waiters.contains_key(&id) {
594                    waiter_id = None;
595                }
596                continue;
597            };
598
599            let now = Instant::now();
600            if now >= deadline {
601                if let Some(id) = waiter_id.take() {
602                    state.waiters.remove(&id);
603                    if let Some(pty) = state.ptys.get_mut(&pty_ref.pty_id) {
604                        pty.waiting_input_reads.retain(|queued| *queued != id);
605                        pty.waiting_output_reads.retain(|queued| *queued != id);
606                    }
607                    self.notify_waiters_and_pollers();
608                }
609                return Err(PtyError::would_block("PTY read timed out"));
610            }
611
612            let remaining = deadline.saturating_duration_since(now);
613            let (next_state, wait_result) =
614                wait_timeout_or_recover(&self.inner.waiters, state, remaining);
615            state = next_state;
616            if !state.waiters.contains_key(&id) {
617                waiter_id = None;
618            }
619            if wait_result.timed_out() {
620                if let Some(id) = waiter_id.take() {
621                    state.waiters.remove(&id);
622                    if let Some(pty) = state.ptys.get_mut(&pty_ref.pty_id) {
623                        pty.waiting_input_reads.retain(|queued| *queued != id);
624                        pty.waiting_output_reads.retain(|queued| *queued != id);
625                    }
626                    self.notify_waiters_and_pollers();
627                }
628                return Err(PtyError::would_block("PTY read timed out"));
629            }
630        }
631    }
632
633    pub fn close(&self, description_id: u64) {
634        let mut state = lock_or_recover(&self.inner.state);
635        let Some(pty_ref) = state.desc_to_pty.remove(&description_id) else {
636            return;
637        };
638
639        let (waiter_ids, remove_pty) = if let Some(pty) = state.ptys.get_mut(&pty_ref.pty_id) {
640            match pty_ref.end {
641                PtyEndKind::Master => {
642                    pty.closed_master = true;
643                    let mut waiters = pty.waiting_input_reads.drain(..).collect::<Vec<_>>();
644                    waiters.extend(pty.waiting_output_reads.drain(..));
645                    (waiters, pty.closed_master && pty.closed_slave)
646                }
647                PtyEndKind::Slave => {
648                    pty.closed_slave = true;
649                    let mut waiters = pty.waiting_output_reads.drain(..).collect::<Vec<_>>();
650                    waiters.extend(pty.waiting_input_reads.drain(..));
651                    (waiters, pty.closed_master && pty.closed_slave)
652                }
653            }
654        } else {
655            (Vec::new(), false)
656        };
657
658        for waiter_id in waiter_ids {
659            if let Some(waiter) = state.waiters.get_mut(&waiter_id) {
660                waiter.result = Some(None);
661            }
662        }
663
664        if remove_pty {
665            state.ptys.remove(&pty_ref.pty_id);
666        }
667        self.notify_waiters_and_pollers();
668    }
669
670    pub fn is_pty(&self, description_id: u64) -> bool {
671        lock_or_recover(&self.inner.state)
672            .desc_to_pty
673            .contains_key(&description_id)
674    }
675
676    pub fn is_slave(&self, description_id: u64) -> bool {
677        lock_or_recover(&self.inner.state)
678            .desc_to_pty
679            .get(&description_id)
680            .map(|pty_ref| pty_ref.end == PtyEndKind::Slave)
681            .unwrap_or(false)
682    }
683
684    pub fn set_discipline(
685        &self,
686        description_id: u64,
687        config: LineDisciplineConfig,
688    ) -> PtyResult<()> {
689        let mut state = lock_or_recover(&self.inner.state);
690        let pty_ref = state
691            .desc_to_pty
692            .get(&description_id)
693            .copied()
694            .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
695        let pty = state
696            .ptys
697            .get_mut(&pty_ref.pty_id)
698            .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?;
699        if let Some(canonical) = config.canonical {
700            pty.termios.icanon = canonical;
701        }
702        if let Some(echo) = config.echo {
703            pty.termios.echo = echo;
704        }
705        if let Some(isig) = config.isig {
706            pty.termios.isig = isig;
707        }
708        Ok(())
709    }
710
711    pub fn get_termios(&self, description_id: u64) -> PtyResult<Termios> {
712        let state = lock_or_recover(&self.inner.state);
713        let pty_ref = state
714            .desc_to_pty
715            .get(&description_id)
716            .copied()
717            .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
718        state
719            .ptys
720            .get(&pty_ref.pty_id)
721            .cloned()
722            .map(|pty| pty.termios)
723            .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))
724    }
725
726    pub fn set_termios(&self, description_id: u64, termios: PartialTermios) -> PtyResult<()> {
727        let mut state = lock_or_recover(&self.inner.state);
728        let pty_ref = state
729            .desc_to_pty
730            .get(&description_id)
731            .copied()
732            .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
733        let pty = state
734            .ptys
735            .get_mut(&pty_ref.pty_id)
736            .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?;
737        pty.termios.merge(termios);
738        Ok(())
739    }
740
741    pub fn set_foreground_pgid(&self, description_id: u64, pgid: u32) -> PtyResult<()> {
742        let mut state = lock_or_recover(&self.inner.state);
743        let pty_ref = state
744            .desc_to_pty
745            .get(&description_id)
746            .copied()
747            .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
748        let pty = state
749            .ptys
750            .get_mut(&pty_ref.pty_id)
751            .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?;
752        pty.foreground_pgid = pgid;
753        Ok(())
754    }
755
756    pub fn get_foreground_pgid(&self, description_id: u64) -> PtyResult<u32> {
757        let state = lock_or_recover(&self.inner.state);
758        let pty_ref = state
759            .desc_to_pty
760            .get(&description_id)
761            .copied()
762            .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
763        state
764            .ptys
765            .get(&pty_ref.pty_id)
766            .map(|pty| pty.foreground_pgid)
767            .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))
768    }
769
770    pub fn resize(&self, description_id: u64, cols: u16, rows: u16) -> PtyResult<Option<u32>> {
771        let mut state = lock_or_recover(&self.inner.state);
772        let pty_ref = state
773            .desc_to_pty
774            .get(&description_id)
775            .copied()
776            .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
777        let pty = state
778            .ptys
779            .get_mut(&pty_ref.pty_id)
780            .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?;
781        let next_size = PtyWindowSize { cols, rows };
782        if pty.window_size == next_size {
783            return Ok(None);
784        }
785        pty.window_size = next_size;
786        Ok((pty.foreground_pgid > 0).then_some(pty.foreground_pgid))
787    }
788
789    pub fn pty_count(&self) -> usize {
790        lock_or_recover(&self.inner.state).ptys.len()
791    }
792
793    pub fn buffered_input_bytes(&self) -> usize {
794        lock_or_recover(&self.inner.state)
795            .ptys
796            .values()
797            .map(|pty| buffer_size(&pty.input_buffer))
798            .sum()
799    }
800
801    pub fn buffered_output_bytes(&self) -> usize {
802        lock_or_recover(&self.inner.state)
803            .ptys
804            .values()
805            .map(|pty| buffer_size(&pty.output_buffer))
806            .sum()
807    }
808
809    pub fn path_for(&self, description_id: u64) -> Option<String> {
810        let state = lock_or_recover(&self.inner.state);
811        let pty_ref = state.desc_to_pty.get(&description_id)?;
812        state.ptys.get(&pty_ref.pty_id).map(|pty| pty.path.clone())
813    }
814
815    fn notify_waiters_and_pollers(&self) {
816        self.inner.waiters.notify_all();
817        if let Some(notifier) = &self.notifier {
818            notifier.notify();
819        }
820    }
821}
822
823fn process_output(termios: &Termios, data: &[u8]) -> Vec<u8> {
824    if !termios.opost || !termios.onlcr || !data.contains(&b'\n') {
825        return data.to_vec();
826    }
827
828    let extra_crs = data
829        .iter()
830        .enumerate()
831        .filter(|(index, byte)| **byte == b'\n' && (*index == 0 || data[*index - 1] != b'\r'))
832        .count();
833    if extra_crs == 0 {
834        return data.to_vec();
835    }
836
837    let mut result = Vec::with_capacity(data.len() + extra_crs);
838    for (index, byte) in data.iter().enumerate() {
839        if *byte == b'\n' && (index == 0 || data[index - 1] != b'\r') {
840            result.push(b'\r');
841        }
842        result.push(*byte);
843    }
844    result
845}
846
847fn process_input(
848    pty: &mut PtyState,
849    waiters: &mut BTreeMap<u64, PendingRead>,
850    data: &[u8],
851    signals: &mut Vec<(u32, i32)>,
852) -> PtyResult<()> {
853    if !pty.termios.icanon && !pty.termios.echo && !pty.termios.isig {
854        let translated = translate_input(&pty.termios, data);
855        deliver_input(pty, waiters, &translated)?;
856        return Ok(());
857    }
858
859    for mut byte in data.iter().copied() {
860        if pty.termios.icrnl && byte == b'\r' {
861            byte = b'\n';
862        }
863
864        if pty.termios.isig {
865            if let Some(signal) = signal_for_byte(&pty.termios, byte) {
866                if pty.termios.icanon {
867                    pty.line_buffer.clear();
868                }
869                if pty.foreground_pgid > 0 {
870                    signals.push((pty.foreground_pgid, signal));
871                }
872                continue;
873            }
874        }
875
876        if pty.termios.icanon {
877            if byte == pty.termios.cc.veof {
878                if pty.line_buffer.is_empty() {
879                    deliver_input(pty, waiters, &[])?;
880                } else {
881                    let line = pty.line_buffer.clone();
882                    deliver_input(pty, waiters, &line)?;
883                    pty.line_buffer.clear();
884                }
885                continue;
886            }
887
888            if byte == pty.termios.cc.verase || byte == 0x08 {
889                if !pty.line_buffer.is_empty() {
890                    pty.line_buffer.pop();
891                    if pty.termios.echo {
892                        deliver_output(pty, waiters, &[0x08, 0x20, 0x08], true)?;
893                    }
894                }
895                continue;
896            }
897
898            if byte == b'\n' {
899                pty.line_buffer.push(b'\n');
900                if pty.termios.echo {
901                    deliver_output(pty, waiters, &[b'\r', b'\n'], true)?;
902                }
903                let line = pty.line_buffer.clone();
904                deliver_input(pty, waiters, &line)?;
905                pty.line_buffer.clear();
906                continue;
907            }
908
909            if pty.line_buffer.len() >= MAX_CANON {
910                continue;
911            }
912            pty.line_buffer.push(byte);
913            if pty.termios.echo {
914                deliver_output(pty, waiters, &[byte], true)?;
915            }
916        } else {
917            if pty.termios.echo {
918                deliver_output(pty, waiters, &[byte], true)?;
919            }
920            deliver_input(pty, waiters, &[byte])?;
921        }
922    }
923
924    Ok(())
925}
926
927fn translate_input(termios: &Termios, data: &[u8]) -> Vec<u8> {
928    if !termios.icrnl || !data.contains(&b'\r') {
929        return data.to_vec();
930    }
931
932    data.iter()
933        .map(|byte| if *byte == b'\r' { b'\n' } else { *byte })
934        .collect()
935}
936
937fn deliver_input(
938    pty: &mut PtyState,
939    waiters: &mut BTreeMap<u64, PendingRead>,
940    data: &[u8],
941) -> PtyResult<()> {
942    if let Some(waiter_id) = pty.waiting_input_reads.pop_front() {
943        if let Some(waiter) = waiters.get_mut(&waiter_id) {
944            waiter.result = Some(Some(data.to_vec()));
945            return Ok(());
946        }
947    }
948
949    if buffer_size(&pty.input_buffer).saturating_add(data.len()) > MAX_PTY_BUFFER_BYTES {
950        return Err(PtyError::would_block("PTY input buffer full"));
951    }
952
953    pty.input_buffer.push_back(data.to_vec());
954    Ok(())
955}
956
957fn deliver_output(
958    pty: &mut PtyState,
959    waiters: &mut BTreeMap<u64, PendingRead>,
960    data: &[u8],
961    echo: bool,
962) -> PtyResult<()> {
963    if let Some(waiter_id) = pty.waiting_output_reads.pop_front() {
964        if let Some(waiter) = waiters.get_mut(&waiter_id) {
965            waiter.result = Some(Some(data.to_vec()));
966            return Ok(());
967        }
968    }
969
970    if buffer_size(&pty.output_buffer).saturating_add(data.len()) > MAX_PTY_BUFFER_BYTES {
971        let message = if echo {
972            "PTY output buffer full (echo backpressure)"
973        } else {
974            "PTY output buffer full"
975        };
976        return Err(PtyError::would_block(message));
977    }
978
979    pty.output_buffer.push_back(data.to_vec());
980    Ok(())
981}
982
983fn signal_for_byte(termios: &Termios, byte: u8) -> Option<i32> {
984    if byte == termios.cc.vintr {
985        return Some(SIGINT);
986    }
987    if byte == termios.cc.vquit {
988        return Some(SIGQUIT);
989    }
990    if byte == termios.cc.vsusp {
991        return Some(SIGTSTP);
992    }
993    None
994}
995
996fn buffer_size(buffer: &VecDeque<Vec<u8>>) -> usize {
997    buffer.iter().map(Vec::len).sum()
998}
999
1000fn available_capacity(buffer: &VecDeque<Vec<u8>>) -> usize {
1001    MAX_PTY_BUFFER_BYTES.saturating_sub(buffer_size(buffer))
1002}
1003
1004fn drain_buffer(buffer: &mut VecDeque<Vec<u8>>, length: usize) -> Vec<u8> {
1005    let mut chunks = Vec::new();
1006    let mut remaining = length;
1007
1008    while remaining > 0 {
1009        let Some(chunk) = buffer.pop_front() else {
1010            break;
1011        };
1012        if chunk.len() <= remaining {
1013            remaining -= chunk.len();
1014            chunks.push(chunk);
1015        } else {
1016            let (head, tail) = chunk.split_at(remaining);
1017            chunks.push(head.to_vec());
1018            buffer.push_front(tail.to_vec());
1019            remaining = 0;
1020        }
1021    }
1022
1023    if chunks.len() == 1 {
1024        return chunks.pop().expect("single chunk should exist");
1025    }
1026
1027    let total = chunks.iter().map(Vec::len).sum();
1028    let mut result = Vec::with_capacity(total);
1029    for chunk in chunks {
1030        result.extend_from_slice(&chunk);
1031    }
1032    result
1033}
1034
1035fn lock_or_recover<'a, T>(mutex: &'a Mutex<T>) -> MutexGuard<'a, T> {
1036    match mutex.lock() {
1037        Ok(guard) => guard,
1038        Err(poisoned) => poisoned.into_inner(),
1039    }
1040}
1041
1042fn wait_or_recover<'a, T>(condvar: &Condvar, guard: MutexGuard<'a, T>) -> MutexGuard<'a, T> {
1043    match condvar.wait(guard) {
1044        Ok(guard) => guard,
1045        Err(poisoned) => poisoned.into_inner(),
1046    }
1047}
1048
1049fn wait_timeout_or_recover<'a, T>(
1050    condvar: &Condvar,
1051    guard: MutexGuard<'a, T>,
1052    timeout: Duration,
1053) -> (MutexGuard<'a, T>, std::sync::WaitTimeoutResult) {
1054    match condvar.wait_timeout(guard, timeout) {
1055        Ok(result) => result,
1056        Err(poisoned) => poisoned.into_inner(),
1057    }
1058}