Skip to main content

agentos_kernel/
pty.rs

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