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