Skip to main content

agentos_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;
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 canonical: Option<bool>,
68    pub echo: Option<bool>,
69    pub isig: Option<bool>,
70    pub opost: Option<bool>,
71    pub onlcr: Option<bool>,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct Termios {
76    pub icrnl: bool,
77    pub opost: bool,
78    pub onlcr: bool,
79    pub icanon: bool,
80    pub echo: bool,
81    pub isig: bool,
82    pub cc: TermiosControlChars,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
86pub struct PartialTermios {
87    pub icrnl: Option<bool>,
88    pub opost: Option<bool>,
89    pub onlcr: Option<bool>,
90    pub icanon: Option<bool>,
91    pub echo: Option<bool>,
92    pub isig: Option<bool>,
93    pub cc: Option<PartialTermiosControlChars>,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct TermiosControlChars {
98    pub vintr: u8,
99    pub vquit: u8,
100    pub vsusp: u8,
101    pub veof: u8,
102    pub verase: u8,
103    pub vkill: u8,
104    pub vwerase: u8,
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
108pub struct PartialTermiosControlChars {
109    pub vintr: Option<u8>,
110    pub vquit: Option<u8>,
111    pub vsusp: Option<u8>,
112    pub veof: Option<u8>,
113    pub verase: Option<u8>,
114    pub vkill: Option<u8>,
115    pub vwerase: Option<u8>,
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub struct PtyWindowSize {
120    pub cols: u16,
121    pub rows: u16,
122}
123
124impl Default for PtyWindowSize {
125    fn default() -> Self {
126        Self {
127            cols: DEFAULT_PTY_COLUMNS,
128            rows: DEFAULT_PTY_ROWS,
129        }
130    }
131}
132
133impl Default for Termios {
134    fn default() -> Self {
135        Self {
136            icrnl: true,
137            opost: true,
138            onlcr: true,
139            icanon: true,
140            echo: true,
141            isig: true,
142            cc: TermiosControlChars {
143                vintr: 0x03,
144                vquit: 0x1c,
145                vsusp: 0x1a,
146                veof: 0x04,
147                verase: 0x7f,
148                vkill: 0x15,
149                vwerase: 0x17,
150            },
151        }
152    }
153}
154
155impl Termios {
156    fn merge(&mut self, update: PartialTermios) {
157        if let Some(icrnl) = update.icrnl {
158            self.icrnl = icrnl;
159        }
160        if let Some(opost) = update.opost {
161            self.opost = opost;
162        }
163        if let Some(onlcr) = update.onlcr {
164            self.onlcr = onlcr;
165        }
166        if let Some(icanon) = update.icanon {
167            self.icanon = icanon;
168        }
169        if let Some(echo) = update.echo {
170            self.echo = echo;
171        }
172        if let Some(isig) = update.isig {
173            self.isig = isig;
174        }
175        if let Some(cc) = update.cc {
176            self.cc.merge(cc);
177        }
178    }
179}
180
181impl TermiosControlChars {
182    fn merge(&mut self, update: PartialTermiosControlChars) {
183        if let Some(vintr) = update.vintr {
184            self.vintr = vintr;
185        }
186        if let Some(vquit) = update.vquit {
187            self.vquit = vquit;
188        }
189        if let Some(vsusp) = update.vsusp {
190            self.vsusp = vsusp;
191        }
192        if let Some(veof) = update.veof {
193            self.veof = veof;
194        }
195        if let Some(verase) = update.verase {
196            self.verase = verase;
197        }
198        if let Some(vkill) = update.vkill {
199            self.vkill = vkill;
200        }
201        if let Some(vwerase) = update.vwerase {
202            self.vwerase = vwerase;
203        }
204    }
205}
206
207#[derive(Debug, Clone)]
208pub struct PtyEnd {
209    pub description: SharedFileDescription,
210    pub filetype: u8,
211}
212
213#[derive(Debug, Clone)]
214pub struct PtyPair {
215    pub master: PtyEnd,
216    pub slave: PtyEnd,
217    pub path: String,
218}
219
220#[derive(Debug, Clone, Copy, PartialEq, Eq)]
221struct PtyRef {
222    pty_id: u64,
223    end: PtyEndKind,
224}
225
226#[derive(Debug, Clone, Copy, PartialEq, Eq)]
227enum PtyEndKind {
228    Master,
229    Slave,
230}
231
232#[derive(Debug, Default)]
233struct PendingRead {
234    length: usize,
235    result: Option<Option<Vec<u8>>>,
236}
237
238#[derive(Debug, Clone, Default)]
239struct PtyState {
240    path: String,
241    input_buffer: VecDeque<Vec<u8>>,
242    output_buffer: VecDeque<Vec<u8>>,
243    input_eof_pending: bool,
244    closed_master: bool,
245    closed_slave: bool,
246    waiting_input_reads: VecDeque<u64>,
247    waiting_output_reads: VecDeque<u64>,
248    termios: Termios,
249    line_buffer: Vec<u8>,
250    foreground_pgid: u32,
251    window_size: PtyWindowSize,
252}
253
254#[derive(Debug)]
255struct PtyManagerState {
256    ptys: BTreeMap<u64, PtyState>,
257    desc_to_pty: BTreeMap<u64, PtyRef>,
258    waiters: BTreeMap<u64, PendingRead>,
259    next_pty_id: u64,
260    next_desc_id: u64,
261    next_waiter_id: u64,
262}
263
264impl Default for PtyManagerState {
265    fn default() -> Self {
266        Self {
267            ptys: BTreeMap::new(),
268            desc_to_pty: BTreeMap::new(),
269            waiters: BTreeMap::new(),
270            next_pty_id: 0,
271            next_desc_id: 200_000,
272            next_waiter_id: 1,
273        }
274    }
275}
276
277#[derive(Debug)]
278struct PtyManagerInner {
279    state: Mutex<PtyManagerState>,
280    waiters: Condvar,
281}
282
283#[derive(Clone)]
284pub struct PtyManager {
285    inner: Arc<PtyManagerInner>,
286    on_signal: Option<SignalHandler>,
287    notifier: Option<PollNotifier>,
288}
289
290impl Default for PtyManager {
291    fn default() -> Self {
292        Self {
293            inner: Arc::new(PtyManagerInner {
294                state: Mutex::new(PtyManagerState::default()),
295                waiters: Condvar::new(),
296            }),
297            on_signal: None,
298            notifier: None,
299        }
300    }
301}
302
303impl PtyManager {
304    pub fn new() -> Self {
305        Self::default()
306    }
307
308    pub fn with_signal_handler(on_signal: SignalHandler) -> Self {
309        let mut manager = Self::new();
310        manager.on_signal = Some(on_signal);
311        manager
312    }
313
314    pub(crate) fn with_signal_handler_and_notifier(
315        on_signal: SignalHandler,
316        notifier: PollNotifier,
317    ) -> Self {
318        let mut manager = Self::with_notifier(notifier);
319        manager.on_signal = Some(on_signal);
320        manager
321    }
322
323    pub(crate) fn with_notifier(notifier: PollNotifier) -> Self {
324        Self {
325            notifier: Some(notifier),
326            ..Self::default()
327        }
328    }
329
330    pub fn create_pty(&self) -> PtyPair {
331        let mut state = lock_or_recover(&self.inner.state);
332        let pty_id = state.next_pty_id;
333        state.next_pty_id += 1;
334
335        let master_id = state.next_desc_id;
336        state.next_desc_id += 1;
337        let slave_id = state.next_desc_id;
338        state.next_desc_id += 1;
339
340        let path = format!("/dev/pts/{pty_id}");
341        state.ptys.insert(
342            pty_id,
343            PtyState {
344                path: path.clone(),
345                termios: Termios::default(),
346                window_size: PtyWindowSize::default(),
347                ..PtyState::default()
348            },
349        );
350        state.desc_to_pty.insert(
351            master_id,
352            PtyRef {
353                pty_id,
354                end: PtyEndKind::Master,
355            },
356        );
357        state.desc_to_pty.insert(
358            slave_id,
359            PtyRef {
360                pty_id,
361                end: PtyEndKind::Slave,
362            },
363        );
364        drop(state);
365
366        PtyPair {
367            master: PtyEnd {
368                description: Arc::new(FileDescription::with_ref_count(
369                    master_id,
370                    format!("pty:{pty_id}:master"),
371                    O_RDWR,
372                    0,
373                )),
374                filetype: FILETYPE_CHARACTER_DEVICE,
375            },
376            slave: PtyEnd {
377                description: Arc::new(FileDescription::with_ref_count(
378                    slave_id,
379                    path.clone(),
380                    O_RDWR,
381                    0,
382                )),
383                filetype: FILETYPE_CHARACTER_DEVICE,
384            },
385            path,
386        }
387    }
388
389    pub fn create_pty_fds(&self, fd_table: &mut ProcessFdTable) -> FdResult<(u32, u32, String)> {
390        let pty = self.create_pty();
391        let master_fd = fd_table.open_with(
392            Arc::clone(&pty.master.description),
393            FILETYPE_CHARACTER_DEVICE,
394            None,
395        )?;
396        match fd_table.open_with(
397            Arc::clone(&pty.slave.description),
398            FILETYPE_CHARACTER_DEVICE,
399            None,
400        ) {
401            Ok(slave_fd) => Ok((master_fd, slave_fd, pty.path)),
402            Err(error) => {
403                fd_table.close(master_fd);
404                self.close(pty.master.description.id());
405                self.close(pty.slave.description.id());
406                Err(error)
407            }
408        }
409    }
410
411    pub fn poll(&self, description_id: u64, requested: PollEvents) -> PtyResult<PollEvents> {
412        let state = lock_or_recover(&self.inner.state);
413        let pty_ref = state
414            .desc_to_pty
415            .get(&description_id)
416            .copied()
417            .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
418        let pty = state
419            .ptys
420            .get(&pty_ref.pty_id)
421            .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?;
422
423        let mut events = PollEvents::empty();
424        match pty_ref.end {
425            PtyEndKind::Master => {
426                if requested.intersects(POLLIN) && !pty.output_buffer.is_empty() {
427                    events |= POLLIN;
428                }
429                if pty.closed_slave {
430                    events |= POLLHUP;
431                } else if requested.intersects(POLLOUT)
432                    && (available_capacity(&pty.input_buffer) > 0
433                        || !pty.waiting_input_reads.is_empty())
434                {
435                    events |= POLLOUT;
436                }
437            }
438            PtyEndKind::Slave => {
439                if requested.intersects(POLLIN)
440                    && (pty.input_eof_pending || !pty.input_buffer.is_empty())
441                {
442                    events |= POLLIN;
443                }
444                if pty.closed_master {
445                    events |= POLLHUP;
446                } else if requested.intersects(POLLOUT)
447                    && (available_capacity(&pty.output_buffer) > 0
448                        || !pty.waiting_output_reads.is_empty())
449                {
450                    events |= POLLOUT;
451                }
452            }
453        }
454
455        Ok(events)
456    }
457
458    pub fn write(&self, description_id: u64, data: impl AsRef<[u8]>) -> PtyResult<usize> {
459        let payload = data.as_ref();
460        let mut signals = Vec::new();
461
462        {
463            let mut state = lock_or_recover(&self.inner.state);
464            let pty_ref = state
465                .desc_to_pty
466                .get(&description_id)
467                .copied()
468                .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
469            let PtyManagerState { ptys, waiters, .. } = &mut *state;
470            let pty = ptys
471                .get_mut(&pty_ref.pty_id)
472                .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?;
473
474            match pty_ref.end {
475                PtyEndKind::Master => {
476                    if pty.closed_master {
477                        return Err(PtyError::io("master closed"));
478                    }
479                    if pty.closed_slave {
480                        return Err(PtyError::io("slave closed"));
481                    }
482                    process_input(pty, waiters, payload, &mut signals)?;
483                }
484                PtyEndKind::Slave => {
485                    if pty.closed_slave {
486                        return Err(PtyError::io("slave closed"));
487                    }
488                    if pty.closed_master {
489                        return Err(PtyError::io("master closed"));
490                    }
491
492                    let processed = process_output(&pty.termios, payload);
493                    deliver_output(pty, waiters, &processed, false)?;
494                    // Terminal emulation: answer a Device Status Report cursor-position
495                    // query (ESC[6n) with a cursor report (ESC[row;colR) on the slave's
496                    // input. A real terminal emulator on the master side does this; the
497                    // converged PTY may have no such emulator, so crossterm/reedline guests
498                    // that probe the cursor at startup would otherwise stall and abort.
499                    if contains_dsr_cursor_query(payload) {
500                        deliver_input(pty, waiters, b"\x1b[1;1R")?;
501                    }
502                }
503            }
504        }
505
506        self.notify_waiters_and_pollers();
507        if let Some(on_signal) = &self.on_signal {
508            for (pgid, signal) in signals {
509                if pgid > 0 {
510                    on_signal(pgid, signal);
511                }
512            }
513        }
514
515        Ok(payload.len())
516    }
517
518    pub fn read(&self, description_id: u64, length: usize) -> PtyResult<Option<Vec<u8>>> {
519        self.read_with_timeout(description_id, length, None)
520    }
521
522    pub fn read_with_timeout(
523        &self,
524        description_id: u64,
525        length: usize,
526        timeout: Option<Duration>,
527    ) -> PtyResult<Option<Vec<u8>>> {
528        let mut state = lock_or_recover(&self.inner.state);
529        let pty_ref = state
530            .desc_to_pty
531            .get(&description_id)
532            .copied()
533            .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
534        let mut waiter_id = None;
535        let deadline = timeout.map(|duration| Instant::now() + duration);
536
537        loop {
538            if let Some(id) = waiter_id {
539                if let Some(waiter) = state.waiters.get_mut(&id) {
540                    if let Some(result) = waiter.result.take() {
541                        state.waiters.remove(&id);
542                        return Ok(result);
543                    }
544                }
545            }
546
547            {
548                let pty = state
549                    .ptys
550                    .get_mut(&pty_ref.pty_id)
551                    .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?;
552
553                match pty_ref.end {
554                    PtyEndKind::Master => {
555                        if pty.closed_master {
556                            if let Some(id) = waiter_id {
557                                state.waiters.remove(&id);
558                            }
559                            return Err(PtyError::io("master closed"));
560                        }
561
562                        if !pty.output_buffer.is_empty() {
563                            let result = drain_buffer(&mut pty.output_buffer, length);
564                            // This reader consumed buffered data directly, so its queued waiter
565                            // entry must be removed or a later delivery will assign data to an
566                            // orphan.
567                            if let Some(id) = waiter_id.take() {
568                                pty.waiting_input_reads.retain(|queued| *queued != id);
569                                pty.waiting_output_reads.retain(|queued| *queued != id);
570                                state.waiters.remove(&id);
571                            }
572                            self.notify_waiters_and_pollers();
573                            return Ok(Some(result));
574                        }
575
576                        if pty.closed_slave {
577                            if let Some(id) = waiter_id {
578                                state.waiters.remove(&id);
579                            }
580                            return Ok(None);
581                        }
582                    }
583                    PtyEndKind::Slave => {
584                        if pty.closed_slave {
585                            if let Some(id) = waiter_id {
586                                state.waiters.remove(&id);
587                            }
588                            return Err(PtyError::io("slave closed"));
589                        }
590
591                        if !pty.input_buffer.is_empty() {
592                            let result = drain_buffer(&mut pty.input_buffer, length);
593                            // This reader consumed buffered data directly, so its queued waiter
594                            // entry must be removed or a later delivery will assign data to an
595                            // orphan.
596                            if let Some(id) = waiter_id.take() {
597                                pty.waiting_input_reads.retain(|queued| *queued != id);
598                                pty.waiting_output_reads.retain(|queued| *queued != id);
599                                state.waiters.remove(&id);
600                            }
601                            self.notify_waiters_and_pollers();
602                            return Ok(Some(result));
603                        }
604
605                        if pty.input_eof_pending {
606                            pty.input_eof_pending = false;
607                            if let Some(id) = waiter_id {
608                                state.waiters.remove(&id);
609                            }
610                            self.notify_waiters_and_pollers();
611                            return Ok(None);
612                        }
613
614                        if pty.closed_master {
615                            if let Some(id) = waiter_id {
616                                state.waiters.remove(&id);
617                            }
618                            return Ok(None);
619                        }
620                    }
621                }
622            }
623
624            let id = if let Some(id) = waiter_id {
625                id
626            } else {
627                let next = state.next_waiter_id;
628                state.next_waiter_id += 1;
629                state.waiters.insert(
630                    next,
631                    PendingRead {
632                        length,
633                        result: None,
634                    },
635                );
636                let Some(pty) = state.ptys.get_mut(&pty_ref.pty_id) else {
637                    state.waiters.remove(&next);
638                    return Err(PtyError::bad_file_descriptor("PTY not found"));
639                };
640                match pty_ref.end {
641                    PtyEndKind::Master => pty.waiting_output_reads.push_back(next),
642                    PtyEndKind::Slave => pty.waiting_input_reads.push_back(next),
643                }
644                self.notify_waiters_and_pollers();
645                waiter_id = Some(next);
646                next
647            };
648
649            let Some(deadline) = deadline else {
650                state = wait_or_recover(&self.inner.waiters, state);
651                if !state.waiters.contains_key(&id) {
652                    waiter_id = None;
653                }
654                continue;
655            };
656
657            let now = Instant::now();
658            if now >= deadline {
659                if let Some(id) = waiter_id.take() {
660                    state.waiters.remove(&id);
661                    if let Some(pty) = state.ptys.get_mut(&pty_ref.pty_id) {
662                        pty.waiting_input_reads.retain(|queued| *queued != id);
663                        pty.waiting_output_reads.retain(|queued| *queued != id);
664                    }
665                    self.notify_waiters_and_pollers();
666                }
667                return Err(PtyError::would_block("PTY read timed out"));
668            }
669
670            let remaining = deadline.saturating_duration_since(now);
671            let (next_state, wait_result) =
672                wait_timeout_or_recover(&self.inner.waiters, state, remaining);
673            state = next_state;
674            if !state.waiters.contains_key(&id) {
675                waiter_id = None;
676            }
677            if wait_result.timed_out() {
678                if let Some(id) = waiter_id.take() {
679                    state.waiters.remove(&id);
680                    if let Some(pty) = state.ptys.get_mut(&pty_ref.pty_id) {
681                        pty.waiting_input_reads.retain(|queued| *queued != id);
682                        pty.waiting_output_reads.retain(|queued| *queued != id);
683                    }
684                    self.notify_waiters_and_pollers();
685                }
686                return Err(PtyError::would_block("PTY read timed out"));
687            }
688        }
689    }
690
691    pub fn close(&self, description_id: u64) {
692        let mut state = lock_or_recover(&self.inner.state);
693        let Some(pty_ref) = state.desc_to_pty.remove(&description_id) else {
694            return;
695        };
696
697        let (waiter_ids, remove_pty) = if let Some(pty) = state.ptys.get_mut(&pty_ref.pty_id) {
698            match pty_ref.end {
699                PtyEndKind::Master => {
700                    pty.closed_master = true;
701                    let mut waiters = pty.waiting_input_reads.drain(..).collect::<Vec<_>>();
702                    waiters.extend(pty.waiting_output_reads.drain(..));
703                    (waiters, pty.closed_master && pty.closed_slave)
704                }
705                PtyEndKind::Slave => {
706                    pty.closed_slave = true;
707                    let mut waiters = pty.waiting_output_reads.drain(..).collect::<Vec<_>>();
708                    waiters.extend(pty.waiting_input_reads.drain(..));
709                    (waiters, pty.closed_master && pty.closed_slave)
710                }
711            }
712        } else {
713            (Vec::new(), false)
714        };
715
716        for waiter_id in waiter_ids {
717            if let Some(waiter) = state.waiters.get_mut(&waiter_id) {
718                waiter.result = Some(None);
719            }
720        }
721
722        if remove_pty {
723            state.ptys.remove(&pty_ref.pty_id);
724        }
725        self.notify_waiters_and_pollers();
726    }
727
728    pub fn is_pty(&self, description_id: u64) -> bool {
729        lock_or_recover(&self.inner.state)
730            .desc_to_pty
731            .contains_key(&description_id)
732    }
733
734    pub fn is_slave(&self, description_id: u64) -> bool {
735        lock_or_recover(&self.inner.state)
736            .desc_to_pty
737            .get(&description_id)
738            .map(|pty_ref| pty_ref.end == PtyEndKind::Slave)
739            .unwrap_or(false)
740    }
741
742    pub fn set_discipline(
743        &self,
744        description_id: u64,
745        config: LineDisciplineConfig,
746    ) -> PtyResult<()> {
747        let mut state = lock_or_recover(&self.inner.state);
748        let pty_ref = state
749            .desc_to_pty
750            .get(&description_id)
751            .copied()
752            .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
753        let pty = state
754            .ptys
755            .get_mut(&pty_ref.pty_id)
756            .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?;
757        if let Some(canonical) = config.canonical {
758            pty.termios.icanon = canonical;
759        }
760        if let Some(echo) = config.echo {
761            pty.termios.echo = echo;
762        }
763        if let Some(isig) = config.isig {
764            pty.termios.isig = isig;
765        }
766        if let Some(opost) = config.opost {
767            pty.termios.opost = opost;
768        }
769        if let Some(onlcr) = config.onlcr {
770            pty.termios.onlcr = onlcr;
771        }
772        Ok(())
773    }
774
775    pub fn get_termios(&self, description_id: u64) -> PtyResult<Termios> {
776        let state = lock_or_recover(&self.inner.state);
777        let pty_ref = state
778            .desc_to_pty
779            .get(&description_id)
780            .copied()
781            .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
782        state
783            .ptys
784            .get(&pty_ref.pty_id)
785            .cloned()
786            .map(|pty| pty.termios)
787            .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))
788    }
789
790    pub fn set_termios(&self, description_id: u64, termios: PartialTermios) -> PtyResult<()> {
791        let mut state = lock_or_recover(&self.inner.state);
792        let pty_ref = state
793            .desc_to_pty
794            .get(&description_id)
795            .copied()
796            .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
797        let pty = state
798            .ptys
799            .get_mut(&pty_ref.pty_id)
800            .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?;
801        pty.termios.merge(termios);
802        Ok(())
803    }
804
805    pub fn set_foreground_pgid(&self, description_id: u64, pgid: u32) -> PtyResult<()> {
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        pty.foreground_pgid = pgid;
817        Ok(())
818    }
819
820    pub fn get_foreground_pgid(&self, description_id: u64) -> PtyResult<u32> {
821        let state = lock_or_recover(&self.inner.state);
822        let pty_ref = state
823            .desc_to_pty
824            .get(&description_id)
825            .copied()
826            .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
827        state
828            .ptys
829            .get(&pty_ref.pty_id)
830            .map(|pty| pty.foreground_pgid)
831            .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))
832    }
833
834    pub fn window_size(&self, description_id: u64) -> PtyResult<PtyWindowSize> {
835        let state = lock_or_recover(&self.inner.state);
836        let pty_ref = state
837            .desc_to_pty
838            .get(&description_id)
839            .copied()
840            .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
841        state
842            .ptys
843            .get(&pty_ref.pty_id)
844            .map(|pty| pty.window_size)
845            .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))
846    }
847
848    pub fn resize(&self, description_id: u64, cols: u16, rows: u16) -> PtyResult<Option<u32>> {
849        let mut state = lock_or_recover(&self.inner.state);
850        let pty_ref = state
851            .desc_to_pty
852            .get(&description_id)
853            .copied()
854            .ok_or_else(|| PtyError::bad_file_descriptor("not a PTY end"))?;
855        let pty = state
856            .ptys
857            .get_mut(&pty_ref.pty_id)
858            .ok_or_else(|| PtyError::bad_file_descriptor("PTY not found"))?;
859        let next_size = PtyWindowSize { cols, rows };
860        if pty.window_size == next_size {
861            return Ok(None);
862        }
863        pty.window_size = next_size;
864        Ok((pty.foreground_pgid > 0).then_some(pty.foreground_pgid))
865    }
866
867    pub fn pty_count(&self) -> usize {
868        lock_or_recover(&self.inner.state).ptys.len()
869    }
870
871    pub fn buffered_input_bytes(&self) -> usize {
872        lock_or_recover(&self.inner.state)
873            .ptys
874            .values()
875            .map(|pty| buffer_size(&pty.input_buffer))
876            .sum()
877    }
878
879    pub fn buffered_output_bytes(&self) -> usize {
880        lock_or_recover(&self.inner.state)
881            .ptys
882            .values()
883            .map(|pty| buffer_size(&pty.output_buffer))
884            .sum()
885    }
886
887    pub fn pending_read_waiter_count(&self) -> usize {
888        lock_or_recover(&self.inner.state).waiters.len()
889    }
890
891    pub fn queued_read_waiter_count(&self) -> usize {
892        lock_or_recover(&self.inner.state)
893            .ptys
894            .values()
895            .map(|pty| pty.waiting_input_reads.len() + pty.waiting_output_reads.len())
896            .sum()
897    }
898
899    pub fn path_for(&self, description_id: u64) -> Option<String> {
900        let state = lock_or_recover(&self.inner.state);
901        let pty_ref = state.desc_to_pty.get(&description_id)?;
902        state.ptys.get(&pty_ref.pty_id).map(|pty| pty.path.clone())
903    }
904
905    fn notify_waiters_and_pollers(&self) {
906        self.inner.waiters.notify_all();
907        if let Some(notifier) = &self.notifier {
908            notifier.notify();
909        }
910    }
911}
912
913/// True if `data` contains a Device Status Report cursor-position query
914/// (`ESC [ 6 n`). Used to drive the converged PTY's terminal-style auto-reply.
915fn contains_dsr_cursor_query(data: &[u8]) -> bool {
916    const QUERY: &[u8] = b"\x1b[6n";
917    data.windows(QUERY.len()).any(|window| window == QUERY)
918}
919
920fn process_output(termios: &Termios, data: &[u8]) -> Vec<u8> {
921    if !termios.opost || !termios.onlcr || !data.contains(&b'\n') {
922        return data.to_vec();
923    }
924
925    let extra_crs = data
926        .iter()
927        .enumerate()
928        .filter(|(index, byte)| **byte == b'\n' && (*index == 0 || data[*index - 1] != b'\r'))
929        .count();
930    if extra_crs == 0 {
931        return data.to_vec();
932    }
933
934    let mut result = Vec::with_capacity(data.len() + extra_crs);
935    for (index, byte) in data.iter().enumerate() {
936        if *byte == b'\n' && (index == 0 || data[index - 1] != b'\r') {
937            result.push(b'\r');
938        }
939        result.push(*byte);
940    }
941    result
942}
943
944fn process_input(
945    pty: &mut PtyState,
946    waiters: &mut BTreeMap<u64, PendingRead>,
947    data: &[u8],
948    signals: &mut Vec<(u32, i32)>,
949) -> PtyResult<()> {
950    if !pty.termios.icanon && !pty.termios.echo && !pty.termios.isig {
951        let translated = translate_input(&pty.termios, data);
952        deliver_input(pty, waiters, &translated)?;
953        return Ok(());
954    }
955
956    for mut byte in data.iter().copied() {
957        if pty.termios.icrnl && byte == b'\r' {
958            byte = b'\n';
959        }
960
961        if pty.termios.isig {
962            if let Some(signal) = signal_for_byte(&pty.termios, byte) {
963                if pty.termios.icanon {
964                    pty.line_buffer.clear();
965                }
966                let has_foreground_process_group = pty.foreground_pgid > 0;
967                // Only echo the signal-generating control char (e.g. "^C") as a
968                // line-editor fallback when there is NO foreground process group
969                // to receive the signal. With a foreground process group the
970                // signal is delivered to it and the char is not echoed, matching
971                // the integration suite's VINTR/VSUSP/VQUIT expectations.
972                if pty.termios.echo && !has_foreground_process_group {
973                    deliver_output(pty, waiters, &echo_control_byte(byte), true)?;
974                    if pty.termios.icanon {
975                        deliver_output(pty, waiters, b"\r\n", true)?;
976                    }
977                }
978                if has_foreground_process_group {
979                    signals.push((pty.foreground_pgid, signal));
980                } else if pty.termios.icanon {
981                    deliver_input(pty, waiters, b"\n")?;
982                }
983                continue;
984            }
985        }
986
987        if pty.termios.icanon {
988            if byte == pty.termios.cc.veof {
989                if pty.line_buffer.is_empty() {
990                    deliver_input_eof(pty, waiters);
991                } else {
992                    let line = pty.line_buffer.clone();
993                    deliver_input(pty, waiters, &line)?;
994                    pty.line_buffer.clear();
995                }
996                continue;
997            }
998
999            if byte == pty.termios.cc.verase || byte == 0x08 {
1000                if let Some(&erased) = pty.line_buffer.last() {
1001                    if pty.termios.echo {
1002                        deliver_output(pty, waiters, &erase_sequence(erased), true)?;
1003                    }
1004                    pty.line_buffer.pop();
1005                }
1006                continue;
1007            }
1008
1009            if byte == pty.termios.cc.vkill {
1010                if !pty.line_buffer.is_empty() {
1011                    if pty.termios.echo {
1012                        let erase: Vec<u8> = pty
1013                            .line_buffer
1014                            .iter()
1015                            .flat_map(|b| erase_sequence(*b))
1016                            .collect();
1017                        deliver_output(pty, waiters, &erase, true)?;
1018                    }
1019                    pty.line_buffer.clear();
1020                }
1021                continue;
1022            }
1023
1024            if byte == pty.termios.cc.vwerase {
1025                let mut erased: Vec<u8> = Vec::new();
1026                while matches!(pty.line_buffer.last(), Some(b' ') | Some(b'\t')) {
1027                    if let Some(b) = pty.line_buffer.pop() {
1028                        erased.push(b);
1029                    }
1030                }
1031                while let Some(&b) = pty.line_buffer.last() {
1032                    if b == b' ' || b == b'\t' {
1033                        break;
1034                    }
1035                    pty.line_buffer.pop();
1036                    erased.push(b);
1037                }
1038                if pty.termios.echo && !erased.is_empty() {
1039                    let sequence: Vec<u8> =
1040                        erased.iter().flat_map(|b| erase_sequence(*b)).collect();
1041                    deliver_output(pty, waiters, &sequence, true)?;
1042                }
1043                continue;
1044            }
1045
1046            if byte == b'\n' {
1047                let mut line = pty.line_buffer.clone();
1048                line.push(b'\n');
1049                if pty.termios.echo {
1050                    deliver_output(pty, waiters, b"\r\n", true)?;
1051                }
1052                deliver_input(pty, waiters, &line)?;
1053                pty.line_buffer.clear();
1054                continue;
1055            }
1056
1057            if pty.line_buffer.len() >= MAX_CANON {
1058                continue;
1059            }
1060            if pty.termios.echo {
1061                // ECHOCTL: echo control chars in caret form (e.g. 0x01 -> "^A")
1062                // so they are visible; printable bytes echo verbatim.
1063                deliver_output(pty, waiters, &echo_control_byte(byte), true)?;
1064            }
1065            pty.line_buffer.push(byte);
1066        } else {
1067            if pty.termios.echo {
1068                deliver_output(pty, waiters, &[byte], true)?;
1069            }
1070            deliver_input(pty, waiters, &[byte])?;
1071        }
1072    }
1073
1074    Ok(())
1075}
1076
1077fn translate_input(termios: &Termios, data: &[u8]) -> Vec<u8> {
1078    if !termios.icrnl || !data.contains(&b'\r') {
1079        return data.to_vec();
1080    }
1081
1082    data.iter()
1083        .map(|byte| if *byte == b'\r' { b'\n' } else { *byte })
1084        .collect()
1085}
1086
1087fn deliver_input(
1088    pty: &mut PtyState,
1089    waiters: &mut BTreeMap<u64, PendingRead>,
1090    data: &[u8],
1091) -> PtyResult<()> {
1092    if let Some(waiter_id) = pty.waiting_input_reads.pop_front() {
1093        if let Some(waiter) = waiters.get_mut(&waiter_id) {
1094            if data.len() <= waiter.length {
1095                waiter.result = Some(Some(data.to_vec()));
1096            } else {
1097                // The waiter consumes `waiter.length` bytes directly; only the
1098                // tail is buffered, so the buffer cap must be enforced on the
1099                // tail. Otherwise a single large write past a pending reader
1100                // bypasses MAX_PTY_BUFFER_BYTES entirely.
1101                let tail_len = data.len() - waiter.length;
1102                if tail_len > available_capacity(&pty.input_buffer) {
1103                    pty.waiting_input_reads.push_front(waiter_id);
1104                    return Err(PtyError::would_block("PTY input buffer full"));
1105                }
1106                let (head, tail) = data.split_at(waiter.length);
1107                waiter.result = Some(Some(head.to_vec()));
1108                pty.input_buffer.push_front(tail.to_vec());
1109            }
1110            return Ok(());
1111        }
1112    }
1113
1114    if buffer_size(&pty.input_buffer).saturating_add(data.len()) > MAX_PTY_BUFFER_BYTES {
1115        return Err(PtyError::would_block("PTY input buffer full"));
1116    }
1117
1118    pty.input_buffer.push_back(data.to_vec());
1119    Ok(())
1120}
1121
1122fn deliver_input_eof(pty: &mut PtyState, waiters: &mut BTreeMap<u64, PendingRead>) {
1123    if let Some(waiter_id) = pty.waiting_input_reads.pop_front() {
1124        if let Some(waiter) = waiters.get_mut(&waiter_id) {
1125            waiter.result = Some(None);
1126            return;
1127        }
1128    }
1129
1130    pty.input_eof_pending = true;
1131}
1132
1133fn deliver_output(
1134    pty: &mut PtyState,
1135    waiters: &mut BTreeMap<u64, PendingRead>,
1136    data: &[u8],
1137    echo: bool,
1138) -> PtyResult<()> {
1139    if let Some(waiter_id) = pty.waiting_output_reads.pop_front() {
1140        if let Some(waiter) = waiters.get_mut(&waiter_id) {
1141            if data.len() <= waiter.length {
1142                waiter.result = Some(Some(data.to_vec()));
1143            } else {
1144                // Enforce the buffer cap on the tail (see deliver_input).
1145                let tail_len = data.len() - waiter.length;
1146                if tail_len > available_capacity(&pty.output_buffer) {
1147                    pty.waiting_output_reads.push_front(waiter_id);
1148                    let message = if echo {
1149                        "PTY output buffer full (echo backpressure)"
1150                    } else {
1151                        "PTY output buffer full"
1152                    };
1153                    return Err(PtyError::would_block(message));
1154                }
1155                let (head, tail) = data.split_at(waiter.length);
1156                waiter.result = Some(Some(head.to_vec()));
1157                pty.output_buffer.push_front(tail.to_vec());
1158            }
1159            return Ok(());
1160        }
1161    }
1162
1163    if buffer_size(&pty.output_buffer).saturating_add(data.len()) > MAX_PTY_BUFFER_BYTES {
1164        let message = if echo {
1165            "PTY output buffer full (echo backpressure)"
1166        } else {
1167            "PTY output buffer full"
1168        };
1169        return Err(PtyError::would_block(message));
1170    }
1171
1172    pty.output_buffer.push_back(data.to_vec());
1173    Ok(())
1174}
1175
1176fn signal_for_byte(termios: &Termios, byte: u8) -> Option<i32> {
1177    if byte == termios.cc.vintr {
1178        return Some(SIGINT);
1179    }
1180    if byte == termios.cc.vquit {
1181        return Some(SIGQUIT);
1182    }
1183    if byte == termios.cc.vsusp {
1184        return Some(SIGTSTP);
1185    }
1186    None
1187}
1188
1189fn echo_control_byte(byte: u8) -> Vec<u8> {
1190    if byte < 0x20 {
1191        vec![b'^', byte + 0x40]
1192    } else if byte == 0x7f {
1193        b"^?".to_vec()
1194    } else {
1195        vec![byte]
1196    }
1197}
1198
1199/// Backspace-erase sequence for a single buffered input byte, accounting for how
1200/// wide it was echoed: a control char echoed in caret form (`^X`, ECHOCTL)
1201/// occupies two columns and needs two `BS SP BS` triples, while a printable byte
1202/// occupies one. Used by VERASE / VKILL / VWERASE erase echo so the erased echo
1203/// width matches the displayed width.
1204fn erase_sequence(byte: u8) -> Vec<u8> {
1205    let columns = echo_control_byte(byte).len();
1206    (0..columns).flat_map(|_| [0x08, 0x20, 0x08]).collect()
1207}
1208
1209fn buffer_size(buffer: &VecDeque<Vec<u8>>) -> usize {
1210    buffer.iter().map(Vec::len).sum()
1211}
1212
1213fn available_capacity(buffer: &VecDeque<Vec<u8>>) -> usize {
1214    MAX_PTY_BUFFER_BYTES.saturating_sub(buffer_size(buffer))
1215}
1216
1217fn drain_buffer(buffer: &mut VecDeque<Vec<u8>>, length: usize) -> Vec<u8> {
1218    let mut chunks = Vec::new();
1219    let mut remaining = length;
1220
1221    while remaining > 0 {
1222        let Some(chunk) = buffer.pop_front() else {
1223            break;
1224        };
1225        if chunk.len() <= remaining {
1226            remaining -= chunk.len();
1227            chunks.push(chunk);
1228        } else {
1229            let (head, tail) = chunk.split_at(remaining);
1230            chunks.push(head.to_vec());
1231            buffer.push_front(tail.to_vec());
1232            remaining = 0;
1233        }
1234    }
1235
1236    if chunks.len() == 1 {
1237        return chunks.pop().expect("single chunk should exist");
1238    }
1239
1240    let total = chunks.iter().map(Vec::len).sum();
1241    let mut result = Vec::with_capacity(total);
1242    for chunk in chunks {
1243        result.extend_from_slice(&chunk);
1244    }
1245    result
1246}
1247
1248fn lock_or_recover<'a, T>(mutex: &'a Mutex<T>) -> MutexGuard<'a, T> {
1249    match mutex.lock() {
1250        Ok(guard) => guard,
1251        Err(poisoned) => poisoned.into_inner(),
1252    }
1253}
1254
1255fn wait_or_recover<'a, T>(condvar: &Condvar, guard: MutexGuard<'a, T>) -> MutexGuard<'a, T> {
1256    match condvar.wait(guard) {
1257        Ok(guard) => guard,
1258        Err(poisoned) => poisoned.into_inner(),
1259    }
1260}
1261
1262fn wait_timeout_or_recover<'a, T>(
1263    condvar: &Condvar,
1264    guard: MutexGuard<'a, T>,
1265    timeout: Duration,
1266) -> (MutexGuard<'a, T>, std::sync::WaitTimeoutResult) {
1267    match condvar.wait_timeout(guard, timeout) {
1268        Ok(result) => result,
1269        Err(poisoned) => poisoned.into_inner(),
1270    }
1271}